From b4cac3494a226724bd0bc9c290cb8055beacfe9b Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Thu, 15 Jul 2021 15:56:17 +0200 Subject: [PATCH] fix(devtools): state grouping --- playground/src/main.ts | 8 ----- src/devtools/plugin.ts | 72 +++++++++++++++++++----------------------- src/rootStore.ts | 1 + src/store.ts | 17 ++++++++++ src/types.ts | 14 +++++--- 5 files changed, 60 insertions(+), 52 deletions(-) diff --git a/playground/src/main.ts b/playground/src/main.ts index 914ca520..70a36665 100644 --- a/playground/src/main.ts +++ b/playground/src/main.ts @@ -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) => { // // 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') diff --git a/src/devtools/plugin.ts b/src/devtools/plugin.ts index 468a1ea8..381c5ada 100644 --- a/src/devtools/plugin.ts +++ b/src/devtools/plugin.ts @@ -25,11 +25,6 @@ import { } from './formatting' import { isPinia, toastMessage } from './utils' -/** - * Registered stores used for devtools. - */ -const registeredStores = /*#__PURE__*/ new Map() - 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._p] - stores = stores.concat(Array.from(registeredStores.values())) + let stores: Array = [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 = GettersTree, A /* extends ActionsTree */ = ActionsTree ->({ app, store, options }: PiniaPluginContext) { +>({ app, store, options, pinia }: PiniaPluginContext) { // 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 ) diff --git a/src/rootStore.ts b/src/rootStore.ts index 1b827800..955dcda0 100644 --- a/src/rootStore.ts +++ b/src/rootStore.ts @@ -101,6 +101,7 @@ export interface Pinia { _e: EffectScope /** + * Registry of stores used by this pinia. * * @internal */ diff --git a/src/store.ts b/src/store.ts index 49b85012..8ac71fb7 100644 --- a/src/store.ts +++ b/src/store.ts @@ -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 diff --git a/src/types.ts b/src/types.ts index 2d269b95..45883dc5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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): () => void + $subscribe(callback: SubscriptionCallback, 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): () => void + $onAction( + callback: StoreOnActionListener, + detached?: boolean + ): () => void } /** -- 2.47.2