Skip to content

Commit 29835d1

Browse files
committed
feat: diagnostic wrong values in plugins
1 parent 2bc4871 commit 29835d1

4 files changed

Lines changed: 79 additions & 82 deletions

File tree

packages/pinia/__tests__/storePlugins.spec.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { describe, it, expect, vi } from 'vitest'
22
import { createPinia, defineStore } from '../src'
33
import { mount } from '@vue/test-utils'
4-
import { App, computed, ref, toRef, watch } from 'vue'
4+
import { App, computed, markRaw, reactive, ref, toRef, watch } from 'vue'
5+
import { mockWarn } from './vitest-mock-warn'
56

67
declare module '../src' {
78
export interface PiniaCustomProperties<Id> {
@@ -13,6 +14,9 @@ declare module '../src' {
1314
globalB: string
1415
shared: number
1516
double: number
17+
external: { dark: boolean } | null
18+
reactiveProp: { n: number }
19+
refProp: number
1620
}
1721

1822
export interface PiniaCustomStateProperties<S> {
@@ -299,4 +303,44 @@ describe('store plugins', () => {
299303

300304
expect(spy).toHaveBeenCalledTimes(1)
301305
})
306+
307+
describe('non-reactive properties', () => {
308+
mockWarn()
309+
310+
it('warns when a plugin adds a non-reactive object property', () => {
311+
const pinia = createPinia()
312+
pinia.use(() => ({ external: { dark: true } }))
313+
mount({ template: 'none' }, { global: { plugins: [pinia] } })
314+
315+
useStore(pinia)
316+
expect('"external"').toHaveBeenWarned()
317+
expect('is not reactive').toHaveBeenWarned()
318+
})
319+
320+
it('warns when a plugin adds a null property', () => {
321+
const pinia = createPinia()
322+
pinia.use(() => ({ external: null }))
323+
mount({ template: 'none' }, { global: { plugins: [pinia] } })
324+
325+
useStore(pinia)
326+
expect('"external"').toHaveBeenWarned()
327+
})
328+
329+
it('does not warn on markRaw, reactive, ref or primitive plugin properties', () => {
330+
const pinia = createPinia()
331+
pinia.use(() => ({
332+
external: markRaw({ dark: true }),
333+
reactiveProp: reactive({ n: 1 }),
334+
// @ts-expect-error: cannot be a ref yet
335+
refProp: ref(2),
336+
shared: 10,
337+
}))
338+
mount({ template: 'none' }, { global: { plugins: [pinia] } })
339+
340+
const store = useStore(pinia)
341+
// mockWarn() guarantees the diagnostic was not emitted for these
342+
expect(store.external).toEqual({ dark: true })
343+
expect(store.shared).toBe(10)
344+
})
345+
})
302346
})

packages/pinia/__tests__/storeToRefs.spec.ts

Lines changed: 4 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, beforeEach, it, expect, vi } from 'vitest'
2-
import { computed, markRaw, reactive, ref, ToRefs } from 'vue'
2+
import { computed, reactive, ref, ToRefs } from 'vue'
33
import { createPinia, defineStore, setActivePinia, storeToRefs } from '../src'
44
import { mockWarn } from './vitest-mock-warn'
55

@@ -203,7 +203,7 @@ describe('storeToRefs', () => {
203203
expect(spy).toHaveBeenCalledTimes(0)
204204
})
205205

