From ca59257a4ca3a37f54d6b9690a2ceedbc545dedd Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Thu, 15 Jul 2021 16:23:35 +0200 Subject: [PATCH] feat(devtools): display pinia without stores --- playground/src/stores/counterSetup.ts | 2 +- src/createPinia.ts | 5 +- src/devtools/index.ts | 2 +- src/devtools/plugin.ts | 355 +++++++++++++------------- 4 files changed, 190 insertions(+), 174 deletions(-) diff --git a/playground/src/stores/counterSetup.ts b/playground/src/stores/counterSetup.ts index 80dbfc4c..97e77c3b 100644 --- a/playground/src/stores/counterSetup.ts +++ b/playground/src/stores/counterSetup.ts @@ -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 { diff --git a/src/createPinia.ts b/src/createPinia.ts index e0ecb77c..fb8ce0d6 100644 --- a/src/createPinia.ts +++ b/src/createPinia.ts @@ -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)) }, diff --git a/src/devtools/index.ts b/src/devtools/index.ts index d81c448e..e2dfab10 100644 --- a/src/devtools/index.ts +++ b/src/devtools/index.ts @@ -1 +1 @@ -export { devtoolsPlugin } from './plugin' +export { devtoolsPlugin, registerPiniaDevtools } from './plugin' diff --git a/src/devtools/plugin.ts b/src/devtools/plugin.ts index 381c5ada..3f415609 100644 --- a/src/devtools/plugin.ts +++ b/src/devtools/plugin.ts @@ -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), }) + } + }) + } + }) - 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), - }) - } - }) + api.on.getInspectorTree((payload) => { + if (payload.app === app && payload.inspectorId === INSPECTOR_ID) { + let stores: Array = [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 = [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 = GettersTree, A /* extends ActionsTree */ = ActionsTree ->({ app, store, options, pinia }: PiniaPluginContext) { +>({ app, store, options }: PiniaPluginContext) { // 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 ) -- 2.47.2