]> git.ipfire.org Git - thirdparty/vuejs/pinia.git/commitdiff
feat: add devtools support
authorEduardo San Martin Morote <posva13@gmail.com>
Wed, 23 Sep 2020 15:35:41 +0000 (17:35 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Wed, 23 Sep 2020 15:35:41 +0000 (17:35 +0200)
package.json
scripts/check-size.js [new file with mode: 0644]
size-checks/rollup.config.js [new file with mode: 0644]
size-checks/small.js [new file with mode: 0644]
src/devtools.ts [new file with mode: 0644]
src/global.d.ts [new file with mode: 0644]
src/index.ts
src/rootStore.ts
src/store.ts
tsconfig.json
yarn.lock

index dcb77579c74d6f42dd74c6f2c59863f0bd851e8b..61ac9236573574814b8cad123f370f1399cc7172 100644 (file)
@@ -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 (file)
index 0000000..77439dc
--- /dev/null
@@ -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 (file)
index 0000000..5b868ef
--- /dev/null
@@ -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 (file)
index 0000000..5c02b11
--- /dev/null
@@ -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 (file)
index 0000000..aeb897c
--- /dev/null
@@ -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<string, any> = {
+          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 (file)
index 0000000..fac0549
--- /dev/null
@@ -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
index 4ca79b37ccef4b34524f292b501232d5ea0e68b5..8e4abc28f3dbd907251efda6663b9daf5cb7f990 100644 (file)
@@ -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 }
index 83779123689e5dd0a73844383b55eef58f4fdbd4..49160052c765f47556411dbbfb41e5b79613d211 100644 (file)
@@ -1,3 +1,4 @@
+import { App } from 'vue'
 import { NonNullObject, StateTree, GenericStore } from './types'
 
 /**
@@ -55,3 +56,29 @@ export function getRootState(req: NonNullObject): Record<string, StateTree> {
 
   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<GenericStore>()
+
+export function registerStore(store: GenericStore) {
+  stores.add(store)
+}
+
+export const getRegisteredStores = () => stores
index 7a715d7c41de6eef532ec69a09749ac1f366fdd3..efb37c92e3456134b9b7e164ebf11eb2933c2b71 100644 (file)
@@ -15,7 +15,11 @@ import {
   setActiveReq,
   storesMap,
   getInitialState,
+  getClientApp,
 } from './rootStore'
+import { addDevtools } from './devtools'
+
+const IS_CLIENT = typeof window !== 'undefined'
 
 function innerPatch<T extends StateTree>(
   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
index b51241d580c9acc7c46e1c5f218e5f15deb34e47..84d3d695dc6bc3f1378a56968dfff419b7303cf7 100644 (file)
@@ -1,5 +1,9 @@
 {
-  "include": ["src/**/*.ts", "__tests__/**/*.ts"],
+  "include": [
+    "src/global.d.ts",
+    "src/**/*.ts",
+    "__tests__/**/*.ts"
+  ],
   "compilerOptions": {
     "baseUrl": ".",
     "rootDir": ".",
index dc94672c7665f8a0acf58187fc6f0e2c780304b3..39090f9a6ecd6dcf0d7b31e765e5c64876898ca3 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
     "@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"