From de93d61b9477d746ef3fcf2542ba437468151f0c Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Tue, 11 May 2021 22:53:20 +0200 Subject: [PATCH] refactor: remove circular dep --- __tests__/store.spec.ts | 20 ++-- jest.config.js | 2 +- src/createPinia.ts | 61 +++++++++++ src/devtools/formatting.ts | 88 +++++++++++++++ src/devtools/index.ts | 1 + src/{devtools.ts => devtools/plugin.ts} | 138 ++++++------------------ src/index.ts | 4 +- src/rootStore.ts | 130 +++++++--------------- src/store.ts | 5 +- 9 files changed, 236 insertions(+), 213 deletions(-) create mode 100644 src/createPinia.ts create mode 100644 src/devtools/formatting.ts create mode 100644 src/devtools/index.ts rename src/{devtools.ts => devtools/plugin.ts} (78%) diff --git a/__tests__/store.spec.ts b/__tests__/store.spec.ts index 6699b0ee..60f44956 100644 --- a/__tests__/store.spec.ts +++ b/__tests__/store.spec.ts @@ -1,4 +1,10 @@ -import { createPinia, defineStore, setActivePinia, Pinia } from '../src' +import { + createPinia, + defineStore, + setActivePinia, + Pinia, + MutationType, +} from '../src' import { mount } from '@vue/test-utils' import { defineComponent, getCurrentInstance, nextTick, watch } from 'vue' @@ -148,11 +154,11 @@ describe('Store', () => { store.$state.a = false expect(spy).toHaveBeenCalledWith( - { + expect.objectContaining({ payload: {}, storeName: 'main', - type: expect.stringContaining('in place'), - }, + type: MutationType.direct, + }), store.$state ) }) @@ -166,11 +172,11 @@ describe('Store', () => { store.$patch(patch) expect(spy).toHaveBeenCalledWith( - { + expect.objectContaining({ payload: patch, storeName: 'main', - type: expect.stringContaining('patch'), - }, + type: MutationType.patchObject, + }), store.$state ) }) diff --git a/jest.config.js b/jest.config.js index 16629a55..c5d0241a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,7 +8,7 @@ module.exports = { '/node_modules/', 'src/index.ts', '\\.d\\.ts$', - 'src/devtools.ts', + 'src/devtools', 'src/deprecated.ts', ], testMatch: ['/__tests__/**/*.spec.ts'], diff --git a/src/createPinia.ts b/src/createPinia.ts new file mode 100644 index 00000000..910194b7 --- /dev/null +++ b/src/createPinia.ts @@ -0,0 +1,61 @@ +import { + Pinia, + PiniaStorePlugin, + setActivePinia, + piniaSymbol, +} from './rootStore' +import { ref, App } from 'vue' +import { devtoolsPlugin } from './devtools' +import { IS_CLIENT } from './env' + +/** + * Creates a Pinia instance to be used by the application + */ +export function createPinia(): Pinia { + // NOTE: here we could check the window object for a state and directly set it + // if there is anything like it with Vue 3 SSR + const state = ref({}) + + let localApp: App | undefined + let _p: Pinia['_p'] = [] + // plugins added before calling app.use(pinia) + const toBeInstalled: PiniaStorePlugin[] = [] + + const pinia: Pinia = { + install(app: App) { + pinia._a = localApp = app + // pinia._a = app + app.provide(piniaSymbol, pinia) + app.config.globalProperties.$pinia = pinia + // TODO: write test + // only set the app on client for devtools + if (__BROWSER__ && IS_CLIENT) { + // this allows calling useStore() outside of a component setup after + // installing pinia's plugin + setActivePinia(pinia) + } + toBeInstalled.forEach((plugin) => _p.push(plugin)) + }, + + use(plugin) { + if (!localApp) { + toBeInstalled.push(plugin) + } else { + _p.push(plugin) + } + return this + }, + + _p, + // it's actually undefined here + _a: localApp!, + + state, + } + + if (IS_CLIENT && __BROWSER__ && __DEV__) { + pinia.use(devtoolsPlugin) + } + + return pinia +} diff --git a/src/devtools/formatting.ts b/src/devtools/formatting.ts new file mode 100644 index 00000000..0b3aaaf9 --- /dev/null +++ b/src/devtools/formatting.ts @@ -0,0 +1,88 @@ +import { CustomInspectorNode, CustomInspectorState } from '@vue/devtools-api' +import { GenericStore, GettersTree, MutationType, StateTree } from '../types' +import { DebuggerEvent } from 'vue' + +export function formatDisplay(display: string) { + return { + _custom: { + display, + }, + } +} + +export function formatStoreForInspectorTree( + store: GenericStore +): CustomInspectorNode { + return { + id: store.$id, + label: store.$id, + tags: [], + } +} + +export function formatStoreForInspectorState( + store: GenericStore +): CustomInspectorState[string] { + const fields: CustomInspectorState[string] = [ + { editable: false, key: 'id', value: formatDisplay(store.$id) }, + { editable: true, key: 'state', value: store.$state }, + ] + + // avoid adding empty getters + if (store._getters?.length) { + fields.push({ + editable: false, + key: 'getters', + value: store._getters.reduce((getters, key) => { + getters[key] = store[key] + return getters + }, {} as GettersTree), + }) + } + + return fields +} + +export function formatEventData( + events: DebuggerEvent[] | DebuggerEvent | undefined +) { + if (!events) return {} + if (Array.isArray(events)) { + // TODO: handle add and delete for arrays and objects + return events.reduce( + (data, event) => { + data.keys.push(event.key) + data.operations.push(event.type) + data.oldValue[event.key] = event.oldValue + data.newValue[event.key] = event.newValue + return data + }, + { + oldValue: {} as Record, + keys: [] as string[], + operations: [] as string[], + newValue: {} as Record, + } + ) + } else { + return { + operation: formatDisplay(events.type), + key: formatDisplay(events.key), + oldValue: events.oldValue, + newValue: events.newValue, + } + } +} + +export function formatMutationType(type: MutationType): string { + switch (type) { + case MutationType.direct: + return 'mutation' + case MutationType.patchFunction: + return '$patch' + case MutationType.patchObject: + return '$patch' + default: + return 'unknown' + } +} diff --git a/src/devtools/index.ts b/src/devtools/index.ts new file mode 100644 index 00000000..d81c448e --- /dev/null +++ b/src/devtools/index.ts @@ -0,0 +1 @@ +export { devtoolsPlugin } from './plugin' diff --git a/src/devtools.ts b/src/devtools/plugin.ts similarity index 78% rename from src/devtools.ts rename to src/devtools/plugin.ts index 8b62bb12..d9e1c857 100644 --- a/src/devtools.ts +++ b/src/devtools/plugin.ts @@ -1,49 +1,25 @@ -import { - CustomInspectorNode, - CustomInspectorState, - setupDevtoolsPlugin, - TimelineEvent, -} from '@vue/devtools-api' -import { App, DebuggerEvent } from 'vue' -import { PiniaPluginContext, setActivePinia } from './rootStore' +import { setupDevtoolsPlugin, TimelineEvent } from '@vue/devtools-api' +import { App } from 'vue' +import { PiniaPluginContext, setActivePinia } from '../rootStore' import { GenericStore, GettersTree, MutationType, StateTree, _Method, -} from './types' - -function formatDisplay(display: string) { - return { - _custom: { - display, - }, - } -} +} from '../types' +import { + formatEventData, + formatMutationType, + formatStoreForInspectorState, + formatStoreForInspectorTree, +} from './formatting' /** * Registered stores used for devtools. */ const registeredStores = /*#__PURE__*/ new Set() -function toastMessage( - message: string, - type?: 'normal' | 'error' | 'warning' | undefined -) { - const piniaMessage = '🍍 ' + message - - if (typeof __VUE_DEVTOOLS_TOAST__ === 'function') { - __VUE_DEVTOOLS_TOAST__(piniaMessage, type) - } else if (type === 'error') { - console.error(piniaMessage) - } else if (type === 'warning') { - console.warn(piniaMessage) - } else { - console.log(piniaMessage) - } -} - let isAlreadyInstalled: boolean | undefined // timeline can be paused when directly changing the state let isTimelineActive = true @@ -270,79 +246,6 @@ export function addDevtools(app: App, store: GenericStore) { ) } -function formatStoreForInspectorTree(store: GenericStore): CustomInspectorNode { - return { - id: store.$id, - label: store.$id, - tags: [], - } -} - -function formatStoreForInspectorState( - store: GenericStore -): CustomInspectorState[string] { - const fields: CustomInspectorState[string] = [ - { editable: false, key: 'id', value: formatDisplay(store.$id) }, - { editable: true, key: 'state', value: store.$state }, - ] - - // avoid adding empty getters - if (store._getters?.length) { - fields.push({ - editable: false, - key: 'getters', - value: store._getters.reduce((getters, key) => { - getters[key] = store[key] - return getters - }, {} as GettersTree), - }) - } - - return fields -} - -function formatEventData(events: DebuggerEvent[] | DebuggerEvent | undefined) { - if (!events) return {} - if (Array.isArray(events)) { - // TODO: handle add and delete for arrays and objects - return events.reduce( - (data, event) => { - data.keys.push(event.key) - data.operations.push(event.type) - data.oldValue[event.key] = event.oldValue - data.newValue[event.key] = event.newValue - return data - }, - { - oldValue: {} as Record, - keys: [] as string[], - operations: [] as string[], - newValue: {} as Record, - } - ) - } else { - return { - operation: formatDisplay(events.type), - key: formatDisplay(events.key), - oldValue: events.oldValue, - newValue: events.newValue, - } - } -} - -function formatMutationType(type: MutationType): string { - switch (type) { - case MutationType.direct: - return 'mutation' - case MutationType.patchFunction: - return '$patch' - case MutationType.patchObject: - return '$patch' - default: - return 'unknown' - } -} - let runningActionId = 0 let activeAction: number | undefined @@ -395,5 +298,24 @@ export function devtoolsPlugin< } /** - * Another idea: wrap the getter/setter of all state properties to call setActiveAction. This way, we can directly use it in $subscribe to attach it to its action + * Shows a toast or console.log + * + * @param message - message to log + * @param type - different color of the tooltip */ +function toastMessage( + message: string, + type?: 'normal' | 'error' | 'warning' | undefined +) { + const piniaMessage = '🍍 ' + message + + if (typeof __VUE_DEVTOOLS_TOAST__ === 'function') { + __VUE_DEVTOOLS_TOAST__(piniaMessage, type) + } else if (type === 'error') { + console.error(piniaMessage) + } else if (type === 'warning') { + console.warn(piniaMessage) + } else { + console.log(piniaMessage) + } +} diff --git a/src/index.ts b/src/index.ts index c65f147b..cd8d286b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ -export { setActivePinia, createPinia } from './rootStore' +export { setActivePinia } from './rootStore' +export { createPinia } from './createPinia' export type { Pinia, PiniaStorePlugin, PiniaPluginContext } from './rootStore' export { defineStore } from './store' @@ -18,6 +19,7 @@ export type { PiniaCustomProperties, DefineStoreOptions, } from './types' +export { MutationType } from './types' export { mapActions, diff --git a/src/rootStore.ts b/src/rootStore.ts index feeffce3..c54ef5bc 100644 --- a/src/rootStore.ts +++ b/src/rootStore.ts @@ -1,6 +1,4 @@ -import { App, InjectionKey, Plugin, Ref, ref, warn } from 'vue' -import { devtoolsPlugin } from './devtools' -import { IS_CLIENT } from './env' +import { App, InjectionKey, Plugin, Ref, warn } from 'vue' import { StateTree, StoreWithState, @@ -61,49 +59,6 @@ export const storesMap = new WeakMap< > >() -/** - * Context argument passed to Pinia plugins. - */ -export interface PiniaPluginContext< - Id extends string = string, - S extends StateTree = StateTree, - G extends GettersTree = GettersTree, - A = Record -> { - /** - * pinia instance. - */ - pinia: Pinia - - /** - * Current app created with `Vue.createApp()`. - */ - app: App - - /** - * Current store being extended. - */ - store: Store - - /** - * Current store being extended. - */ - options: DefineStoreOptions -} - -/** - * Plugin to extend every store - */ -export interface PiniaStorePlugin { - /** - * Plugin to extend every store. Returns an object to extend the store or - * nothing. - * - * @param context - Context - */ - (context: PiniaPluginContext): Partial | void -} - /** * Every application must own its own pinia to be able to create stores */ @@ -160,53 +115,44 @@ export const piniaSymbol = (__DEV__ Symbol()) as InjectionKey /** - * Creates a Pinia instance to be used by the application + * Context argument passed to Pinia plugins. */ -export function createPinia(): Pinia { - // NOTE: here we could check the window object for a state and directly set it - // if there is anything like it with Vue 3 SSR - const state = ref({}) - - let localApp: App | undefined - let _p: Pinia['_p'] = [] - // plugins added before calling app.use(pinia) - const toBeInstalled: PiniaStorePlugin[] = [] - - const pinia: Pinia = { - install(app: App) { - pinia._a = localApp = app - // pinia._a = app - app.provide(piniaSymbol, pinia) - app.config.globalProperties.$pinia = pinia - // TODO: write test - // only set the app on client for devtools - if (__BROWSER__ && IS_CLIENT) { - // this allows calling useStore() outside of a component setup after - // installing pinia's plugin - setActivePinia(pinia) - } - toBeInstalled.forEach((plugin) => _p.push(plugin)) - }, - - use(plugin) { - if (!localApp) { - toBeInstalled.push(plugin) - } else { - _p.push(plugin) - } - return this - }, - - _p, - // it's actually undefined here - _a: localApp!, - - state, - } +export interface PiniaPluginContext< + Id extends string = string, + S extends StateTree = StateTree, + G extends GettersTree = GettersTree, + A = Record +> { + /** + * pinia instance. + */ + pinia: Pinia - if (IS_CLIENT && __BROWSER__ && __DEV__) { - pinia.use(devtoolsPlugin) - } + /** + * Current app created with `Vue.createApp()`. + */ + app: App - return pinia + /** + * Current store being extended. + */ + store: Store + + /** + * Current store being extended. + */ + options: DefineStoreOptions +} + +/** + * Plugin to extend every store + */ +export interface PiniaStorePlugin { + /** + * Plugin to extend every store. Returns an object to extend the store or + * nothing. + * + * @param context - Context + */ + (context: PiniaPluginContext): Partial | void } diff --git a/src/store.ts b/src/store.ts index 5aab347a..9eef8c41 100644 --- a/src/store.ts +++ b/src/store.ts @@ -140,10 +140,7 @@ function initStore( }) } - function $subscribe( - callback: SubscriptionCallback, - onTrigger?: (event: DebuggerEvent) => void - ) { + function $subscribe(callback: SubscriptionCallback) { subscriptions.push(callback) // watch here to link the subscription to the current active instance -- 2.47.3