From f027bf587b37c7fc30eba4da5f90d52d99e6536d Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Tue, 5 Jan 2021 15:53:54 +0100 Subject: [PATCH] feat: store plugins --- __tests__/store.spec.ts | 25 ++++++----------- __tests__/storePlugins.spec.ts | 47 ++++++++++++++++++++++++++++++++ src/index.ts | 2 ++ src/rootStore.ts | 50 ++++++++++++++++++++++++++++++++-- src/store.ts | 10 +++++++ 5 files changed, 116 insertions(+), 18 deletions(-) create mode 100644 __tests__/storePlugins.spec.ts diff --git a/__tests__/store.spec.ts b/__tests__/store.spec.ts index 601c16f6..01702135 100644 --- a/__tests__/store.spec.ts +++ b/__tests__/store.spec.ts @@ -1,10 +1,4 @@ -import { - createPinia, - defineStore, - setActivePinia, - setStateProvider, - Pinia, -} from '../src' +import { createPinia, defineStore, setActivePinia, Pinia } from '../src' import { mount } from '@vue/test-utils' import { getCurrentInstance, nextTick, watch } from 'vue' @@ -61,7 +55,8 @@ describe('Store', () => { }) it('can hydrate the state', () => { - setActivePinia(createPinia()) + const pinia = createPinia() + setActivePinia(pinia) const useStore = defineStore({ id: 'main', state: () => ({ @@ -73,15 +68,13 @@ describe('Store', () => { }), }) - setStateProvider(() => ({ - main: { - a: false, - nested: { - foo: 'bar', - a: { b: 'string' }, - }, + pinia.state.value.main = { + a: false, + nested: { + foo: 'bar', + a: { b: 'string' }, }, - })) + } const store = useStore() diff --git a/__tests__/storePlugins.spec.ts b/__tests__/storePlugins.spec.ts new file mode 100644 index 00000000..fbfca39f --- /dev/null +++ b/__tests__/storePlugins.spec.ts @@ -0,0 +1,47 @@ +import { createPinia, defineStore } from '../src' +import { mount } from '@vue/test-utils' +import { App } from 'vue' + +declare module '../src' { + export interface PiniaCustomProperties { + n: number + uid: App['_uid'] + hasApp: boolean + } +} + +describe('store plugins', () => { + const useStore = defineStore({ id: 'test' }) + it('adds properties to stores', () => { + const pinia = createPinia() + + mount({ template: 'none' }, { global: { plugins: [pinia] } }) + + // must call use after installing the plugin + pinia.use((app) => { + return { n: 20, uid: app._uid } + }) + + const store = useStore(pinia) + + expect(store.n).toBe(20) + expect(store.uid).toBeDefined() + }) + + it('can install plugins before installing pinia', () => { + const pinia = createPinia() + + pinia.use(() => ({ n: 1 })) + pinia.use((app) => ({ uid: app._uid })) + + mount({ template: 'none' }, { global: { plugins: [pinia] } }) + + pinia.use((app) => ({ hasApp: !!app })) + + const store = useStore(pinia) + + expect(store.n).toBe(1) + expect(store.uid).toBeDefined() + expect(store.hasApp).toBe(true) + }) +}) diff --git a/src/index.ts b/src/index.ts index 7b86b398..dfabe420 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,8 @@ export { getRootState, createPinia, Pinia, + PiniaStorePlugin, + PiniaCustomProperties, } from './rootStore' export { defineStore } from './store' export { diff --git a/src/rootStore.ts b/src/rootStore.ts index 6b85bd54..9aabaa4a 100644 --- a/src/rootStore.ts +++ b/src/rootStore.ts @@ -86,6 +86,13 @@ export let clientApp: App | undefined export const setClientApp = (app: App) => (clientApp = app) export const getClientApp = () => clientApp +/** + * Plugin to extend every store + */ +export interface PiniaStorePlugin { + (app: App): Partial +} + /** * Every application must own its own pinia to be able to create stores */ @@ -95,7 +102,21 @@ export interface Pinia { /** * root state */ - state: Ref + state: Ref> + + /** + * Adds a store plugin to extend every store + * + * @param plugin - store plugin to add + */ + use(plugin: PiniaStorePlugin): void + + /** + * Installed store plugins + * + * @internal + */ + _p: Array<() => Partial> } declare module '@vue/runtime-core' { @@ -115,25 +136,50 @@ export const piniaSymbol = (__DEV__ * 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) { + localApp = app + // pinia._a = app app.provide(piniaSymbol, pinia) app.config.globalProperties.$pinia = pinia // TODO: write test - // only set the app on client + // only set the app on client for devtools if (__BROWSER__ && IS_CLIENT) { setClientApp(app) } + toBeInstalled.forEach((plugin) => _p.push(plugin.bind(null, localApp!))) + }, + + use(plugin) { + if (!localApp) { + toBeInstalled.push(plugin) + } else { + _p.push(plugin.bind(null, localApp)) + } }, + _p, + state, } return pinia } +/** + * Properties that are added to every store by `pinia.use()` + */ +export interface PiniaCustomProperties {} + /** * Registered stores */ diff --git a/src/store.ts b/src/store.ts index f9648d2b..bc08d836 100644 --- a/src/store.ts +++ b/src/store.ts @@ -19,6 +19,7 @@ import { getClientApp, piniaSymbol, Pinia, + PiniaCustomProperties, } from './rootStore' import { addDevtools } from './devtools' import { IS_CLIENT } from './env' @@ -205,7 +206,16 @@ function buildStoreToUse< } as StoreWithActions[typeof actionName] } + const extensions = _p._p.reduce( + (extended, extender) => ({ + ...extended, + ...extender(), + }), + {} as PiniaCustomProperties + ) + const store: Store = reactive({ + ...extensions, ...partialStore, // using this means no new properties can be added as state ...computedFromState(_p.state, $id), -- 2.47.3