*/
const getStoreType = (id: string) => '๐ ' + id
+type DevtoolsPluginAPI = Parameters<
+ Parameters<typeof setupDevtoolsPlugin>[1]
+>[0]
+
+const registeredApps = new WeakMap<App, DevtoolsPluginAPI>()
+
/**
* 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',
- label: 'Pinia ๐',
- logo: 'https://pinia.vuejs.org/logo.svg',
- packageName: 'pinia',
- homepage: 'https://pinia.vuejs.org',
- componentStateTypes,
- app,
- },
- (api) => {
- if (typeof api.now !== 'function') {
- toastMessage(
- 'You seem to be using an outdated version of Vue Devtools. Are you still using the Beta release instead of the stable one? You can find the links at https://devtools.vuejs.org/guide/installation.html.'
- )
- }
+export async function registerPiniaDevtools(app: App, pinia: Pinia) {
+ return (
+ registeredApps.get(app) ||
+ new Promise<DevtoolsPluginAPI>((resolve) => {
+ setupDevtoolsPlugin(
+ {
+ id: 'dev.esm.pinia',
+ label: 'Pinia ๐',
+ logo: 'https://pinia.vuejs.org/logo.svg',
+ packageName: 'pinia',
+ homepage: 'https://pinia.vuejs.org',
+ componentStateTypes,
+ app,
+ },
+ (api) => {
+ registeredApps.set(app, api)
- api.addTimelineLayer({
- id: MUTATIONS_LAYER_ID,
- label: `Pinia ๐`,
- color: 0xe5df88,
- })
+ if (typeof api.now !== 'function') {
+ toastMessage(
+ 'You seem to be using an outdated version of Vue Devtools. Are you still using the Beta release instead of the stable one? You can find the links at https://devtools.vuejs.org/guide/installation.html.'
+ )
+ }
- 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',
- },
- {
- 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',
- },
- {
- icon: 'save',
- action: () => {
- actionGlobalSaveState(pinia)
- },
- 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',
- },
- ],
- nodeActions: [
- {
- icon: 'restore',
- tooltip: 'Reset the state (with "$reset")',
- action: (nodeId) => {
- const store = pinia._s.get(nodeId)
- if (!store) {
- toastMessage(
- `Cannot reset "${nodeId}" store because it wasn't found.`,
- 'warn'
- )
- } else if (typeof store.$reset !== 'function') {
- toastMessage(
- `Cannot reset "${nodeId}" store because it doesn't have a "$reset" method implemented.`,
- 'warn'
- )
- } else {
- store.$reset()
- toastMessage(`Store "${nodeId}" reset.`)
- }
- },
- },
- ],
- })
+ api.addTimelineLayer({
+ id: MUTATIONS_LAYER_ID,
+ label: `Pinia ๐`,
+ color: 0xe5df88,
+ })
- api.on.inspectComponent((payload) => {
- 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._isOptionsAPI
- ? {
- _custom: {
- value: toRaw(store.$state),
- actions: [
- {
- icon: 'restore',
- tooltip: 'Reset the state of this store',
- action: () => store.$reset(),
- },
- ],
- },
- }
- : // NOTE: workaround to unwrap transferred refs
- Object.keys(store.$state).reduce((state, key) => {
- state[key] = store.$state[key]
- return state
- }, {} as 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) => {
- try {
- getters[key] = store[key]
- } catch (error) {
- // @ts-expect-error: we just want to show it in devtools
- getters[key] = error
+ 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',
+ },
+ {
+ 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',
+ },
+ {
+ icon: 'save',
+ action: () => {
+ actionGlobalSaveState(pinia)
+ },
+ 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',
+ },
+ ],
+ nodeActions: [
+ {
+ icon: 'restore',
+ tooltip: 'Reset the state (with "$reset")',
+ action: (nodeId) => {
+ const store = pinia._s.get(nodeId)
+ if (!store) {
+ toastMessage(
+ `Cannot reset "${nodeId}" store because it wasn't found.`,
+ 'warn'
+ )
+ } else if (typeof store.$reset !== 'function') {
+ toastMessage(
+ `Cannot reset "${nodeId}" store because it doesn't have a "$reset" method implemented.`,
+ 'warn'
+ )
+ } else {
+ store.$reset()
+ toastMessage(`Store "${nodeId}" reset.`)
}
- return getters
- }, {} as _GettersTree<StateTree>),
+ },
+ },
+ ],
+ })
+
+ api.on.inspectComponent((payload) => {
+ 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._isOptionsAPI
+ ? {
+ _custom: {
+ value: toRaw(store.$state),
+ actions: [
+ {
+ icon: 'restore',
+ tooltip: 'Reset the state of this store',
+ action: () => store.$reset(),
+ },
+ ],
+ },
+ }
+ : // NOTE: workaround to unwrap transferred refs
+ Object.keys(store.$state).reduce((state, key) => {
+ state[key] = store.$state[key]
+ return state
+ }, {} as 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) => {
+ try {
+ getters[key] = store[key]
+ } catch (error) {
+ // @ts-expect-error: we just want to show it in devtools
+ getters[key] = error
+ }
+ return getters
+ }, {} as _GettersTree<StateTree>),
+ })
+ }
})
}
})
- }
- })
-
- api.on.getInspectorTree((payload) => {
- if (payload.app === app && payload.inspectorId === INSPECTOR_ID) {
- let stores: Array<StoreGeneric | 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)
- }
- })
- // Expose pinia instance as $pinia to window
- globalThis.$pinia = pinia
+ api.on.getInspectorTree((payload) => {
+ if (payload.app === app && payload.inspectorId === INSPECTOR_ID) {
+ let stores: Array<StoreGeneric | 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)
+ // Expose pinia instance as $pinia to window
+ globalThis.$pinia = pinia
- if (!inspectedStore) {
- // this could be the selected store restored for a different project
- // so it's better not to say anything here
- return
- }
-
- if (inspectedStore) {
- // Expose selected store as $store to window
- if (payload.nodeId !== PINIA_ROOT_ID)
- globalThis.$store = toRaw(inspectedStore as StoreGeneric)
- 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)
- api.on.editInspectorState((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
+ }
- if (!inspectedStore) {
- return toastMessage(`store "${payload.nodeId}" not found`, 'error')
- }
+ if (inspectedStore) {
+ // Expose selected store as $store to window
+ if (payload.nodeId !== PINIA_ROOT_ID)
+ globalThis.$store = toRaw(inspectedStore as StoreGeneric)
+ payload.state = formatStoreForInspectorState(inspectedStore)
+ }
+ }
+ })
- const { path } = payload
+ api.on.editInspectorState((payload) => {
+ 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'
+ )
+ }
- if (!isPinia(inspectedStore)) {
- // access only the state
- if (
- path.length !== 1 ||
- !inspectedStore._customProperties.has(path[0]) ||
- path[0] in inspectedStore.$state
- ) {
- path.unshift('$state')
+ 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 {
+ // Root access, we can omit the `.value` because the devtools API does it for us
+ path.unshift('state')
+ }
+ isTimelineActive = false
+ payload.set(inspectedStore, path, payload.state.value)
+ isTimelineActive = true
}
- } else {
- // Root access, we can omit the `.value` because the devtools API does it for us
- path.unshift('state')
- }
- isTimelineActive = false
- payload.set(inspectedStore, path, payload.state.value)
- isTimelineActive = true
- }
- })
+ })
- api.on.editComponentState((payload) => {
- if (payload.type.startsWith('๐')) {
- const storeId = payload.type.replace(/^๐\s*/, '')
- const store = pinia._s.get(storeId)
+ api.on.editComponentState((payload) => {
+ if (payload.type.startsWith('๐')) {
+ const storeId = payload.type.replace(/^๐\s*/, '')
+ const store = pinia._s.get(storeId)
- if (!store) {
- return toastMessage(`store "${storeId}" not found`, 'error')
- }
+ if (!store) {
+ return toastMessage(`store "${storeId}" not found`, 'error')
+ }
- const { path } = payload
- if (path[0] !== 'state') {
- return toastMessage(
- `Invalid path for store "${storeId}":\n${path}\nOnly state can be modified.`
- )
- }
+ const { path } = payload
+ if (path[0] !== 'state') {
+ return toastMessage(
+ `Invalid path for store "${storeId}":\n${path}\nOnly state can be modified.`
+ )
+ }
- // 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
+ // 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
+ }
+ })
+
+ resolve(api)
}
- })
- }
+ )
+ })
)
}
-function addStoreToDevtools(app: App, store: StoreGeneric) {
+async function addStoreToDevtools(app: App, store: StoreGeneric) {
if (!componentStateTypes.includes(getStoreType(store.$id))) {
componentStateTypes.push(getStoreType(store.$id))
}
- setupDevtoolsPlugin(
- {
- id: 'dev.esm.pinia',
- label: 'Pinia ๐',
- logo: 'https://pinia.vuejs.org/logo.svg',
- packageName: 'pinia',
- homepage: 'https://pinia.vuejs.org',
- componentStateTypes,
- app,
- settings: {
- logStoreChanges: {
- label: 'Notify about new/deleted stores',
- type: 'boolean',
- defaultValue: true,
+ const api =
+ registeredApps.get(app) || (await registerPiniaDevtools(app, store._p))
+
+ // gracefully handle errors
+ const now = typeof api.now === 'function' ? api.now.bind(api) : Date.now
+
+ store.$onAction(({ after, onError, name, args }) => {
+ const groupId = runningActionId++
+
+ api.addTimelineEvent({
+ layerId: MUTATIONS_LAYER_ID,
+ event: {
+ time: now(),
+ title: '๐ซ ' + name,
+ subtitle: 'start',
+ data: {
+ store: formatDisplay(store.$id),
+ action: formatDisplay(name),
+ args,
},
- // useEmojis: {
- // label: 'Use emojis in messages โก๏ธ',
- // type: 'boolean',
- // defaultValue: true,
- // },
+ groupId,
},
- },
- (api) => {
- // gracefully handle errors
- const now = typeof api.now === 'function' ? api.now.bind(api) : Date.now
-
- store.$onAction(({ after, onError, name, args }) => {
- const groupId = runningActionId++
-
- api.addTimelineEvent({
- layerId: MUTATIONS_LAYER_ID,
- event: {
- time: now(),
- title: '๐ซ ' + name,
- subtitle: 'start',
- data: {
- store: formatDisplay(store.$id),
- action: formatDisplay(name),
- args,
- },
- groupId,
+ })
+
+ after((result) => {
+ activeAction = undefined
+ api.addTimelineEvent({
+ layerId: MUTATIONS_LAYER_ID,
+ event: {
+ time: now(),
+ title: '๐ฌ ' + name,
+ subtitle: 'end',
+ data: {
+ store: formatDisplay(store.$id),
+ action: formatDisplay(name),
+ args,
+ result,
},
- })
+ groupId,
+ },
+ })
+ })
- after((result) => {
- activeAction = undefined
- api.addTimelineEvent({
- layerId: MUTATIONS_LAYER_ID,
- event: {
- time: now(),
- title: '๐ฌ ' + name,
- subtitle: 'end',
- data: {
- store: formatDisplay(store.$id),
- action: formatDisplay(name),
- args,
- result,
- },
- groupId,
- },
- })
- })
+ onError((error) => {
+ activeAction = undefined
+ api.addTimelineEvent({
+ layerId: MUTATIONS_LAYER_ID,
+ event: {
+ time: now(),
+ logType: 'error',
+ title: '๐ฅ ' + name,
+ subtitle: 'end',
+ data: {
+ store: formatDisplay(store.$id),
+ action: formatDisplay(name),
+ args,
+ error,
+ },
+ groupId,
+ },
+ })
+ })
+ }, true)
- onError((error) => {
- activeAction = undefined
+ store._customProperties.forEach((name) => {
+ watch(
+ () => unref<unknown>(store[name]),
+ (newValue, oldValue) => {
+ api.notifyComponentUpdate()
+ api.sendInspectorState(INSPECTOR_ID)
+ if (isTimelineActive) {
api.addTimelineEvent({
layerId: MUTATIONS_LAYER_ID,
event: {
time: now(),
- logType: 'error',
- title: '๐ฅ ' + name,
- subtitle: 'end',
+ title: 'Change',
+ subtitle: name,
data: {
- store: formatDisplay(store.$id),
- action: formatDisplay(name),
- args,
- error,
+ newValue,
+ oldValue,
},
- groupId,
+ groupId: activeAction,
},
})
- })
- }, true)
-
- store._customProperties.forEach((name) => {
- watch(
- () => unref<unknown>(store[name]),
- (newValue, oldValue) => {
- api.notifyComponentUpdate()
- api.sendInspectorState(INSPECTOR_ID)
- if (isTimelineActive) {
- api.addTimelineEvent({
- layerId: MUTATIONS_LAYER_ID,
- event: {
- time: now(),
- title: 'Change',
- subtitle: name,
- data: {
- newValue,
- oldValue,
- },
- groupId: activeAction,
- },
- })
- }
- },
- { deep: true }
- )
- })
-
- store.$subscribe(
- ({ events, type }, state) => {
- api.notifyComponentUpdate()
- api.sendInspectorState(INSPECTOR_ID)
-
- if (!isTimelineActive) return
- // rootStore.state[store.id] = state
-
- const eventData: TimelineEvent = {
- time: now(),
- title: formatMutationType(type),
- data: assign(
- { store: formatDisplay(store.$id) },
- formatEventData(events)
- ),
- groupId: activeAction,
- }
+ }
+ },
+ { deep: true }
+ )
+ })
- if (type === MutationType.patchFunction) {
- eventData.subtitle = 'โคต๏ธ'
- } else if (type === MutationType.patchObject) {
- eventData.subtitle = '๐งฉ'
- } else if (events && !Array.isArray(events)) {
- eventData.subtitle = events.type
- }
+ store.$subscribe(
+ ({ events, type }, state) => {
+ api.notifyComponentUpdate()
+ api.sendInspectorState(INSPECTOR_ID)
- if (events) {
- eventData.data['rawEvent(s)'] = {
- _custom: {
- display: 'DebuggerEvent',
- type: 'object',
- tooltip: 'raw DebuggerEvent[]',
- value: events,
- },
- }
- }
+ if (!isTimelineActive) return
+ // rootStore.state[store.id] = state
+
+ const eventData: TimelineEvent = {
+ time: now(),
+ title: formatMutationType(type),
+ data: assign(
+ { store: formatDisplay(store.$id) },
+ formatEventData(events)
+ ),
+ groupId: activeAction,
+ }
- api.addTimelineEvent({
- layerId: MUTATIONS_LAYER_ID,
- event: eventData,
- })
- },
- { detached: true, flush: 'sync' }
- )
+ if (type === MutationType.patchFunction) {
+ eventData.subtitle = 'โคต๏ธ'
+ } else if (type === MutationType.patchObject) {
+ eventData.subtitle = '๐งฉ'
+ } else if (events && !Array.isArray(events)) {
+ eventData.subtitle = events.type
+ }
- const hotUpdate = store._hotUpdate
- store._hotUpdate = markRaw((newStore) => {
- hotUpdate(newStore)
- api.addTimelineEvent({
- layerId: MUTATIONS_LAYER_ID,
- event: {
- time: now(),
- title: '๐ฅ ' + store.$id,
- subtitle: 'HMR update',
- data: {
- store: formatDisplay(store.$id),
- info: formatDisplay(`HMR update`),
- },
+ if (events) {
+ eventData.data['rawEvent(s)'] = {
+ _custom: {
+ display: 'DebuggerEvent',
+ type: 'object',
+ tooltip: 'raw DebuggerEvent[]',
+ value: events,
},
- })
- // update the devtools too
- api.notifyComponentUpdate()
- api.sendInspectorTree(INSPECTOR_ID)
- api.sendInspectorState(INSPECTOR_ID)
- })
-
- const { $dispose } = store
- store.$dispose = () => {
- $dispose()
- api.notifyComponentUpdate()
- api.sendInspectorTree(INSPECTOR_ID)
- api.sendInspectorState(INSPECTOR_ID)
- api.getSettings().logStoreChanges &&
- toastMessage(`Disposed "${store.$id}" store ๐`)
+ }
}
- // trigger an update so it can display new registered stores
- api.notifyComponentUpdate()
- api.sendInspectorTree(INSPECTOR_ID)
- api.sendInspectorState(INSPECTOR_ID)
- api.getSettings().logStoreChanges &&
- toastMessage(`"${store.$id}" store installed ๐`)
- }
+ api.addTimelineEvent({
+ layerId: MUTATIONS_LAYER_ID,
+ event: eventData,
+ })
+ },
+ { detached: true, flush: 'sync' }
)
+
+ const hotUpdate = store._hotUpdate
+ store._hotUpdate = markRaw((newStore) => {
+ hotUpdate(newStore)
+ api.addTimelineEvent({
+ layerId: MUTATIONS_LAYER_ID,
+ event: {
+ time: now(),
+ title: '๐ฅ ' + store.$id,
+ subtitle: 'HMR update',
+ data: {
+ store: formatDisplay(store.$id),
+ info: formatDisplay(`HMR update`),
+ },
+ },
+ })
+ // update the devtools too
+ api.notifyComponentUpdate()
+ api.sendInspectorTree(INSPECTOR_ID)
+ api.sendInspectorState(INSPECTOR_ID)
+ })
+
+ const { $dispose } = store
+ store.$dispose = () => {
+ $dispose()
+ api.notifyComponentUpdate()
+ api.sendInspectorTree(INSPECTOR_ID)
+ api.sendInspectorState(INSPECTOR_ID)
+ api.getSettings().logStoreChanges &&
+ toastMessage(`Disposed "${store.$id}" store ๐`)
+ }
+
+ // trigger an update so it can display new registered stores
+ api.notifyComponentUpdate()
+ api.sendInspectorTree(INSPECTOR_ID)
+ api.sendInspectorState(INSPECTOR_ID)
+ api.getSettings().logStoreChanges &&
+ toastMessage(`"${store.$id}" store installed ๐`)
}
let runningActionId = 0