]> git.ipfire.org Git - thirdparty/vuejs/pinia.git/commitdiff
refactor(plugins): make it work with actions
authorEduardo San Martin Morote <posva13@gmail.com>
Fri, 9 Jul 2021 18:48:29 +0000 (20:48 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Mon, 19 Jul 2021 09:51:12 +0000 (11:51 +0200)
__tests__/ssr.spec.ts
__tests__/storePlugins.spec.ts
__tests__/storeSetup.spec.ts
src/devtools/plugin.ts
src/mapHelpers.ts
src/rootStore.ts
src/store.ts
src/testing.ts
src/types.ts

index 9860ec601d7a3d89730db6cf2d941547f903c7fb..7d0a417b2a32c2fa2658e618449a27235b085d79 100644 (file)
@@ -9,7 +9,7 @@ import { useCartStore } from './pinia/stores/cart'
 
 describe('SSR', () => {
   const App = {
-    ssrRender(ctx: any, push: any, parent: any) {
+    ssrRender(ctx: any, push: any, _parent: any) {
       push(
         `<div>${ssrInterpolate(ctx.user.name)}: ${ssrInterpolate(
           ctx.cart.items
index 89621e49ae2241bc6712bb2bb010294c12c3a0ef..313571c3e2ebbdf7427ef5953105f75d92419874 100644 (file)
@@ -1,6 +1,6 @@
-import { createPinia, defineStore } from '../src'
+import { createPinia, defineSetupStore, defineStore } from '../src'
 import { mount } from '@vue/test-utils'
-import { App, ref, toRef } from 'vue'
+import { App, computed, Ref, ref, toRef } from 'vue'
 
 declare module '../src' {
   export interface PiniaCustomProperties<Id> {
@@ -156,4 +156,58 @@ describe('store plugins', () => {
     expect(store2.$state.shared).toBe(1)
     expect(store2.shared).toBe(1)
   })
+
+  it('passes the options of the options store', (done) => {
+    const options = {
+      id: 'main',
+      state: () => ({ n: 0 }),
+      actions: {
+        increment() {
+          // @ts-expect-error
+          this.n++
+        },
+      },
+      getters: {
+        a() {
+          return 'a'
+        },
+      },
+    }
+    const useStore = defineStore(options)
+    const pinia = createPinia()
+    mount({ template: 'none' }, { global: { plugins: [pinia] } })
+
+    pinia.use((context) => {
+      expect(context.options).toEqual(options)
+      done()
+    })
+    useStore(pinia)
+  })
+
+  it('passes the options of a setup store', (done) => {
+    function increment(n: Ref<number>) {
+      n.value++
+    }
+
+    const useStore = defineSetupStore('main', () => {
+      const n = ref(0)
+
+      const a = computed(() => 'a')
+
+      return { n, increment, a }
+    })
+    const pinia = createPinia()
+    mount({ template: 'none' }, { global: { plugins: [pinia] } })
+
+    pinia.use((context) => {
+      expect(context.options).toEqual({
+        actions: {
+          increment,
+        },
+      })
+      done()
+    })
+
+    useStore()
+  })
 })
index 6431f9c4bc776021ecf37123eab7ac94a4955ab6..df617d90b40b85da09e8deeee2fd5384e171f626 100644 (file)
@@ -39,6 +39,18 @@ describe('store with setup syntax', () => {
     expect(store.$state).not.toHaveProperty('increment')
   })
 
+  it('can store a function', () => {
+    const store = defineSetupStore('main', () => {
+      const fn = ref(() => {})
+      function action() {}
+      return { fn, action }
+    })()
+    expectType<{ fn: () => void }>(store.$state)
+    expect(store.$state).toEqual({ fn: expect.any(Function) })
+    expect(store.fn).toEqual(expect.any(Function))
+    store.action()
+  })
+
   it('can directly access state at the store level', () => {
     const store = useStore()
 
@@ -55,17 +67,6 @@ describe('store with setup syntax', () => {
     expect(upperCased.value).toBe('ED')
   })
 
-  // it('watch', () => {
-  //   setActivePinia(createPinia())
-  //   defineStore({
-  //     id: 'main',
-  //     state: () => ({
-  //       name: 'Eduardo',
-  //       counter: 0,
-  //     }),
-  //   })()
-  // })
-
   it('state can be watched', async () => {
     const store = useStore()
     const spy = jest.fn()
index 82b55e4585fb5553da8ab5e4f0e6deb8dbf5e63b..ade52b4e73ca0b883706b52e8f5af6883cdaed60 100644 (file)
@@ -368,7 +368,7 @@ export function devtoolsPlugin<
   A /* extends ActionsTree */ = ActionsTree
 >({ app, store, options, pinia }: PiniaPluginContext<Id, S, G, A>) {
   // original actions of the store as they are given by pinia. We are going to override them
-  const actions = Object.keys(options.actions || ({} as A)).reduce(
+  const actions = Object.keys(options.actions).reduce(
     (storeActions, actionName) => {
       // @ts-expect-error
       // use toRaw to avoid tracking #541
index 5f04f0e8cb1b6415ee1f08a16e98f55cf64596a9..7d4a5eba11dacc934c89eb5883946bb3527c617f 100644 (file)
@@ -273,7 +273,8 @@ export function mapState<
           // function
           return typeof storeKey === 'function'
             ? (storeKey as (store: Store<Id, S, G, A>) => any).call(this, store)
-            : store[storeKey as keyof typeof store]
+            : // @ts-expect-error
+              store[storeKey]
         }
         return reduced
       }, {} as _MapStateObjectReturn<Id, S, G, A, KeyMapper>)
index a5d276f9260716be60647769f42b39d4d4575d4a..a1402c9bc2aed697aaf650472e1dca492ec834ef 100644 (file)
@@ -11,6 +11,7 @@ import {
   ActionsTree,
   PiniaCustomStateProperties,
   GenericStore,
+  DefineStoreOptionsInPlugin,
 } from './types'
 
 /**
@@ -162,7 +163,7 @@ export interface PiniaPluginContext<
   /**
    * Current store being extended.
    */
-  options: DefineStoreOptions<Id, S, G, A>
+  options: DefineStoreOptionsInPlugin<Id, S, G, A>
 }
 
 /**
index 43c53dd650be5e0fe3a72b409f5e03a6f4bd494a..931543b3261ded5d58b604604a7b93214f4e6a48 100644 (file)
@@ -32,6 +32,9 @@ import {
   ActionsTree,
   SubscriptionCallbackMutation,
   _UnionToTuple,
+  DefineOptionStoreOptions,
+  DefineSetupStoreOptions,
+  DefineStoreOptionsInPlugin,
 } from './types'
 import {
   getActivePinia,
@@ -68,15 +71,6 @@ function innerPatch<T extends StateTree>(
 
 const { assign } = Object
 
-export interface DefineSetupStoreOptions<
-  Id extends string,
-  S extends StateTree,
-  G extends ActionsTree, // TODO: naming
-  A extends ActionsTree
-> {
-  hydrate?(store: Store<Id, S, G, A>, initialState: S | undefined): void
-}
-
 function isComputed(o: any): o is ComputedRef {
   return o && o.effect && o.effect.computed
 }
@@ -86,7 +80,10 @@ function createOptionsStore<
   S extends StateTree,
   G extends GettersTree<S>,
   A extends ActionsTree
->(options: DefineStoreOptions<Id, S, G, A>, pinia: Pinia): Store<Id, S, G, A> {
+>(
+  options: DefineOptionStoreOptions<Id, S, G, A>,
+  pinia: Pinia
+): Store<Id, S, G, A> {
   const { id, state, actions, getters } = options
   function $reset() {
     pinia.state.value[id] = state ? state() : {}
@@ -110,13 +107,7 @@ function createOptionsStore<
     )
   }
 
-  const store = createSetupStore(
-    id,
-    setup,
-    // TODO: actual hydrate option to be added to options store
-    // @ts-expect-error: fixme
-    options
-  )
+  const store = createSetupStore(id, setup, options)
 
   store.$reset = $reset
 
@@ -134,13 +125,16 @@ function createSetupStore<
 >(
   $id: Id,
   setup: () => SS,
-  options: DefineSetupStoreOptions<Id, S, G, A> = {}
+  options: DefineStoreOptions<Id, S, G, A> = {}
 ): Store<Id, S, G, A> {
   const pinia = getActivePinia()
   let scope!: EffectScope
-  const hydrate = options.hydrate || innerPatch
-  // @ts-expect-error
-  const state = options.state
+  const buildState = (options as DefineOptionStoreOptions<Id, S, G, A>).state
+
+  const optionsForPlugin: DefineStoreOptionsInPlugin<Id, S, G, A> = {
+    actions: {} as A,
+    ...options,
+  }
 
   // watcher options for $subscribe
   const $subscribeOptions: WatchOptions = { deep: true, flush: 'sync' }
@@ -168,7 +162,7 @@ function createSetupStore<
   let subscriptions: SubscriptionCallback<S>[] = markRaw([])
   let actionSubscriptions: StoreOnActionListener<Id, S, G, A>[] = markRaw([])
   let debuggerEvents: DebuggerEvent[] | DebuggerEvent
-  const initialState = pinia.state.value[$id] as S | undefined
+  const initialState = pinia.state.value[$id] as UnwrapRef<S> | undefined
 
   if (!initialState) {
     // should be set in Vue 2
@@ -187,6 +181,7 @@ function createSetupStore<
   }
 
   // TODO: idea create skipSerialize that marks properties as non serializable and they are skipped
+  // TODO: store the scope somewhere
   const setupStore = pinia._e.run(() => {
     scope = effectScope()
     return scope.run(() => {
@@ -291,8 +286,14 @@ function createSetupStore<
 
   function $reset() {
     // TODO: is it worth? probably should be removed
-    // maybe it can stop the effect and create it again
-    // pinia.state.value[$id] = buildState()
+    // maybe it can stop the effect and create it again but should be a plugin
+    if (buildState) {
+      pinia.state.value[$id] = buildState()
+    } else if (__DEV__) {
+      throw new Error(
+        `🍍: Store "${$id}" is build using the setup syntax and does not implement $reset().`
+      )
+    }
   }
 
   // overwrite existing actions to support $onAction
@@ -300,8 +301,8 @@ function createSetupStore<
     const prop = setupStore[key]
 
     if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
-      // @ts-expect-error: fixme
-      if (!options.state) {
+      // createOptionStore already did this
+      if (!buildState) {
         // mark it as a piece of state to be serialized
         pinia.state.value[$id][key] = toRef(setupStore as any, key)
       }
@@ -343,6 +344,9 @@ function createSetupStore<
 
         return ret
       }
+      // list actions so they can be used in plugins
+      // @ts-expect-error
+      optionsForPlugin.actions[key] = prop
     } else if (__DEV__ && IS_CLIENT) {
       // add getters for devtools
       if (isComputed(prop)) {
@@ -391,22 +395,30 @@ function createSetupStore<
         store,
         app: pinia._a,
         pinia,
-        // TODO: completely different options...
         // @ts-expect-error
-        options,
+        options: optionsForPlugin,
       })
       Object.keys(extensions || {}).forEach((key) =>
         store._customProperties.add(key)
       )
       assign(store, extensions)
     } else {
-      // @ts-expect-error: conflict between A and ActionsTree
-      assign(store, extender({ store, app: pinia._a, pinia, options }))
+      assign(
+        store,
+        extender({
+          // @ts-expect-error: conflict between A and ActionsTree
+          store,
+          app: pinia._a,
+          pinia,
+          // @ts-expect-error
+          options: optionsForPlugin,
+        })
+      )
     }
   })
 
   if (initialState) {
-    hydrate(store, initialState)
+    ;(options.hydrate || innerPatch)(store, initialState)
   }
 
   return store
@@ -548,7 +560,9 @@ export function defineStore<
   G extends GettersTree<S>,
   // cannot extends ActionsTree because we loose the typings
   A /* extends ActionsTree */
->(options: DefineStoreOptions<Id, S, G, A>): StoreDefinition<Id, S, G, A> {
+>(
+  options: DefineOptionStoreOptions<Id, S, G, A>
+): StoreDefinition<Id, S, G, A> {
   const { id } = options
 
   function useStore(pinia?: Pinia | null) {
index 18c3efef44263e610181ce47eba3a95e3d9beee7..75db70d285f029f63e9810bd6cea92d299bd2e4e 100644 (file)
@@ -85,10 +85,10 @@ export function createTestingPinia({
   const spiedActions = new Map<string, Record<string, any>>()
 
   pinia.use(({ store, options }) => {
-    if (!spiedActions.has(options.id)) {
-      spiedActions.set(options.id, {})
+    if (!spiedActions.has(store.$id)) {
+      spiedActions.set(store.$id, {})
     }
-    const actionsCache = spiedActions.get(options.id)!
+    const actionsCache = spiedActions.get(store.$id)!
 
     Object.keys(options.actions || {}).forEach((action) => {
       actionsCache[action] =
index d1eff336cb02e1384475730d294c9e2b6eba8b9d..a69ffba51d37988eab539f4f9796590e9e053ff4 100644 (file)
@@ -455,7 +455,7 @@ export type ActionsTree = Record<string, _Method>
  * Options parameter of `defineStore()`. Can be extended to augment stores with
  * the plugin API.
  */
-export interface DefineStoreOptions<
+export interface DefineOptionStoreOptions<
   Id extends string,
   S extends StateTree,
   G extends GettersTree<S>,
@@ -480,6 +480,7 @@ export interface DefineStoreOptions<
         StoreWithGetters<G> &
         PiniaCustomProperties
     >
+
   /**
    * Optional object of actions.
    */
@@ -491,8 +492,62 @@ export interface DefineStoreOptions<
         StoreWithGetters<GettersTree<S> extends G ? {} : G> &
         PiniaCustomProperties
     >
+
+  /**
+   * Allows hydrating the store during SSR when there is an available state in
+   * pinia.state.
+   *
+   * @param store - the store
+   * @param initialState - initialState
+   */
+  hydrate?(store: Store<Id, S, G, A>, initialState: UnwrapRef<S>): void
+}
+
+export interface DefineSetupStoreOptions<
+  Id extends string,
+  S extends StateTree,
+  G extends ActionsTree, // TODO: naming
+  A /* extends ActionsTree */
+> extends Pick<DefineOptionStoreOptions<Id, S, G, A>, 'hydrate'> {
+  /**
+   * Extracted actions. Added by useStore(). SHOULD NOT be added by the user when
+   * creating the store. Can be used in plugins to get the list of actions in a
+   * store defined with a setup function. Note this is always defined
+   */
+  actions?: A
+}
+
+/**
+ * Available `options` when creating a pinia plugin.
+ */
+export interface DefineStoreOptionsInPlugin<
+  Id extends string,
+  S extends StateTree,
+  G extends ActionsTree, // TODO: naming
+  A /* extends ActionsTree */
+> extends Omit<DefineOptionStoreOptions<Id, S, G, A>, 'id'> {
+  /**
+   * Extracted object of actions. Added by useStore() when the store is built
+   * using the setup API, otherwise uses the one passed to `defineStore()`.
+   * Defaults to an empty object if no actions are defined.
+   */
+  actions: A
+
+  /**
+   * Id of the store. Only available when the options API is used.
+   *
+   * @deprecated  Use `store.$id` instead.
+   */
+  id?: Id
 }
 
+export type DefineStoreOptions<
+  Id extends string,
+  S extends StateTree,
+  G extends GettersTree<S>,
+  A /* extends ActionsTree */
+> = DefineOptionStoreOptions<Id, S, G, A> | DefineSetupStoreOptions<Id, S, G, A>
+
 export type _UnionToTuple<U> = _UnionToTupleRecursively<[], U>
 
 type _Overwrite<T, S extends any> = {