]> git.ipfire.org Git - thirdparty/vuejs/pinia.git/commitdiff
fix(devtools): state grouping
authorEduardo San Martin Morote <posva13@gmail.com>
Thu, 15 Jul 2021 13:56:17 +0000 (15:56 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Mon, 19 Jul 2021 09:52:24 +0000 (11:52 +0200)
playground/src/main.ts
src/devtools/plugin.ts
src/rootStore.ts
src/store.ts
src/types.ts

index 914ca52053bc94345dba0bc67e2ac9e632a19ee0..70a366656015da7471c541caffe0cbde2c55968c 100644 (file)
@@ -6,25 +6,19 @@ import { router } from './router'
 const pinia = createPinia()
 
 if (import.meta.hot) {
-  import.meta.hot.data.pinia = pinia
-  console.log('set', import.meta.hot.data)
   //   const isUseStore = (fn: any): fn is StoreDefinition => {
   //     return typeof fn === 'function' && typeof fn.$id === 'string'
   //   }
-
   //   // import.meta.hot.accept(
   //   //   './stores/counter.ts',
   //   //   (newStore: Record<string, unknown>) => {
   //   //     console.log('haha', newStore)
   //   //   }
   //   // )
-
   //   import.meta.hot.accept('./test.ts', (newTest) => {
   //     console.log('test updated', newTest)
   //   })
-
   //   const stores = import.meta.glob('./stores/*.ts')
-
   //   for (const storeId in stores) {
   //     console.log('configuring HMR for', storeId)
   //     const oldUseStore = await stores[storeId]()
@@ -48,6 +42,4 @@ if (import.meta.hot) {
   //   }
 }
 
-// TODO: HMR for plugins
-
 createApp(App).use(router).use(pinia).mount('#app')
index 468a1ea809ef2ede635aef90938bf0fbef71e96d..381c5adae11ae3aac1f7be5f4f32e0de8dd2956e 100644 (file)
@@ -25,11 +25,6 @@ import {
 } from './formatting'
 import { isPinia, toastMessage } from './utils'
 
-/**
- * Registered stores used for devtools.
- */
-const registeredStores = /*#__PURE__*/ new Map<string, Store>()
-
 let isAlreadyInstalled: boolean | undefined
 // timeline can be paused when directly changing the state
 let isTimelineActive = true
@@ -38,17 +33,17 @@ const componentStateTypes: string[] = []
 const MUTATIONS_LAYER_ID = 'pinia:mutations'
 const INSPECTOR_ID = 'pinia'
 
-function addDevtools(app: App, store: Store) {
-  // TODO: we probably need to ensure the latest version of the store is kept:
-  // without effectScope, multiple stores will be created and will have a
-  // limited lifespan for getters.
-  // add a dev only variable that is removed in unmounted and replace the store
-  let hasSubscribed = true
-  const storeType = '🍍 ' + store.$id
-  if (!registeredStores.has(store.$id)) {
-    registeredStores.set(store.$id, store)
-    componentStateTypes.push(storeType)
-    hasSubscribed = false
+/**
+ * Gets the displayed name of a store in devtools
+ *
+ * @param id - id of the store
+ * @returns a formatted string
+ */
+const getStoreType = (id: string) => '🍍 ' + id
+
+function addDevtools(app: App, pinia: Pinia, store: Store) {
+  if (!componentStateTypes.includes(getStoreType(store.$id))) {
+    componentStateTypes.push(getStoreType(store.$id))
   }
 
   setupDevtoolsPlugin(
@@ -78,14 +73,14 @@ function addDevtools(app: App, store: Store) {
             {
               icon: 'content_copy',
               action: () => {
-                actionGlobalCopyState(store._p)
+                actionGlobalCopyState(pinia)
               },
               tooltip: 'Serialize and copy the state',
             },
             {
               icon: 'content_paste',
               action: async () => {
-                await actionGlobalPasteState(store._p)
+                await actionGlobalPasteState(pinia)
                 api.sendInspectorTree(INSPECTOR_ID)
                 api.sendInspectorState(INSPECTOR_ID)
               },
@@ -94,14 +89,14 @@ function addDevtools(app: App, store: Store) {
             {
               icon: 'save',
               action: () => {
-                actionGlobalSaveState(store._p)
+                actionGlobalSaveState(pinia)
               },
               tooltip: 'Save the state as a JSON file',
             },
             {
               icon: 'folder_open',
               action: async () => {
-                await actionGlobalOpenStateFile(store._p)
+                await actionGlobalOpenStateFile(pinia)
                 api.sendInspectorTree(INSPECTOR_ID)
                 api.sendInspectorState(INSPECTOR_ID)
               },
@@ -122,7 +117,7 @@ function addDevtools(app: App, store: Store) {
 
             Object.values(piniaStores).forEach((store) => {
               payload.instanceData.state.push({
-                type: storeType,
+                type: getStoreType(store.$id),
                 key: 'state',
                 editable: true,
                 value: store.$state,
@@ -130,7 +125,7 @@ function addDevtools(app: App, store: Store) {
 
               if (store._getters && store._getters.length) {
                 payload.instanceData.state.push({
-                  type: storeType,
+                  type: getStoreType(store.$id),
                   key: 'getters',
                   editable: false,
                   value: store._getters.reduce((getters, key) => {
@@ -146,8 +141,8 @@ function addDevtools(app: App, store: Store) {
 
         api.on.getInspectorTree((payload) => {
           if (payload.app === app && payload.inspectorId === INSPECTOR_ID) {
-            let stores: Array<Store | Pinia> = [store._p]
-            stores = stores.concat(Array.from(registeredStores.values()))
+            let stores: Array<Store | Pinia> = [pinia]
+            stores = stores.concat(Array.from(pinia._s.values()))
 
             payload.rootNodes = (
               payload.filter
@@ -169,8 +164,8 @@ function addDevtools(app: App, store: Store) {
           if (payload.app === app && payload.inspectorId === INSPECTOR_ID) {
             const inspectedStore =
               payload.nodeId === PINIA_ROOT_ID
-                ? store._p
-                : registeredStores.get(payload.nodeId)
+                ? pinia
+                : pinia._s.get(payload.nodeId)
 
             if (!inspectedStore) {
               // this could be the selected store restored for a different project
@@ -188,8 +183,8 @@ function addDevtools(app: App, store: Store) {
           if (payload.app === app && payload.inspectorId === INSPECTOR_ID) {
             const inspectedStore =
               payload.nodeId === PINIA_ROOT_ID
-                ? store._p
-                : registeredStores.get(payload.nodeId)
+                ? pinia
+                : pinia._s.get(payload.nodeId)
 
             if (!inspectedStore) {
               return toastMessage(
@@ -200,12 +195,12 @@ function addDevtools(app: App, store: Store) {
 
             const { path } = payload
 
-            if (!isPinia(store)) {
+            if (!isPinia(inspectedStore)) {
               // access only the state
               if (
                 path.length !== 1 ||
-                !store._customProperties.has(path[0]) ||
-                path[0] in store.$state
+                !inspectedStore._customProperties.has(path[0]) ||
+                path[0] in inspectedStore.$state
               ) {
                 path.unshift('$state')
               }
@@ -221,7 +216,7 @@ function addDevtools(app: App, store: Store) {
         api.on.editComponentState((payload) => {
           if (payload.type.startsWith('🍍')) {
             const storeId = payload.type.replace(/^🍍\s*/, '')
-            const store = registeredStores.get(storeId)
+            const store = pinia._s.get(storeId)
 
             if (!store) {
               return toastMessage(`store "${storeId}" not found`, 'error')
@@ -249,10 +244,7 @@ function addDevtools(app: App, store: Store) {
         api.sendInspectorState(INSPECTOR_ID)
       }
 
-      // avoid subscribing to mutations and actions twice
-      if (hasSubscribed) return
-
-      store.$onAction(({ after, onError, name, args, store }) => {
+      store.$onAction(({ after, onError, name, args }) => {
         const groupId = runningActionId++
 
         api.addTimelineEvent({
@@ -305,7 +297,7 @@ function addDevtools(app: App, store: Store) {
             },
           })
         })
-      })
+      }, true)
 
       store.$subscribe(({ events, type }, state) => {
         if (!isTimelineActive) return
@@ -347,10 +339,9 @@ function addDevtools(app: App, store: Store) {
           layerId: MUTATIONS_LAYER_ID,
           event: eventData,
         })
-      })
+      }, true)
 
       // trigger an update so it can display new registered stores
-      // @ts-ignore
       api.notifyComponentUpdate()
       toastMessage(`"${store.$id}" store installed`)
     }
@@ -407,7 +398,7 @@ export function devtoolsPlugin<
   S extends StateTree = StateTree,
   G extends GettersTree<S> = GettersTree<S>,
   A /* extends ActionsTree */ = ActionsTree
->({ app, store, options }: PiniaPluginContext<Id, S, G, A>) {
+>({ app, store, options, pinia }: PiniaPluginContext<Id, S, G, A>) {
   // HMR module
   if (store.$id.startsWith('__hot:')) {
     return
@@ -432,6 +423,7 @@ export function devtoolsPlugin<
 
   addDevtools(
     app,
+    pinia,
     // @ts-expect-error: FIXME: if possible...
     store
   )
index 1b8278001fd37572d156564e425ab167ea80ec93..955dcda00a0325f979d16e764c8ec226193ae3ca 100644 (file)
@@ -101,6 +101,7 @@ export interface Pinia {
   _e: EffectScope
 
   /**
+   * Registry of stores used by this pinia.
    *
    * @internal
    */
index 49b85012f1df347f330ac3fb9f3225513c8a0336..8ac71fb7222268897db635a76ce6d07b70f9ceab 100644 (file)
@@ -484,6 +484,23 @@ function createSetupStore<
 
       // TODO: remove old actions and getters
     })
+
+    const nonEnumerable = {
+      writable: true,
+      configurable: true,
+      // avoid warning on devtools trying to display this property
+      enumerable: false,
+    }
+
+    // avoid listing internal properties in devtools
+    ;(['_p', '_hmrPayload', '_getters', '_customProperties'] as const).forEach(
+      (p) => {
+        Object.defineProperty(store, p, {
+          value: store[p],
+          ...nonEnumerable,
+        })
+      }
+    )
   }
 
   // apply all plugins
index 2d269b9546690acab3171cbb112a52838b9275df..45883dc5a7bc5b442b9685562b4041980f9588df 100644 (file)
@@ -297,12 +297,14 @@ export interface StoreWithState<
    * Setups a callback to be called whenever the state changes. It also returns
    * a function to remove the callback. Note than when calling
    * `store.$subscribe()` inside of a component, it will be automatically
-   * cleanup up when the component gets unmounted.
+   * cleanup up when the component gets unmounted unless `detached` is set to
+   * true.
    *
    * @param callback - callback passed to the watcher
+   * @param detached - detach the subscription from the context this is called from
    * @returns function that removes the watcher
    */
-  $subscribe(callback: SubscriptionCallback<S>): () => void
+  $subscribe(callback: SubscriptionCallback<S>, detached?: boolean): () => void
 
   /**
    * @alpha Please send feedback at https://github.com/posva/pinia/issues/240
@@ -318,7 +320,7 @@ export interface StoreWithState<
    *
    * It also returns a function to remove the callback. Note than when calling
    * `store.$onAction()` inside of a component, it will be automatically cleanup
-   * up when the component gets unmounted.
+   * up when the component gets unmounted unless `detached` is set to true.
    *
    * @example
    *
@@ -338,9 +340,13 @@ export interface StoreWithState<
    *```
    *
    * @param callback - callback called before every action
+   * @param detached - detach the subscription from the context this is called from
    * @returns function that removes the watcher
    */
-  $onAction(callback: StoreOnActionListener<Id, S, G, A>): () => void
+  $onAction(
+    callback: StoreOnActionListener<Id, S, G, A>,
+    detached?: boolean
+  ): () => void
 }
 
 /**