} 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[] = []
*/
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',
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++
// 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`)
}
)
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
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
)