]> git.ipfire.org Git - thirdparty/vuejs/pinia.git/commitdiff
feat(devtools): display pinia without stores
authorEduardo San Martin Morote <posva13@gmail.com>
Thu, 15 Jul 2021 14:23:35 +0000 (16:23 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Mon, 19 Jul 2021 09:52:25 +0000 (11:52 +0200)
playground/src/stores/counterSetup.ts
src/createPinia.ts
src/devtools/index.ts
src/devtools/plugin.ts

index 80dbfc4c799e13adc6b8b9d7b97dfac977f9ac00..97e77c3b34c0c2d75d3c6835a331db49f3c0a2f0 100644 (file)
@@ -43,8 +43,8 @@ export const useCounter = defineSetupStore('counter-setup', () => {
     while (state.n > 0) {
       state.n -= 1
       state.decrementedTimes += 1
+      await delay(interval)
     }
-    await delay(interval)
   }
 
   return {
index e0ecb77c6de1e0ccc9e27b4897ffdb1047bd884f..fb8ce0d63d5916e240e13a6a313ad157807e4c20 100644 (file)
@@ -5,7 +5,7 @@ import {
   piniaSymbol,
 } from './rootStore'
 import { ref, App, markRaw, effectScope } from 'vue'
-import { devtoolsPlugin } from './devtools'
+import { registerPiniaDevtools, devtoolsPlugin } from './devtools'
 import { IS_CLIENT } from './env'
 import { StateTree, Store } from './types'
 
@@ -31,6 +31,9 @@ export function createPinia(): Pinia {
         // this allows calling useStore() outside of a component setup after
         // installing pinia's plugin
         setActivePinia(pinia)
+        if (__DEV__) {
+          registerPiniaDevtools(app, pinia)
+        }
       }
       toBeInstalled.forEach((plugin) => _p.push(plugin))
     },
index d81c448ebb015193ba46a89b8efd64699caa09ac..e2dfab10643cea6b000664d6d460ebb74217b77e 100644 (file)
@@ -1 +1 @@
-export { devtoolsPlugin } from './plugin'
+export { devtoolsPlugin, registerPiniaDevtools } from './plugin'
index 381c5adae11ae3aac1f7be5f4f32e0de8dd2956e..3f41560932f4b059898ba295a2f612587d8e5fc6 100644 (file)
@@ -25,7 +25,6 @@ import {
 } from './formatting'
 import { isPinia, toastMessage } from './utils'
 
-let isAlreadyInstalled: boolean | undefined
 // timeline can be paused when directly changing the state
 let isTimelineActive = true
 const componentStateTypes: string[] = []
@@ -41,11 +40,14 @@ const INSPECTOR_ID = 'pinia'
  */
 const getStoreType = (id: string) => '🍍 ' + id
 
-function addDevtools(app: App, pinia: Pinia, store: Store) {
-  if (!componentStateTypes.includes(getStoreType(store.$id))) {
-    componentStateTypes.push(getStoreType(store.$id))
-  }
-
+/**
+ * Add the pinia plugin without any store. Allows displaying a Pinia plugin tab
+ * as soon as it is added to the application.
+ *
+ * @param app - Vue application
+ * @param pinia - pinia instance
+ */
+export function registerPiniaDevtools(app: App, pinia: Pinia) {
   setupDevtoolsPlugin(
     {
       id: 'dev.esm.pinia',
@@ -57,193 +59,202 @@ function addDevtools(app: App, pinia: Pinia, store: Store) {
       app,
     },
     (api) => {
-      if (!isAlreadyInstalled) {
-        api.addTimelineLayer({
-          id: MUTATIONS_LAYER_ID,
-          label: `Pinia 🍍`,
-          color: 0xe5df88,
-        })
+      api.addTimelineLayer({
+        id: MUTATIONS_LAYER_ID,
+        label: `Pinia 🍍`,
+        color: 0xe5df88,
+      })
 
-        api.addInspector({
-          id: INSPECTOR_ID,
-          label: 'Pinia 🍍',
-          icon: 'storage',
-          treeFilterPlaceholder: 'Search stores',
-          actions: [
-            {
-              icon: 'content_copy',
-              action: () => {
-                actionGlobalCopyState(pinia)
-              },
-              tooltip: 'Serialize and copy the state',
+      api.addInspector({
+        id: INSPECTOR_ID,
+        label: 'Pinia 🍍',
+        icon: 'storage',
+        treeFilterPlaceholder: 'Search stores',
+        actions: [
+          {
+            icon: 'content_copy',
+            action: () => {
+              actionGlobalCopyState(pinia)
             },
-            {
-              icon: 'content_paste',
-              action: async () => {
-                await actionGlobalPasteState(pinia)
-                api.sendInspectorTree(INSPECTOR_ID)
-                api.sendInspectorState(INSPECTOR_ID)
-              },
-              tooltip: 'Replace the state with the content of your clipboard',
+            tooltip: 'Serialize and copy the state',
+          },
+          {
+            icon: 'content_paste',
+            action: async () => {
+              await actionGlobalPasteState(pinia)
+              api.sendInspectorTree(INSPECTOR_ID)
+              api.sendInspectorState(INSPECTOR_ID)
             },
-            {
-              icon: 'save',
-              action: () => {
-                actionGlobalSaveState(pinia)
-              },
-              tooltip: 'Save the state as a JSON file',
+            tooltip: 'Replace the state with the content of your clipboard',
+          },
+          {
+            icon: 'save',
+            action: () => {
+              actionGlobalSaveState(pinia)
             },
-            {
-              icon: 'folder_open',
-              action: async () => {
-                await actionGlobalOpenStateFile(pinia)
-                api.sendInspectorTree(INSPECTOR_ID)
-                api.sendInspectorState(INSPECTOR_ID)
-              },
-              tooltip: 'Import the state from a JSON file',
+            tooltip: 'Save the state as a JSON file',
+          },
+          {
+            icon: 'folder_open',
+            action: async () => {
+              await actionGlobalOpenStateFile(pinia)
+              api.sendInspectorTree(INSPECTOR_ID)
+              api.sendInspectorState(INSPECTOR_ID)
             },
-          ],
-        })
+            tooltip: 'Import the state from a JSON file',
+          },
+        ],
+      })
+
+      api.on.inspectComponent((payload, ctx) => {
+        const proxy = (payload.componentInstance &&
+          payload.componentInstance.proxy) as
+          | ComponentPublicInstance
+          | undefined
+        if (proxy && proxy._pStores) {
+          const piniaStores = (
+            payload.componentInstance.proxy as ComponentPublicInstance
+          )._pStores!
+
+          Object.values(piniaStores).forEach((store) => {
+            payload.instanceData.state.push({
+              type: getStoreType(store.$id),
+              key: 'state',
+              editable: true,
+              value: store.$state,
+            })
 
-        api.on.inspectComponent((payload, ctx) => {
-          const proxy = (payload.componentInstance &&
-            payload.componentInstance.proxy) as
-            | ComponentPublicInstance
-            | undefined
-          if (proxy && proxy._pStores) {
-            const piniaStores = (
-              payload.componentInstance.proxy as ComponentPublicInstance
-            )._pStores!
-
-            Object.values(piniaStores).forEach((store) => {
+            if (store._getters && store._getters.length) {
               payload.instanceData.state.push({
                 type: getStoreType(store.$id),
-                key: 'state',
-                editable: true,
-                value: store.$state,
+                key: 'getters',
+                editable: false,
+                value: store._getters.reduce((getters, key) => {
+                  // @ts-expect-error
+                  getters[key] = store[key]
+                  return getters
+                }, {} as GettersTree<StateTree>),
               })
+            }
+          })
+        }
+      })
 
-              if (store._getters && store._getters.length) {
-                payload.instanceData.state.push({
-                  type: getStoreType(store.$id),
-                  key: 'getters',
-                  editable: false,
-                  value: store._getters.reduce((getters, key) => {
-                    // @ts-expect-error
-                    getters[key] = store[key]
-                    return getters
-                  }, {} as GettersTree<StateTree>),
-                })
-              }
-            })
+      api.on.getInspectorTree((payload) => {
+        if (payload.app === app && payload.inspectorId === INSPECTOR_ID) {
+          let stores: Array<Store | Pinia> = [pinia]
+          stores = stores.concat(Array.from(pinia._s.values()))
+
+          payload.rootNodes = (
+            payload.filter
+              ? stores.filter((store) =>
+                  '$id' in store
+                    ? store.$id
+                        .toLowerCase()
+                        .includes(payload.filter.toLowerCase())
+                    : PINIA_ROOT_LABEL.toLowerCase().includes(
+                        payload.filter.toLowerCase()
+                      )
+                )
+              : stores
+          ).map(formatStoreForInspectorTree)
+        }
+      })
+
+      api.on.getInspectorState((payload) => {
+        if (payload.app === app && payload.inspectorId === INSPECTOR_ID) {
+          const inspectedStore =
+            payload.nodeId === PINIA_ROOT_ID
+              ? pinia
+              : pinia._s.get(payload.nodeId)
+
+          if (!inspectedStore) {
+            // this could be the selected store restored for a different project
+            // so it's better not to say anything here
+            return
           }
-        })
 
-        api.on.getInspectorTree((payload) => {
-          if (payload.app === app && payload.inspectorId === INSPECTOR_ID) {
-            let stores: Array<Store | Pinia> = [pinia]
-            stores = stores.concat(Array.from(pinia._s.values()))
-
-            payload.rootNodes = (
-              payload.filter
-                ? stores.filter((store) =>
-                    '$id' in store
-                      ? store.$id
-                          .toLowerCase()
-                          .includes(payload.filter.toLowerCase())
-                      : PINIA_ROOT_LABEL.toLowerCase().includes(
-                          payload.filter.toLowerCase()
-                        )
-                  )
-                : stores
-            ).map(formatStoreForInspectorTree)
+          if (inspectedStore) {
+            payload.state = formatStoreForInspectorState(inspectedStore)
           }
-        })
+        }
+      })
 
-        api.on.getInspectorState((payload) => {
-          if (payload.app === app && payload.inspectorId === INSPECTOR_ID) {
-            const inspectedStore =
-              payload.nodeId === PINIA_ROOT_ID
-                ? pinia
-                : pinia._s.get(payload.nodeId)
-
-            if (!inspectedStore) {
-              // this could be the selected store restored for a different project
-              // so it's better not to say anything here
-              return
-            }
+      api.on.editInspectorState((payload, ctx) => {
+        if (payload.app === app && payload.inspectorId === INSPECTOR_ID) {
+          const inspectedStore =
+            payload.nodeId === PINIA_ROOT_ID
+              ? pinia
+              : pinia._s.get(payload.nodeId)
 
-            if (inspectedStore) {
-              payload.state = formatStoreForInspectorState(inspectedStore)
-            }
+          if (!inspectedStore) {
+            return toastMessage(`store "${payload.nodeId}" not found`, 'error')
           }
-        })
 
-        api.on.editInspectorState((payload, ctx) => {
-          if (payload.app === app && payload.inspectorId === INSPECTOR_ID) {
-            const inspectedStore =
-              payload.nodeId === PINIA_ROOT_ID
-                ? pinia
-                : pinia._s.get(payload.nodeId)
-
-            if (!inspectedStore) {
-              return toastMessage(
-                `store "${payload.nodeId}" not found`,
-                'error'
-              )
-            }
+          const { path } = payload
 
-            const { path } = payload
-
-            if (!isPinia(inspectedStore)) {
-              // access only the state
-              if (
-                path.length !== 1 ||
-                !inspectedStore._customProperties.has(path[0]) ||
-                path[0] in inspectedStore.$state
-              ) {
-                path.unshift('$state')
-              }
-            } else {
-              path.unshift('state', 'value')
+          if (!isPinia(inspectedStore)) {
+            // access only the state
+            if (
+              path.length !== 1 ||
+              !inspectedStore._customProperties.has(path[0]) ||
+              path[0] in inspectedStore.$state
+            ) {
+              path.unshift('$state')
             }
-            isTimelineActive = false
-            payload.set(inspectedStore, path, payload.state.value)
-            isTimelineActive = true
+          } else {
+            path.unshift('state', 'value')
           }
-        })
-
-        api.on.editComponentState((payload) => {
-          if (payload.type.startsWith('🍍')) {
-            const storeId = payload.type.replace(/^🍍\s*/, '')
-            const store = pinia._s.get(storeId)
+          isTimelineActive = false
+          payload.set(inspectedStore, path, payload.state.value)
+          isTimelineActive = true
+        }
+      })
 
-            if (!store) {
-              return toastMessage(`store "${storeId}" not found`, 'error')
-            }
+      api.on.editComponentState((payload) => {
+        if (payload.type.startsWith('🍍')) {
+          const storeId = payload.type.replace(/^🍍\s*/, '')
+          const store = pinia._s.get(storeId)
 
-            const { path } = payload
-            if (path[0] !== 'state') {
-              return toastMessage(
-                `Invalid path for store "${storeId}":\n${path}\nOnly state can be modified.`
-              )
-            }
+          if (!store) {
+            return toastMessage(`store "${storeId}" not found`, 'error')
+          }
 
-            // rewrite the first entry to be able to directly set the state as
-            // well as any other path
-            path[0] = '$state'
-            isTimelineActive = false
-            payload.set(store, path, payload.state.value)
-            isTimelineActive = true
+          const { path } = payload
+          if (path[0] !== 'state') {
+            return toastMessage(
+              `Invalid path for store "${storeId}":\n${path}\nOnly state can be modified.`
+            )
           }
-        })
 
-        isAlreadyInstalled = true
-      } else {
-        api.sendInspectorTree(INSPECTOR_ID)
-        api.sendInspectorState(INSPECTOR_ID)
-      }
+          // rewrite the first entry to be able to directly set the state as
+          // well as any other path
+          path[0] = '$state'
+          isTimelineActive = false
+          payload.set(store, path, payload.state.value)
+          isTimelineActive = true
+        }
+      })
+    }
+  )
+}
+
+function addStoreToDevtools(app: App, store: Store) {
+  if (!componentStateTypes.includes(getStoreType(store.$id))) {
+    componentStateTypes.push(getStoreType(store.$id))
+  }
 
+  setupDevtoolsPlugin(
+    {
+      id: 'dev.esm.pinia',
+      label: 'Pinia 🍍',
+      logo: 'https://pinia.esm.dev/logo.svg',
+      packageName: 'pinia',
+      homepage: 'https://pinia.esm.dev',
+      componentStateTypes,
+      app,
+    },
+    (api) => {
       store.$onAction(({ after, onError, name, args }) => {
         const groupId = runningActionId++
 
@@ -343,6 +354,8 @@ function addDevtools(app: App, pinia: Pinia, store: Store) {
 
       // trigger an update so it can display new registered stores
       api.notifyComponentUpdate()
+      api.sendInspectorTree(INSPECTOR_ID)
+      api.sendInspectorState(INSPECTOR_ID)
       toastMessage(`"${store.$id}" store installed`)
     }
   )
@@ -398,7 +411,7 @@ export function devtoolsPlugin<
   S extends StateTree = StateTree,
   G extends GettersTree<S> = GettersTree<S>,
   A /* extends ActionsTree */ = ActionsTree
->({ app, store, options, pinia }: PiniaPluginContext<Id, S, G, A>) {
+>({ app, store, options }: PiniaPluginContext<Id, S, G, A>) {
   // HMR module
   if (store.$id.startsWith('__hot:')) {
     return
@@ -412,18 +425,18 @@ export function devtoolsPlugin<
 
   const originalHotUpdate = store.hotUpdate
 
+  // Upgrade the HMR to also update the new actions
   toRaw(store).hotUpdate = function (newStore) {
     originalHotUpdate.apply(this, arguments as any)
     patchActionForGrouping(
       // @ts-expect-error: can cast the store...
       store,
-      Object.keys(toRaw(newStore)._hmrPayload.actions)
+      Object.keys(newStore._hmrPayload.actions)
     )
   }
 
-  addDevtools(
+  addStoreToDevtools(
     app,
-    pinia,
     // @ts-expect-error: FIXME: if possible...
     store
   )