import { createApp } from 'vue'
import App from './App.vue'
-import { createPinia } from '../../src'
+import { createPinia, StoreDefinition } from '../../src'
+import { ha } from './test'
-createApp(App).use(createPinia()).mount('#app')
+console.log({ ha })
+
+const pinia = createPinia()
+
+if (import.meta.hot) {
+ import.meta.hot.data.pinia = pinia
+ console.log('set', import.meta.hot.data)
+ // const isUseStore = (fn: any): fn is StoreDefinition => {
+ // return typeof fn === 'function' && typeof fn.$id === 'string'
+ // }
+
+ // // import.meta.hot.accept(
+ // // './stores/counter.ts',
+ // // (newStore: Record<string, unknown>) => {
+ // // console.log('haha', newStore)
+ // // }
+ // // )
+
+ // import.meta.hot.accept('./test.ts', (newTest) => {
+ // console.log('test updated', newTest)
+ // })
+
+ // const stores = import.meta.glob('./stores/*.ts')
+
+ // for (const storeId in stores) {
+ // console.log('configuring HMR for', storeId)
+ // const oldUseStore = await stores[storeId]()
+ // console.log('got', oldUseStore)
+ // import.meta.hot!.accept(storeId, (newStore: Record<string, unknown>) => {
+ // console.log('Accepting update for', storeId)
+ // for (const exportName in newStore) {
+ // const useStore = newStore[exportName]
+ // if (isUseStore(useStore) && pinia._s.has(useStore.$id)) {
+ // const id = useStore.$id
+ // const existingStore = pinia._s.get(id)!
+ // // remove the existing store from the cache to force a new one
+ // pinia._s.delete(id)
+ // // this adds any new state to pinia and then runs the `hydrate` function
+ // // which, by default, will reuse the existing state in pinia
+ // const newStore = useStore(pinia)
+ // // pinia._s.set(id, existingStore)
+ // }
+ // }
+ // })
+ // }
+}
+
+// TODO: HMR for plugins
+
+createApp(App).use(pinia).mount('#app')
-import { defineStore } from '../../../src'
+import { defineStore, Pinia, Store, StoreDefinition } from '../../../src'
const delay = (t: number) => new Promise((r) => setTimeout(r, t))
id: 'counter',
state: () => ({
- n: 0,
+ n: 5,
incrementedTimes: 0,
decrementedTimes: 0,
numbers: [] as number[],
this.n += amount
},
+ newOne() {
+ console.log('neeeew')
+ },
+
async fail() {
const n = this.n
await delay(1000)
},
},
})
+
+if (import.meta.hot) {
+ const isUseStore = (fn: any): fn is StoreDefinition => {
+ return typeof fn === 'function' && typeof fn.$id === 'string'
+ }
+
+ const oldUseStore = useCounter
+ import.meta.hot.accept((newStore) => {
+ if (!import.meta.hot) throw new Error('import.meta.hot disappeared')
+
+ const pinia: Pinia | undefined =
+ import.meta.hot.data.pinia || oldUseStore._pinia
+
+ if (!pinia) {
+ console.warn(`Missing the pinia instance for "${oldUseStore.$id}".`)
+ return import.meta.hot.invalidate()
+ }
+
+ // preserve the pinia instance across loads
+ import.meta.hot.data.pinia = pinia
+
+ console.log('got data', newStore)
+ for (const exportName in newStore) {
+ const useStore = newStore[exportName]
+ console.log('checking for', exportName)
+ if (isUseStore(useStore) && pinia._s.has(useStore.$id)) {
+ console.log('Accepting update for', useStore.$id)
+ const id = useStore.$id
+
+ if (id !== oldUseStore.$id) {
+ console.warn(
+ `The id of the store changed from "${oldUseStore.$id}" to "${id}". Reloading.`
+ )
+ return import.meta.hot!.invalidate()
+ }
+
+ const existingStore: Store = pinia._s.get(id)!
+ if (!existingStore) {
+ console.log(`skipping hmr because store doesn't exist yet`)
+ // TODO: replace the useCounter var???
+ return
+ }
+ console.log('patching')
+ useStore(pinia, existingStore)
+ // remove the existing store from the cache to force a new one
+ // pinia._s.delete(id)
+ // this adds any new state to pinia and then runs the `hydrate` function
+ // which, by default, will reuse the existing state in pinia
+ // const newStore = useStore(pinia)
+ // console.log('going there', newStore._hmrPayload)
+ // pinia._s.set(id, existingStore)
+ }
+ }
+ })
+}
ComputedRef,
toRef,
toRefs,
+ Ref,
+ ref,
} from 'vue'
import {
StateTree,
activePinia,
} from './rootStore'
import { IS_CLIENT } from './env'
+import { createPinia } from './createPinia'
function innerPatch<T extends StateTree>(
target: T,
S extends StateTree,
G extends GettersTree<S>,
A extends ActionsTree
->(options: DefineStoreOptions<Id, S, G, A>, pinia: Pinia): Store<Id, S, G, A> {
+>(
+ options: DefineStoreOptions<Id, S, G, A>,
+ pinia: Pinia,
+ hot?: boolean
+): Store<Id, S, G, A> {
const { id, state, actions, getters } = options
function $reset() {
pinia.state.value[id] = state ? state() : {}
}
+ const initialState: StateTree | undefined = pinia.state.value[id]
+
function setup() {
- $reset()
+ if (!initialState) {
+ $reset()
+ }
// pinia.state.value[id] = state ? state() : {}
return assign(
- toRefs(pinia.state.value[id]),
+ initialState || toRefs(pinia.state.value[id]),
actions,
Object.keys(getters || {}).reduce((computedGetters, name) => {
computedGetters[name] = computed(() => {
)
}
- const store = createSetupStore(id, setup, options)
+ const store = createSetupStore(id, setup, options, hot)
+
+ // TODO: HMR should also replace getters here
store.$reset = $reset
setup: () => SS,
options:
| DefineSetupStoreOptions<Id, S, G, A>
- | DefineStoreOptions<Id, S, G, A> = {}
+ | DefineStoreOptions<Id, S, G, A> = {},
+ hot?: boolean
): Store<Id, S, G, A> {
const pinia = getActivePinia()
let scope!: EffectScope
return scope.run(() => {
const store = setup()
- watch(
- () => pinia.state.value[$id] as UnwrapRef<S>,
- (state, oldState) => {
- if (isListening) {
- triggerSubscriptions(
- {
- storeId: $id,
- type: MutationType.direct,
- events: debuggerEvents as DebuggerEvent,
- },
- state
- )
- }
- },
- $subscribeOptions
- )!
+ // skip setting up the watcher on HMR
+ if (!__DEV__ || !hot) {
+ watch(
+ () => pinia.state.value[$id] as UnwrapRef<S>,
+ (state, oldState) => {
+ if (isListening) {
+ triggerSubscriptions(
+ {
+ storeId: $id,
+ type: MutationType.direct,
+ events: debuggerEvents as DebuggerEvent,
+ },
+ state
+ )
+ }
+ },
+ $subscribeOptions
+ )!
+ }
return store
})
}
}
+ /**
+ * Wraps an action to handle subscriptions
+ *
+ * @param name - name of the action
+ * @param action - action to wrap
+ * @returns a wrapped action to handle subscriptions
+ */
+ function wrapAction(name: string, action: _Method) {
+ return function (this: any) {
+ setActivePinia(pinia)
+ const args = Array.from(arguments)
+
+ let afterCallback: (resolvedReturn: any) => void = noop
+ let onErrorCallback: (error: unknown) => void = noop
+ function after(callback: typeof afterCallback) {
+ afterCallback = callback
+ }
+ function onError(callback: typeof onErrorCallback) {
+ onErrorCallback = callback
+ }
+
+ actionSubscriptions.forEach((callback) => {
+ // @ts-expect-error
+ callback({
+ args,
+ name,
+ store,
+ after,
+ onError,
+ })
+ })
+
+ let ret: any
+ try {
+ ret = action.apply(this || store, args)
+ Promise.resolve(ret).then(afterCallback).catch(onErrorCallback)
+ } catch (error) {
+ onErrorCallback(error)
+ throw error
+ }
+
+ return ret
+ }
+ }
+
+ // TODO: PURE to tree shake?
+ const _hmrPayload = markRaw({
+ actions: {} as Record<string, any>,
+ getters: {} as Record<string, Ref>,
+ state: [] as string[],
+ })
+
// overwrite existing actions to support $onAction
for (const key in setupStore) {
const prop = setupStore[key]
// mark it as a piece of state to be serialized
pinia.state.value[$id][key] = toRef(setupStore as any, key)
}
+ if (__DEV__) {
+ _hmrPayload.state.push(key)
+ }
// action
} else if (typeof prop === 'function') {
// @ts-expect-error: we are overriding the function
- setupStore[key] = function () {
- setActivePinia(pinia)
- const args = Array.from(arguments)
-
- let afterCallback: (resolvedReturn: any) => void = noop
- let onErrorCallback: (error: unknown) => void = noop
- function after(callback: typeof afterCallback) {
- afterCallback = callback
- }
- function onError(callback: typeof onErrorCallback) {
- onErrorCallback = callback
- }
+ // we avoid wrapping if this a hot module replacement store
+ setupStore[key] = __DEV__ && hot ? prop : wrapAction(key, prop)
- actionSubscriptions.forEach((callback) => {
- // @ts-expect-error
- callback({
- args,
- name: key,
- store,
- after,
- onError,
- })
- })
-
- let ret: any
- try {
- ret = prop.apply(this || store, args)
- Promise.resolve(ret).then(afterCallback).catch(onErrorCallback)
- } catch (error) {
- onErrorCallback(error)
- throw error
- }
-
- return ret
+ if (__DEV__) {
+ _hmrPayload.actions[key] = prop
}
+
// list actions so they can be used in plugins
// @ts-expect-error
- optionsForPlugin.actions[key] = prop
- } else if (__DEV__ && IS_CLIENT) {
+ optionsForPlugin.actions[key] = setupStore[key] // TODO: check this change from `prop` is correct
+ } else if (__DEV__) {
// add getters for devtools
if (isComputed(prop)) {
- const getters: string[] =
- // @ts-expect-error: it should be on the store
- setupStore._getters || (setupStore._getters = markRaw([]))
- getters.push(key)
+ _hmrPayload.getters[key] = buildState
+ ? // @ts-expect-error
+ options.getters[key]
+ : prop
+ if (IS_CLIENT) {
+ const getters: string[] =
+ // @ts-expect-error: it should be on the store
+ setupStore._getters || (setupStore._getters = markRaw([]))
+ getters.push(key)
+ }
}
}
}
? // devtools custom properties
{
_customProperties: markRaw(new Set<string>()),
+ _hmrPayload,
}
: {},
partialStore,
;(options.hydrate || innerPatch)(store, initialState)
}
+ if (__DEV__) {
+ store.hotUpdate = (newStore) => {
+ newStore._hmrPayload.state.forEach((stateKey) => {
+ if (!(stateKey in store.$state)) {
+ console.log('setting new key', stateKey)
+ // @ts-expect-error
+ // transfer the ref
+ store.$state[stateKey] = newStore.$state[stateKey]
+ }
+ })
+
+ // remove deleted keys
+ Object.keys(store.$state).forEach((stateKey) => {
+ if (!(stateKey in newStore.$state)) {
+ console.log('deleting old key', stateKey)
+ // @ts-expect-error
+ delete store.$state[stateKey]
+ }
+ })
+
+ for (const actionName in newStore._hmrPayload.actions) {
+ const action: _Method =
+ // @ts-expect-error
+ newStore[actionName]
+
+ // @ts-expect-error: new key
+ store[actionName] =
+ // new line forced for TS
+ wrapAction(actionName, action)
+ }
+
+ for (const getterName in newStore._hmrPayload.getters) {
+ const getter: _Method = newStore._hmrPayload.getters[getterName]
+
+ // @ts-expect-error
+ store[getterName] =
+ // ---
+ buildState
+ ? // special handling of options api
+ computed(() => {
+ setActivePinia(pinia)
+ return getter.call(store, store)
+ })
+ : getter
+ }
+
+ // TODO: remove old actions and getters
+ }
+ }
+
isListening = true
return store
}
_ExtractActionsFromSetupStore<SS>
> {
function useStore(
- pinia?: Pinia | null
+ pinia?: Pinia | null,
+ hot?: Store
): Store<
Id,
_ExtractStateFromSetupStore<SS>,
if (!pinia._s.has(id)) {
pinia._s.set(id, createSetupStore(id, storeSetup, options))
+ if (__DEV__) {
+ // @ts-expect-error: not the right inferred type
+ useStore._pinia = pinia
+ }
}
const store: Store<
_ExtractActionsFromSetupStore<SS>
>
+ if (__DEV__ && hot) {
+ const hotId = '__hot:' + id
+ const newStore = createSetupStore(hotId, storeSetup, options, true)
+ hot.hotUpdate(newStore as any)
+
+ // for state that exists in newStore, try to copy from old state
+
+ // actions
+
+ // cleanup the things
+ delete pinia.state.value[hotId]
+ pinia._s.delete(hotId)
+
+ // TODO: add the patched store to devtools again to override its previous version
+ // addDevtools(pinia._a, hot)
+ }
+
// save stores in instances to access them devtools
if (__DEV__ && IS_CLIENT && currentInstance && currentInstance.proxy) {
const vm = currentInstance.proxy
>(options: DefineStoreOptions<Id, S, G, A>): StoreDefinition<Id, S, G, A> {
const { id } = options
- function useStore(pinia?: Pinia | null) {
+ function useStore(pinia?: Pinia | null, hot?: Store) {
const currentInstance = getCurrentInstance()
pinia =
// in test mode, ignore the argument provided as we can always retrieve a
pinia
)
)
+
+ if (__DEV__) {
+ // @ts-expect-error: not the right inferred type
+ useStore._pinia = pinia
+ }
}
const store: Store<Id, S, G, A> = pinia._s.get(id)! as Store<Id, S, G, A>
+ if (__DEV__ && hot) {
+ const hotId = '__hot:' + id
+ const newStore = createOptionsStore(
+ assign({}, options, { id: hotId }) as any,
+ pinia,
+ true
+ )
+ hot.hotUpdate(newStore as any)
+
+ // for state that exists in newStore, try to copy from old state
+
+ // actions
+
+ // cleanup the things
+ delete pinia.state.value[hotId]
+ pinia._s.delete(hotId)
+
+ // TODO: add the patched store to devtools again to override its previous version
+ // addDevtools(pinia._a, hot)
+ }
+
// save stores in instances to access them devtools
if (__DEV__ && IS_CLIENT && currentInstance && currentInstance.proxy) {
const vm = currentInstance.proxy