Skip to content

Commit 951f895

Browse files
committed
improvement(audits): require an explicit partialize on every zustand persist
sim-stores.md requires persist to whitelist durable fields; check:zustand-v5 now fails on a persist with no partialize or one that spreads the whole state. canvas-mode was the one store without it.
1 parent 3ae2fa1 commit 951f895

2 files changed

Lines changed: 48 additions & 4 deletions

File tree

‎apps/sim/stores/canvas-mode/store.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const useCanvasModeStore = create<CanvasModeState>()(
1515
mode: 'hand',
1616
setMode: (mode) => set({ mode }),
1717
}),
18-
{ name: 'canvas-mode' }
18+
{ name: 'canvas-mode', partialize: (state) => ({ mode: state.mode }) }
1919
),
2020
{ name: 'canvas-mode-store' }
2121
)

‎scripts/check-zustand-v5-selectors.ts‎

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,49 @@ function auditFile(file: string, source: string): Violation[] {
294294
return violations
295295
}
296296

297+
/** A `persist(` middleware call; `.persist(` (an instance method) is excluded. */
298+
const PERSIST_CALL_PATTERN = /(?<![.\w$])persist\s*(?:<[^()]*?>)?\s*\(/g
299+
300+
/**
301+
* `.claude/rules/sim-stores.md`: every `persist` names its durable fields in `partialize`.
302+
* Without one, zustand writes the whole state — transient flags, drag state, `_hasHydrated` —
303+
* to storage and rehydrates it on the next load.
304+
*/
305+
function auditPersist(file: string, source: string): Violation[] {
306+
if (!/import\s*\{[^}]*\bpersist\b[^}]*\}\s*from\s*'zustand\/middleware'/.test(source)) return []
307+
const violations: Violation[] = []
308+
PERSIST_CALL_PATTERN.lastIndex = 0
309+
for (
310+
let match = PERSIST_CALL_PATTERN.exec(source);
311+
match;
312+
match = PERSIST_CALL_PATTERN.exec(source)
313+
) {
314+
if (hasSafeAnnotation(source, match.index)) continue
315+
const openParenIndex = match.index + match[0].length - 1
316+
const closeParenIndex = findMatchingParen(source, openParenIndex)
317+
if (closeParenIndex === -1) continue
318+
const call = source.slice(openParenIndex + 1, closeParenIndex)
319+
const hasPartialize = /\bpartialize\b/.test(call)
320+
const spreadsState = /\bpartialize\s*:\s*\(?\s*(\w+)[^)]*\)?\s*=>\s*\(\s*\{\s*\.\.\.\1\b/.test(
321+
call
322+
)
323+
if (hasPartialize && !spreadsState) continue
324+
violations.push({
325+
file,
326+
line: lineNumberAt(source, match.index),
327+
description: spreadsState
328+
? 'persist partialize spreads the whole state; return an explicit whitelist of durable fields'
329+
: 'persist has no partialize; add `partialize: (state) => ({ <durable fields> })` (sim-stores.md)',
330+
snippet: oneLineSnippet(
331+
source,
332+
match.index,
333+
Math.min(closeParenIndex + 1, match.index + 180)
334+
),
335+
})
336+
}
337+
return violations
338+
}
339+
297340
function returnsPrimitiveDerivedValue(selector: string): boolean {
298341
return (
299342
/\bObject\.(?:keys|values|entries)\s*\([^)]*\)\s*\.\s*(?:length|some|every)\b/.test(selector) ||
@@ -351,16 +394,17 @@ async function main() {
351394
const source = await readFile(file, 'utf8')
352395
const relativeFile = path.relative(ROOT, file)
353396
violations.push(...auditFile(relativeFile, source))
397+
violations.push(...auditPersist(relativeFile, source))
354398
}
355399

356400
if (violations.length === 0) {
357-
console.log('✅ Zustand v5 selector audit OK')
401+
console.log('✅ Zustand store audit OK (selectors, persist partialize)')
358402
return
359403
}
360404

361-
console.error('❌ Zustand v5 selector hazards found:')
405+
console.error('❌ Zustand store hazards found:')
362406
console.error(
363-
`Add useShallow/useStoreWithEqualityFn, split into primitive selectors, or document intentional exceptions with // ${SAFE_ANNOTATION} <reason>.`
407+
`Fix each as described, or document an intentional exception with // ${SAFE_ANNOTATION} <reason>.`
364408
)
365409
for (const violation of violations) {
366410
console.error(

0 commit comments

Comments
 (0)