]> git.ipfire.org Git - thirdparty/vuejs/pinia.git/commitdiff
feat: store plugins
authorEduardo San Martin Morote <posva13@gmail.com>
Tue, 5 Jan 2021 14:53:54 +0000 (15:53 +0100)
committerEduardo San Martin Morote <posva13@gmail.com>
Tue, 5 Jan 2021 14:53:54 +0000 (15:53 +0100)
__tests__/store.spec.ts
__tests__/storePlugins.spec.ts [new file with mode: 0644]
src/index.ts
src/rootStore.ts
src/store.ts

index 601c16f6e57b38e25ac4b004b46becfcc27bee1d..01702135e93b36744c1d909553df4cd5a8ef9971 100644 (file)
@@ -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 (file)
index 0000000..fbfca39
--- /dev/null
@@ -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)
+  })
+})
index 7b86b398114ff762bc89e5f85dc4d54d3dd57345..dfabe4204956e5765371fe13d612ef9e9d7ee670 100644 (file)
@@ -4,6 +4,8 @@ export {
   getRootState,
   createPinia,
   Pinia,
+  PiniaStorePlugin,
+  PiniaCustomProperties,
 } from './rootStore'
 export { defineStore } from './store'
 export {
index 6b85bd54561cf51e942ada71b627360d65b1a095..9aabaa4a10f08a4d137eb44c52f02ee299e87d6d 100644 (file)
@@ -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<PiniaCustomProperties>
+}
+
 /**
  * Every application must own its own pinia to be able to create stores
  */
@@ -95,7 +102,21 @@ export interface Pinia {
   /**
    * root state
    */
-  state: Ref<any>
+  state: Ref<Record<string, StateTree>>
+
+  /**
+   * 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<PiniaCustomProperties>>
 }
 
 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
  */
index f9648d2bde955dd92ffce427e87420743bee647b..bc08d83600e21afd11d7915398e3dcbf570c9d41 100644 (file)
@@ -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<A>[typeof actionName]
   }
 
+  const extensions = _p._p.reduce(
+    (extended, extender) => ({
+      ...extended,
+      ...extender(),
+    }),
+    {} as PiniaCustomProperties
+  )
+
   const store: Store<Id, S, G, A> = reactive({
+    ...extensions,
     ...partialStore,
     // using this means no new properties can be added as state
     ...computedFromState(_p.state, $id),