206-
it('does not crash and warns on a non-reactive null value', () => {
206+
it('does not crash on a non-reactive null value', () => {
207207
const useStore = defineStore('null-val', () => {
208208
return {
209209
nullableItem: null as null | { id: number },
@@ -217,52 +217,8 @@ describe('storeToRefs', () => {
217217
expect(refs).toHaveProperty('text')
218218
expect(refs).not.toHaveProperty('nullableItem')
219219
expect(refs.text.value).toBe('hello')
220-
expect('"nullableItem"').toHaveBeenWarned()
221-
expect('is not reactive').toHaveBeenWarned()
222-
})
223-
224-
it('warns on a non-reactive plain object value', () => {
225-
const useStore = defineStore('plain-obj', () => {
226-
return { config: { dark: true }, textRef: ref('hi') }
227-
})
228-
229-
storeToRefs(useStore())
230-
expect('"config"').toHaveBeenWarned()
231-
expect('"textRef"').not.toHaveBeenWarned()
232-
})
233-
234-
it('does not warn on markRaw non-reactive properties', () => {
235-
const useStore = defineStore('raw-prop', () => {
236-
return { external: markRaw({ dark: true }), text: ref('hi') }
237-
})
238-
239-
const refs = storeToRefs(useStore())
240-
expect(refs).not.toHaveProperty('external')
241-
expect(refs).toHaveProperty('text')
242-
})
243-
244-
it('warns on a non-reactive property added by a plugin', () => {
245-
const pinia = createPinia()
246-
// directly push because no app
247-
pinia._p.push(() => // @ts-expect-error: invalid state
248-
({ external: { dark: true } }))
249-
setActivePinia(pinia)
250-
251-
const refs = storeToRefs(defineStore('a', () => ({ n: ref(0) }))())
252-
expect(refs).not.toHaveProperty('external')
253-
expect('"external"').toHaveBeenWarned()
254-
})
255-
256-
it('does not warn on a markRaw property added by a plugin', () => {
257-
const pinia = createPinia()
258-
pinia._p.push(() => ({ external: markRaw({ dark: true }), shared: 10 }))
259-
setActivePinia(pinia)
260-
261-
const refs = storeToRefs(defineStore('a', () => ({ n: ref(0) }))())
262-
expect(refs).not.toHaveProperty('external')
263-
// primitives are skipped silently too
264-
expect(refs).not.toHaveProperty('shared')
265-
expect(refs).toHaveProperty('n')
220+
// mockWarn() asserts storeToRefs() stays silent: the warning is emitted when
221+
// the property is added by a plugin, not here
266222
})
267223

268224
tds(() => {

packages/pinia/src/store.ts

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -693,33 +693,42 @@ function createSetupStore<
693693

694694
// apply all plugins
695695
pinia._p.forEach((extender) => {
696+
const extensions = scope.run(() =>
697+
extender({
698+
store: store as Store,
699+
app: pinia._a,
700+
pinia,
701+
options: optionsForPlugin,
702+
})
703+
)!
704+
696705
/* istanbul ignore else */
697706
if (__USE_DEVTOOLS__ && IS_CLIENT) {
698-
const extensions = scope.run(() =>
699-
extender({
700-
store: store as Store,
701-
app: pinia._a,
702-
pinia,
703-
options: optionsForPlugin,
704-
})
705-
)!
706707
Object.keys(extensions || {}).forEach((key) =>
707708
store._customProperties.add(key)
708709
)
709-
assign(store, extensions)
710-
} else {
711-
assign(
712-
store,
713-
scope.run(() =>
714-
extender({
715-
store: store as Store,
716-
app: pinia._a,
717-
pinia,
718-
options: optionsForPlugin,
719-
})
720-
)!
721-
)
722710
}
711+
712+
// Check properties that are not properly configured. We check the values
713+
// as the plugin returned them: once assigned to the store, a `reactive()`
714+
// value is unwrapped and indistinguishable from a plain object.
715+
if (__DEV__) {
716+
for (const key in extensions) {
717+
const value = (extensions as any)[key]
718+
// `null` is included (typeof null === 'object'). refs, reactive objects,
719+
// primitives, functions and markRaw() values are skipped.
720+
if (
721+
typeof value === 'object' &&
722+
!isRef(value) &&
723+
!isReactive(value) &&
724+
!value?.__v_skip
725+
) {
726+
diagnostics.PINIA_R1006({ key, id: $id })
727+
}
728+
}
729+
}
730+
731+
assign(store, extensions)
723732
})
724733

725734
if (

packages/pinia/src/storeToRefs.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
ToRefs,
1010
WritableComputedRef,
1111
} from 'vue'
12-
import { diagnostics } from './diagnostics'
1312
import { StoreGetters, StoreState } from './store'
1413
import type {
1514
_ActionsTree,
@@ -110,17 +109,6 @@ export function storeToRefs<SS extends StoreGeneric>(
110109
refs[key] =
111110
// ---
112111
toRef(store, key)
113-
} else if (
114-
__DEV__ &&
115-
// an object that isn't a ref/reactive and that the user didn't markRaw()
116-
// is silently dropped: warn so they wrap it as state or markRaw it.
117-
// `null` is included (typeof null === 'object') as it is the most common
118-
// way to hit this. Primitives, functions (actions) and store API like
119-
// `$id` are skipped on purpose.
120-
typeof value === 'object' &&
121-
!(value as any)?.__v_skip
122-
) {
123-
diagnostics.PINIA_R1006({ key, id: store.$id })
124112
}
125113
}
126114

0 commit comments

Comments
 (0)