From: Eduardo San Martin Morote Date: Wed, 23 Sep 2020 15:35:41 +0000 (+0200) Subject: feat: add devtools support X-Git-Tag: v2.0.0-alpha.2~4 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=849cb3f30559e312bf00625a42a7b697c68d9941;p=thirdparty%2Fvuejs%2Fpinia.git feat: add devtools support --- diff --git a/package.json b/package.json index dcb77579..61ac9236 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "scripts": { "build": "rollup -c rollup.config.js", "build:dts": "api-extractor run --local --verbose", + "size": "rollup -c size-checks/rollup.config.js && node scripts/check-size.js", "release": "bash scripts/release.sh", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 1", "lint": "prettier -c --parser typescript \"{src,__tests__,e2e}/**/*.[jt]s?(x)\"", @@ -57,6 +58,8 @@ "@rollup/plugin-replace": "^2.3.3", "@types/jest": "^26.0.14", "@types/node": "^14.11.2", + "@vue/devtools-api": "^6.0.0-beta.2", + "brotli": "^1.3.2", "codecov": "^3.6.1", "conventional-changelog-cli": "^2.1.0", "jest": "^26.4.2", diff --git a/scripts/check-size.js b/scripts/check-size.js new file mode 100644 index 00000000..77439dcf --- /dev/null +++ b/scripts/check-size.js @@ -0,0 +1,24 @@ +const fs = require('fs') +const path = require('path') +const chalk = require('chalk') +const { gzipSync } = require('zlib') +const { compress } = require('brotli') + +function checkFileSize(filePath) { + if (!fs.existsSync(filePath)) { + return + } + const file = fs.readFileSync(filePath) + const minSize = (file.length / 1024).toFixed(2) + 'kb' + const gzipped = gzipSync(file) + const gzippedSize = (gzipped.length / 1024).toFixed(2) + 'kb' + const compressed = compress(file) + const compressedSize = (compressed.length / 1024).toFixed(2) + 'kb' + console.log( + `${chalk.gray( + chalk.bold(path.basename(filePath)) + )} min:${minSize} / gzip:${gzippedSize} / brotli:${compressedSize}` + ) +} + +checkFileSize(path.resolve(__dirname, '../size-checks/dist/small.js')) diff --git a/size-checks/rollup.config.js b/size-checks/rollup.config.js new file mode 100644 index 00000000..5b868ef2 --- /dev/null +++ b/size-checks/rollup.config.js @@ -0,0 +1,58 @@ +import path from 'path' +import ts from 'rollup-plugin-typescript2' +import replace from '@rollup/plugin-replace' +import resolve from '@rollup/plugin-node-resolve' +import commonjs from '@rollup/plugin-commonjs' +import { terser } from 'rollup-plugin-terser' + +/** @type {import('rollup').RollupOptions} */ +const config = { + external: ['vue'], + output: { + file: path.resolve(__dirname, './dist/small.js'), + format: 'es', + }, + input: path.resolve(__dirname, './small.js'), + plugins: [ + replace({ + __DEV__: false, + // this is only used during tests + __TEST__: false, + // If the build is expected to run directly in the browser (global / esm builds) + __BROWSER__: true, + // is targeting bundlers? + __BUNDLER__: false, + __GLOBAL__: false, + // is targeting Node (SSR)? + __NODE_JS__: false, + __VUE_PROD_DEVTOOLS__: false, + }), + ts({ + check: false, + tsconfig: path.resolve(__dirname, '../tsconfig.json'), + cacheRoot: path.resolve(__dirname, '../node_modules/.rts2_cache'), + tsconfigOverride: { + compilerOptions: { + sourceMap: false, + declaration: false, + declarationMap: false, + }, + exclude: ['__tests__', 'test-dts'], + }, + }), + resolve(), + commonjs(), + terser({ + format: { + comments: false, + }, + module: true, + compress: { + ecma: 2015, + pure_getters: true, + }, + }), + ], +} + +export default config diff --git a/size-checks/small.js b/size-checks/small.js new file mode 100644 index 00000000..5c02b114 --- /dev/null +++ b/size-checks/small.js @@ -0,0 +1,5 @@ +import { createPinia, defineStore } from '../dist/pinia.esm-bundler' + +createPinia() +// @ts-ignore +export default defineStore() diff --git a/src/devtools.ts b/src/devtools.ts new file mode 100644 index 00000000..aeb897cd --- /dev/null +++ b/src/devtools.ts @@ -0,0 +1,152 @@ +import { + CustomInspectorNode, + CustomInspectorState, + setupDevtoolsPlugin, +} from '@vue/devtools-api' +import { App } from 'vue' +import { getRegisteredStores, registerStore } from './rootStore' +import { GenericStore, NonNullObject } from './types' + +function formatDisplay(display: string) { + return { + _custom: { + display, + }, + } +} + +let isAlreadyInstalled: boolean | undefined + +export function addDevtools(app: App, store: GenericStore, req: NonNullObject) { + registerStore(store) + setupDevtoolsPlugin( + { + id: 'pinia', + label: 'Pinia 🍍', + app, + }, + (api) => { + api.on.inspectComponent((payload, ctx) => { + if (payload.instanceData) { + payload.instanceData.state.push({ + type: '🍍 ' + store.id, + key: 'state', + editable: false, + value: store.state, + }) + } + }) + + // watch(router.currentRoute, () => { + // // @ts-ignore + // api.notifyComponentUpdate() + // }) + + const mutationsLayerId = 'pinia:mutations' + const piniaInspectorId = 'pinia' + + if (!isAlreadyInstalled) { + api.addTimelineLayer({ + id: mutationsLayerId, + label: `Pinia 🍍`, + color: 0xe5df88, + }) + + api.addInspector({ + id: piniaInspectorId, + label: 'Pinia 🍍', + icon: 'storage', + treeFilterPlaceholder: 'Search stores', + }) + + isAlreadyInstalled = true + } else { + // @ts-ignore + api.notifyComponentUpdate() + api.sendInspectorTree(piniaInspectorId) + api.sendInspectorState(piniaInspectorId) + } + + store.subscribe((mutation, state) => { + // rootStore.state[store.id] = state + const data: Record = { + store: formatDisplay(mutation.storeName), + type: formatDisplay(mutation.type), + } + + if (mutation.payload) { + data.payload = mutation.payload + } + + // @ts-ignore + api.notifyComponentUpdate() + api.sendInspectorState(piniaInspectorId) + + api.addTimelineEvent({ + layerId: mutationsLayerId, + event: { + time: Date.now(), + data, + // TODO: remove when fixed + meta: {}, + }, + }) + }) + + api.on.getInspectorTree((payload) => { + if (payload.app === app && payload.inspectorId === piniaInspectorId) { + const stores = Array.from(getRegisteredStores()) + + payload.rootNodes = (payload.filter + ? stores.filter((store) => + store.id.toLowerCase().includes(payload.filter.toLowerCase()) + ) + : stores + ).map(formatStoreForInspectorTree) + } + }) + + api.on.getInspectorState((payload) => { + if (payload.app === app && payload.inspectorId === piniaInspectorId) { + const stores = Array.from(getRegisteredStores()) + const store = stores.find((store) => store.id === payload.nodeId) + + if (store) { + payload.state = { + options: formatStoreForInspectorState(store), + } + } else { + __VUE_DEVTOOLS_TOAST__( + `🍍 store "${payload.nodeId}" not found`, + 'error' + ) + } + } + }) + + // trigger an update so it can display new registered stores + // @ts-ignore + api.notifyComponentUpdate() + __VUE_DEVTOOLS_TOAST__(`🍍 "${store.id}" store installed`) + } + ) +} + +function formatStoreForInspectorTree(store: GenericStore): CustomInspectorNode { + return { + id: store.id, + label: store.id, + tags: [], + } +} + +function formatStoreForInspectorState( + store: GenericStore +): CustomInspectorState[string] { + const fields: CustomInspectorState[string] = [ + { editable: false, key: 'id', value: formatDisplay(store.id) }, + { editable: true, key: 'state', value: store.state }, + ] + + return fields +} diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 00000000..fac0549f --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,9 @@ +// Global compile-time constants +declare var __DEV__: boolean +declare var __FEATURE_PROD_DEVTOOLS__: boolean +declare var __BROWSER__: boolean +declare var __CI__: boolean +declare var __VUE_DEVTOOLS_TOAST__: ( + message: string, + type?: 'normal' | 'error' | 'warning' +) => void diff --git a/src/index.ts b/src/index.ts index 4ca79b37..8e4abc28 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,10 @@ -export { createStore } from './store' -export { setActiveReq, setStateProvider, getRootState } from './rootStore' +import { createStore } from './store' +export { + setActiveReq, + setStateProvider, + getRootState, + createPinia, +} from './rootStore' export { StateTree, StoreGetter, Store } from './types' +// TODO: deprecate createStore +export { createStore, createStore as defineStore } diff --git a/src/rootStore.ts b/src/rootStore.ts index 83779123..49160052 100644 --- a/src/rootStore.ts +++ b/src/rootStore.ts @@ -1,3 +1,4 @@ +import { App } from 'vue' import { NonNullObject, StateTree, GenericStore } from './types' /** @@ -55,3 +56,29 @@ export function getRootState(req: NonNullObject): Record { return rootState } + +/** + * Client-side application instance used for devtools + */ +export let clientApp: App | undefined +export const setClientApp = (app: App) => (clientApp = app) +export const getClientApp = () => clientApp + +export function createPinia() { + return { + install(app: App) { + setClientApp(app) + }, + } +} + +/** + * Registered stores + */ +export const stores = new Set() + +export function registerStore(store: GenericStore) { + stores.add(store) +} + +export const getRegisteredStores = () => stores diff --git a/src/store.ts b/src/store.ts index 7a715d7c..efb37c92 100644 --- a/src/store.ts +++ b/src/store.ts @@ -15,7 +15,11 @@ import { setActiveReq, storesMap, getInitialState, + getClientApp, } from './rootStore' +import { addDevtools } from './devtools' + +const IS_CLIENT = typeof window !== 'undefined' function innerPatch( target: T, @@ -200,8 +204,19 @@ export function createStore< (store = buildStore(id, state, getters, actions, getInitialState(id))) ) - // TODO: client devtools when availables - // if (isClient) useStoreDevtools(store) + if (IS_CLIENT && __BROWSER__ && (__DEV__ || __FEATURE_PROD_DEVTOOLS__)) { + const app = getClientApp() + if (app) { + addDevtools(app, store, req) + } else { + console.warn( + `[🍍]: store was instantiated before calling\n` + + `app.use(pinia)\n` + + `Make sure to install pinia's plugin by using createPinia:\n` + + `linkto docs TODO` + ) + } + } } return store diff --git a/tsconfig.json b/tsconfig.json index b51241d5..84d3d695 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,9 @@ { - "include": ["src/**/*.ts", "__tests__/**/*.ts"], + "include": [ + "src/global.d.ts", + "src/**/*.ts", + "__tests__/**/*.ts" + ], "compilerOptions": { "baseUrl": ".", "rootDir": ".", diff --git a/yarn.lock b/yarn.lock index dc94672c..39090f9a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1002,6 +1002,11 @@ "@vue/compiler-core" "3.0.0" "@vue/shared" "3.0.0" +"@vue/devtools-api@^6.0.0-beta.2": + version "6.0.0-beta.2" + resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.0.0-beta.2.tgz#833ad3335f97ae9439e26247d97f9baf7b5a6116" + integrity sha512-5k0A8ffjNNukOiceImBdx1e3W5Jbpwqsu7xYHiZVu9mn4rYxFztIt+Q25mOHm7nwvDnMHrE7u5KtY2zmd+81GA== + "@vue/reactivity@3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.0.0.tgz#fd15632a608650ce2a969c721787e27e2c80aa6b" @@ -1299,6 +1304,11 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base64-js@^1.1.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + base@^0.11.1: version "0.11.2" resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" @@ -1350,6 +1360,13 @@ braces@^3.0.1: dependencies: fill-range "^7.0.1" +brotli@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/brotli/-/brotli-1.3.2.tgz#525a9cad4fcba96475d7d388f6aecb13eed52f46" + integrity sha1-UlqcrU/LqWR119OI9q7LE+7VL0Y= + dependencies: + base64-js "^1.1.2" + browser-process-hrtime@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626"