@buoy-gg/zustand 7.0.38 → 7.0.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/lib/commonjs/index.js +105 -1
  2. package/lib/commonjs/preset.js +102 -1
  3. package/lib/commonjs/zustand/components/ZustandActionButton.js +118 -1
  4. package/lib/commonjs/zustand/components/ZustandDetailViewToggle.js +48 -1
  5. package/lib/commonjs/zustand/components/ZustandEventFilterView.js +43 -1
  6. package/lib/commonjs/zustand/components/ZustandIcon.js +18 -1
  7. package/lib/commonjs/zustand/components/ZustandModal.js +772 -1
  8. package/lib/commonjs/zustand/components/ZustandStateChangeItem.js +117 -1
  9. package/lib/commonjs/zustand/components/ZustandStateDetailContent.js +372 -1
  10. package/lib/commonjs/zustand/components/ZustandStateInfoView.js +488 -1
  11. package/lib/commonjs/zustand/components/ZustandStoreBrowser.js +204 -1
  12. package/lib/commonjs/zustand/components/ZustandStoreDetailContent.js +471 -1
  13. package/lib/commonjs/zustand/components/ZustandStoreEditorScreen.js +393 -1
  14. package/lib/commonjs/zustand/hooks/useZustandStateChanges.js +74 -1
  15. package/lib/commonjs/zustand/sync/zustandSyncAdapter.js +382 -1
  16. package/lib/commonjs/zustand/utils/buoyZustandMiddleware.js +284 -1
  17. package/lib/commonjs/zustand/utils/editableState.js +146 -1
  18. package/lib/commonjs/zustand/utils/snapshotProvider.js +209 -1
  19. package/lib/commonjs/zustand/utils/testIDs.js +82 -1
  20. package/lib/commonjs/zustand/utils/zustandStateStore.js +379 -1
  21. package/lib/module/index.js +90 -1
  22. package/lib/module/preset.js +98 -1
  23. package/lib/module/zustand/components/ZustandActionButton.js +114 -1
  24. package/lib/module/zustand/components/ZustandDetailViewToggle.js +43 -1
  25. package/lib/module/zustand/components/ZustandEventFilterView.js +39 -1
  26. package/lib/module/zustand/components/ZustandIcon.js +11 -1
  27. package/lib/module/zustand/components/ZustandModal.js +767 -1
  28. package/lib/module/zustand/components/ZustandStateChangeItem.js +113 -1
  29. package/lib/module/zustand/components/ZustandStateDetailContent.js +368 -1
  30. package/lib/module/zustand/components/ZustandStateInfoView.js +483 -1
  31. package/lib/module/zustand/components/ZustandStoreBrowser.js +200 -1
  32. package/lib/module/zustand/components/ZustandStoreDetailContent.js +469 -1
  33. package/lib/module/zustand/components/ZustandStoreEditorScreen.js +389 -1
  34. package/lib/module/zustand/hooks/useZustandStateChanges.js +70 -1
  35. package/lib/module/zustand/sync/zustandSyncAdapter.js +380 -1
  36. package/lib/module/zustand/utils/buoyZustandMiddleware.js +278 -1
  37. package/lib/module/zustand/utils/editableState.js +138 -1
  38. package/lib/module/zustand/utils/snapshotProvider.js +209 -1
  39. package/lib/module/zustand/utils/testIDs.js +74 -1
  40. package/lib/module/zustand/utils/zustandStateStore.js +375 -1
  41. package/lib/typescript/index.d.ts.map +1 -0
  42. package/lib/typescript/preset.d.ts.map +1 -0
  43. package/lib/typescript/zustand/components/ZustandActionButton.d.ts.map +1 -0
  44. package/lib/typescript/zustand/components/ZustandDetailViewToggle.d.ts.map +1 -0
  45. package/lib/typescript/zustand/components/ZustandEventFilterView.d.ts.map +1 -0
  46. package/lib/typescript/zustand/components/ZustandIcon.d.ts.map +1 -0
  47. package/lib/typescript/zustand/components/ZustandModal.d.ts.map +1 -0
  48. package/lib/typescript/zustand/components/ZustandStateChangeItem.d.ts.map +1 -0
  49. package/lib/typescript/zustand/components/ZustandStateDetailContent.d.ts.map +1 -0
  50. package/lib/typescript/zustand/components/ZustandStateInfoView.d.ts.map +1 -0
  51. package/lib/typescript/zustand/components/ZustandStoreBrowser.d.ts.map +1 -0
  52. package/lib/typescript/zustand/components/ZustandStoreDetailContent.d.ts.map +1 -0
  53. package/lib/typescript/zustand/components/ZustandStoreEditorScreen.d.ts.map +1 -0
  54. package/lib/typescript/zustand/hooks/useZustandStateChanges.d.ts.map +1 -0
  55. package/lib/typescript/zustand/sync/zustandSyncAdapter.d.ts.map +1 -0
  56. package/lib/typescript/zustand/types/index.d.ts.map +1 -0
  57. package/lib/typescript/zustand/utils/buoyZustandMiddleware.d.ts.map +1 -0
  58. package/lib/typescript/zustand/utils/editableState.d.ts.map +1 -0
  59. package/lib/typescript/zustand/utils/snapshotProvider.d.ts.map +1 -0
  60. package/lib/typescript/zustand/utils/testIDs.d.ts.map +1 -0
  61. package/lib/typescript/zustand/utils/zustandStateStore.d.ts.map +1 -0
  62. package/package.json +3 -3
@@ -1 +1,138 @@
1
- "use strict";export const STRIP_MAX_DEPTH=8;export function isPlainObjectLike(t){if("object"!=typeof t||null===t||Array.isArray(t))return!1;const e=Object.getPrototypeOf(t);return e===Object.prototype||null===e}export function stripFunctions(t,e,n,r,i){if("function"==typeof t)return void n.push(e);if("object"!=typeof t||null===t||i>=8)return t;if(r.has(t))return;if(r.add(t),Array.isArray(t))return t.map((t,o)=>"function"==typeof t?(n.push(`${e}[${o}]`),null):stripFunctions(t,`${e}[${o}]`,n,r,i+1));if(!isPlainObjectLike(t))return t;const o={};for(const[s,a]of Object.entries(t))"function"!=typeof a?o[s]=stripFunctions(a,e?`${e}.${s}`:s,n,r,i+1):n.push(e?`${e}.${s}`:s);return o}export function editableStateOf(t){if(isPlainObjectLike(t)&&!0===t.__buoyStateOnDevice)return{data:null,fnPaths:[],blockedReason:"Store state hasn't synced from the device yet."};if(!isPlainObjectLike(t))return{data:null,fnPaths:[],blockedReason:"Only stores whose state is a plain object can be edited — a merge write has nothing to address in "+(Array.isArray(t)?"an array":"a "+typeof t)+"."};const e=[],n=stripFunctions(t,"",e,new WeakSet,0),r=e.filter(t=>t.includes(".")||t.includes("["));return r.length>0?{data:null,fnPaths:e,blockedReason:`State keeps a function inside "${r[0]}" — a merge write of that key would drop it, so this store can't be edited here.`}:{data:n,fnPaths:e,blockedReason:null}}export function computeMergePartial(t,e){if(!isPlainObjectLike(e))throw new Error("The store's state must stay an object.");const n=Object.keys(t).filter(t=>!(t in e));if(n.length>0)throw new Error(`Can't remove top-level ${1===n.length?"key":"keys"} ${n.map(t=>`"${t}"`).join(", ")} — zustand merge writes can't delete. Edit the value instead.`);const r={};for(const n of Object.keys(e))n in t&&JSON.stringify(e[n])===JSON.stringify(t[n])||(r[n]=e[n]);return r}
1
+ "use strict";
2
+
3
+ /**
4
+ * What of a zustand store's state can be EDITED, and how an edit is written.
5
+ *
6
+ * The two zustand facts every rule here follows from:
7
+ *
8
+ * 1. Stores conventionally keep their action functions IN state
9
+ * (`increment: () => …`). Functions can't be typed into a tree editor and
10
+ * don't survive serialization, so the editor works on a function-free view
11
+ * of state.
12
+ * 2. `setState` is either a TOP-LEVEL SHALLOW MERGE (`Object.assign({}, state,
13
+ * partial)`) or a full replace that deletes every key not present — actions
14
+ * included. So the editor writes merge-only, always: every edit, however
15
+ * deep, is expressed as "replace the top-level key(s) that contain it"
16
+ * ({@link computeMergePartial}), and the two edits a merge cannot express —
17
+ * removing or renaming a top-level key — are refused rather than silently
18
+ * not happening.
19
+ *
20
+ * `stripFunctions` lives here (the Time Machine snapshot provider imports it)
21
+ * because capture and editing need the identical answer to "what is this
22
+ * store's data".
23
+ */
24
+
25
+ /** Bounds every recursive walk over store state. */
26
+ export const STRIP_MAX_DEPTH = 8;
27
+ export function isPlainObjectLike(v) {
28
+ // Only objects whose own enumerable keys ARE their data. A Map, Set, Date,
29
+ // RegExp or typed array has none, so walking it with Object.entries turns
30
+ // it into `{}` — which is how a restored store once got `{}` where the app
31
+ // kept a Map and crashed on `.values()`. Those go through as-is; the vault's
32
+ // superjson knows how to carry them.
33
+ if (typeof v !== "object" || v === null || Array.isArray(v)) return false;
34
+ const proto = Object.getPrototypeOf(v);
35
+ return proto === Object.prototype || proto === null;
36
+ }
37
+
38
+ /** Clone `value` without functions, recording their paths. */
39
+ export function stripFunctions(value, path, fnPaths, seen, depth) {
40
+ if (typeof value === "function") {
41
+ fnPaths.push(path);
42
+ return undefined;
43
+ }
44
+ if (typeof value !== "object" || value === null || depth >= STRIP_MAX_DEPTH) {
45
+ return value;
46
+ }
47
+ if (seen.has(value)) return undefined; // cycle — drop the back-reference
48
+ seen.add(value);
49
+ if (Array.isArray(value)) {
50
+ return value.map((item, i) => typeof item === "function" ? (fnPaths.push(`${path}[${i}]`), null) : stripFunctions(item, `${path}[${i}]`, fnPaths, seen, depth + 1));
51
+ }
52
+ if (!isPlainObjectLike(value)) return value; // Date/Map/Set/etc. — superjson handles
53
+ const out = {};
54
+ for (const [k, v] of Object.entries(value)) {
55
+ if (typeof v === "function") {
56
+ fnPaths.push(path ? `${path}.${k}` : k);
57
+ continue;
58
+ }
59
+ out[k] = stripFunctions(v, path ? `${path}.${k}` : k, fnPaths, seen, depth + 1);
60
+ }
61
+ return out;
62
+ }
63
+ /**
64
+ * The editor's view of one store's state, with the reasons it may refuse.
65
+ *
66
+ * - A remote mirror that hasn't hydrated yet holds a marker, not state — the
67
+ * real value is one `getStoreState` round trip away, so "wait" rather than
68
+ * "can't".
69
+ * - Non-object state (`create(() => 5)`) has no top-level keys, so a merge
70
+ * write cannot address any part of it. Editing it would require a replace,
71
+ * which is the destructive path this editor exists to avoid.
72
+ * - A function nested BELOW the top level (rare — the convention keeps actions
73
+ * at the top) sits inside the very key an edit would rewrite, and a merge
74
+ * write of that key would silently drop it. Refusing the whole store is
75
+ * blunt but honest; per-key locks can come later if this ever bites.
76
+ */
77
+ export function editableStateOf(state) {
78
+ if (isPlainObjectLike(state) && state.__buoyStateOnDevice === true) {
79
+ return {
80
+ data: null,
81
+ fnPaths: [],
82
+ blockedReason: "Store state hasn't synced from the device yet."
83
+ };
84
+ }
85
+ if (!isPlainObjectLike(state)) {
86
+ return {
87
+ data: null,
88
+ fnPaths: [],
89
+ blockedReason: "Only stores whose state is a plain object can be edited — " + "a merge write has nothing to address in " + (Array.isArray(state) ? "an array" : `a ${typeof state}`) + "."
90
+ };
91
+ }
92
+ const fnPaths = [];
93
+ const data = stripFunctions(state, "", fnPaths, new WeakSet(), 0);
94
+ const nested = fnPaths.filter(p => p.includes(".") || p.includes("["));
95
+ if (nested.length > 0) {
96
+ return {
97
+ data: null,
98
+ fnPaths,
99
+ blockedReason: `State keeps a function inside "${nested[0]}" — a merge write of ` + "that key would drop it, so this store can't be edited here."
100
+ };
101
+ }
102
+ return {
103
+ data,
104
+ fnPaths,
105
+ blockedReason: null
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Diff an edited draft against the state it was seeded from, as a `setState`
111
+ * MERGE partial: only the top-level keys whose contents changed.
112
+ *
113
+ * Diffed against the SEED, not the live state at save time, on purpose — the
114
+ * partial then carries exactly what the user changed, so a key the app wrote
115
+ * while the editor was open and the user never touched keeps the app's value
116
+ * (per-key last-write-wins, the same contract the draft's own dirtiness uses).
117
+ *
118
+ * Throws on the two edits a merge cannot express, naming the key: a draft
119
+ * that dropped a top-level key (which is also what a raw-JSON rename produces
120
+ * — the old key dropped, a new one added), because `setState(partial, false)`
121
+ * would leave it exactly as it was while the editor showed it gone.
122
+ */
123
+ export function computeMergePartial(seed, draft) {
124
+ if (!isPlainObjectLike(draft)) {
125
+ throw new Error("The store's state must stay an object.");
126
+ }
127
+ const dropped = Object.keys(seed).filter(k => !(k in draft));
128
+ if (dropped.length > 0) {
129
+ throw new Error(`Can't remove top-level ${dropped.length === 1 ? "key" : "keys"} ` + `${dropped.map(k => `"${k}"`).join(", ")} — zustand merge writes ` + "can't delete. Edit the value instead.");
130
+ }
131
+ const partial = {};
132
+ for (const key of Object.keys(draft)) {
133
+ if (!(key in seed) || JSON.stringify(draft[key]) !== JSON.stringify(seed[key])) {
134
+ partial[key] = draft[key];
135
+ }
136
+ }
137
+ return partial;
138
+ }
@@ -1 +1,209 @@
1
- "use strict";import{classifyPreviewItem,createRestoreScope,emptyRestoreResult,failedRestore,registerStateSnapshotProvider,unionKeys}from"@buoy-gg/shared-ui";import{zustandStateStore}from"./zustandStateStore";import{isPlainObjectLike,stripFunctions,STRIP_MAX_DEPTH}from"./editableState";const MAX_DEPTH=STRIP_MAX_DEPTH;function graftFunctions(t,e,s,n){if(isPlainObjectLike(e)&&isPlainObjectLike(t)&&!(n>=MAX_DEPTH||s.has(e))){s.add(e);for(const[o,r]of Object.entries(e))if("function"==typeof r)t[o]=r;else if(isPlainObjectLike(r)){if(!isPlainObjectLike(t[o])){if(!containsFunction(r,new WeakSet,n+1))continue;t[o]={}}graftFunctions(t[o],r,s,n+1)}}}function containsFunction(t,e,s){if(s>=MAX_DEPTH||e.has(t))return!1;e.add(t);for(const n of Object.values(t)){if("function"==typeof n)return!0;if(isPlainObjectLike(n)&&containsFunction(n,e,s+1))return!0}return!1}function status(){return 0===zustandStateStore.getStores().length?{canCapture:!1,canRestore:!1,detail:"No Zustand stores registered — pass `zustandStores` to FloatingDevTools or add the buoyDevTools middleware"}:{canCapture:!0,canRestore:!0}}async function capture(){const t=[],e={};for(const s of zustandStateStore.getStores()){const n=[];let o;try{o=stripFunctions(s.api.getState(),"",n,new WeakSet,0)}catch(e){t.push(`store "${s.name}": capture failed (${String(e)})`);continue}e[s.name]={state:o,fnPaths:n}}return{data:{stores:e},warnings:t}}async function restore(t,e){const s=t,n=createRestoreScope(e),o=emptyRestoreResult();if(!s?.stores)return failedRestore("empty zustand snapshot");for(const[t,e]of Object.entries(s.stores)){if(!n.allows(t))continue;const s=zustandStateStore.getStore(t);if(s)try{const n=e.state;graftFunctions(n,s.api.getState(),new WeakSet,0),s.api.setState(n,!0),s.api.getState()===n?o.applied+=1:(o.warnings.push(`store "${t}": setState did not take effect`),o.ok=!1)}catch(e){o.skipped.push({id:t,reason:String(e)}),o.ok=!1}else o.skipped.push({id:t,reason:"store not registered (renamed, or created after snapshot?)"})}const r=n.excludedWarning("store(s)");return r&&o.warnings.push(r),o}async function previewItems(t,e){const s=t?.stores??{},n=void 0===e,o={};if(n)for(const t of zustandStateStore.getStores()){const e=[];try{o[t.name]={state:stripFunctions(t.api.getState(),"",e,new WeakSet,0),fnPaths:e}}catch{}}else Object.assign(o,e?.stores??{});const r=[],a=[];let i=0;for(const t of unionKeys(o,s)){const e=t in o;t in s?(i+=s[t].fnPaths?.length??0,!n||zustandStateStore.getStore(t)?r.push(classifyPreviewItem({key:t,label:t,oldValue:o[t]?.state,newValue:s[t].state,inOld:e,inNew:!0},{tooLargeReason:"store too large to render a diff — restore still applies it"})):r.push({key:t,label:t,verdict:"wont-apply",reason:"store not registered (renamed, or created after snapshot?)"})):n?a.push(`store "${t}" not in snapshot — untouched`):r.push({key:t,label:t,verdict:"removed",oldValue:o[t].state})}return i>0&&a.push(`${i} store action function${1===i?"":"s"} preserved on restore (not diffed)`),{items:r,notes:a}}registerStateSnapshotProvider({id:"zustand",label:"Zustand",status:status,capture:capture,restore:restore,previewItems:previewItems});
1
+ "use strict";
2
+
3
+ /**
4
+ * Time Machine snapshot provider for Zustand.
5
+ *
6
+ * Registered into @buoy-gg/shared-ui's stateSnapshotRegistry at module init;
7
+ * consumed by @buoy-gg/time-machine. Neither package imports the other.
8
+ *
9
+ * The Zustand-specific hazard: stores conventionally keep their action
10
+ * functions IN state (`increment: () => ...`). Functions don't survive
11
+ * serialization, and a full `setState(snapshot, true)` replace would wipe
12
+ * them, breaking every `useStore(s => s.increment)` call site. So capture
13
+ * strips functions (recording where they were), and restore re-grafts the
14
+ * LIVE store's functions onto the snapshot before replacing.
15
+ */
16
+ import { classifyPreviewItem, createRestoreScope, emptyRestoreResult, failedRestore, registerStateSnapshotProvider, unionKeys } from "@buoy-gg/shared-ui";
17
+ import { zustandStateStore } from "./zustandStateStore";
18
+ import { isPlainObjectLike, stripFunctions, STRIP_MAX_DEPTH } from "./editableState";
19
+
20
+ // The function-stripping walk is shared with the store editor (editableState
21
+ // .ts) — capture and editing must agree on what a store's "data" is.
22
+ const MAX_DEPTH = STRIP_MAX_DEPTH;
23
+ /**
24
+ * Copy every function-valued property from the LIVE state onto the restored
25
+ * snapshot (creating containers as needed), so store actions keep working
26
+ * after a full replace.
27
+ */
28
+ function graftFunctions(target, live, seen, depth) {
29
+ if (!isPlainObjectLike(live) || !isPlainObjectLike(target)) return;
30
+ if (depth >= MAX_DEPTH || seen.has(live)) return;
31
+ seen.add(live);
32
+ for (const [k, v] of Object.entries(live)) {
33
+ if (typeof v === "function") {
34
+ target[k] = v;
35
+ } else if (isPlainObjectLike(v)) {
36
+ if (!isPlainObjectLike(target[k])) {
37
+ // Branch missing from the snapshot — only recreate it if it holds
38
+ // functions somewhere; otherwise the snapshot's absence wins.
39
+ if (!containsFunction(v, new WeakSet(), depth + 1)) continue;
40
+ target[k] = {};
41
+ }
42
+ graftFunctions(target[k], v, seen, depth + 1);
43
+ }
44
+ }
45
+ }
46
+ function containsFunction(value, seen, depth) {
47
+ if (depth >= MAX_DEPTH || seen.has(value)) return false;
48
+ seen.add(value);
49
+ for (const v of Object.values(value)) {
50
+ if (typeof v === "function") return true;
51
+ if (isPlainObjectLike(v) && containsFunction(v, seen, depth + 1)) return true;
52
+ }
53
+ return false;
54
+ }
55
+ function status() {
56
+ const count = zustandStateStore.getStores().length;
57
+ if (count === 0) {
58
+ return {
59
+ canCapture: false,
60
+ canRestore: false,
61
+ detail: "No Zustand stores registered — pass `zustandStores` to FloatingDevTools or add the buoyDevTools middleware"
62
+ };
63
+ }
64
+ return {
65
+ canCapture: true,
66
+ canRestore: true
67
+ };
68
+ }
69
+ async function capture() {
70
+ const warnings = [];
71
+ const stores = {};
72
+ for (const info of zustandStateStore.getStores()) {
73
+ const fnPaths = [];
74
+ let state;
75
+ try {
76
+ state = stripFunctions(info.api.getState(), "", fnPaths, new WeakSet(), 0);
77
+ } catch (e) {
78
+ warnings.push(`store "${info.name}": capture failed (${String(e)})`);
79
+ continue;
80
+ }
81
+ stores[info.name] = {
82
+ state,
83
+ fnPaths
84
+ };
85
+ }
86
+ return {
87
+ data: {
88
+ stores
89
+ },
90
+ warnings
91
+ };
92
+ }
93
+ async function restore(data, options) {
94
+ const snapshot = data;
95
+ const scope = createRestoreScope(options);
96
+ const result = emptyRestoreResult();
97
+ if (!snapshot?.stores) {
98
+ return failedRestore("empty zustand snapshot");
99
+ }
100
+ for (const [name, envelope] of Object.entries(snapshot.stores)) {
101
+ if (!scope.allows(name)) continue;
102
+ const info = zustandStateStore.getStore(name);
103
+ if (!info) {
104
+ result.skipped.push({
105
+ id: name,
106
+ reason: "store not registered (renamed, or created after snapshot?)"
107
+ });
108
+ continue;
109
+ }
110
+ try {
111
+ const next = envelope.state;
112
+ graftFunctions(next, info.api.getState(), new WeakSet(), 0);
113
+ info.api.setState(next, true);
114
+ if (info.api.getState() === next) {
115
+ result.applied += 1;
116
+ } else {
117
+ result.warnings.push(`store "${name}": setState did not take effect`);
118
+ result.ok = false;
119
+ }
120
+ } catch (e) {
121
+ result.skipped.push({
122
+ id: name,
123
+ reason: String(e)
124
+ });
125
+ result.ok = false;
126
+ }
127
+ }
128
+ const excludedWarning = scope.excludedWarning("store(s)");
129
+ if (excludedWarning) result.warnings.push(excludedWarning);
130
+ return result;
131
+ }
132
+
133
+ /**
134
+ * Per-store restore preview. Both sides are function-stripped through the
135
+ * SAME depth-8 normalizer as capture — diffing snapshot-vs-RAW-live would
136
+ * report every stripped function and every beyond-depth subtree as removed.
137
+ */
138
+ async function previewItems(target, base) {
139
+ const targetStores = target?.stores ?? {};
140
+ const liveMode = base === undefined;
141
+ const baseStores = {};
142
+ if (liveMode) {
143
+ for (const info of zustandStateStore.getStores()) {
144
+ const fnPaths = [];
145
+ try {
146
+ baseStores[info.name] = {
147
+ state: stripFunctions(info.api.getState(), "", fnPaths, new WeakSet(), 0),
148
+ fnPaths
149
+ };
150
+ } catch {
151
+ // unreadable live store — treated as absent
152
+ }
153
+ }
154
+ } else {
155
+ Object.assign(baseStores, base?.stores ?? {});
156
+ }
157
+ const items = [];
158
+ const notes = [];
159
+ let preservedFns = 0;
160
+ for (const name of unionKeys(baseStores, targetStores)) {
161
+ const inTarget = name in targetStores;
162
+ const inBase = name in baseStores;
163
+ if (!inTarget) {
164
+ // Restore never touches stores the snapshot doesn't know about.
165
+ if (liveMode) notes.push(`store "${name}" not in snapshot — untouched`);else items.push({
166
+ key: name,
167
+ label: name,
168
+ verdict: "removed",
169
+ oldValue: baseStores[name].state
170
+ });
171
+ continue;
172
+ }
173
+ preservedFns += targetStores[name].fnPaths?.length ?? 0;
174
+ if (liveMode && !zustandStateStore.getStore(name)) {
175
+ items.push({
176
+ key: name,
177
+ label: name,
178
+ verdict: "wont-apply",
179
+ reason: "store not registered (renamed, or created after snapshot?)"
180
+ });
181
+ continue;
182
+ }
183
+ items.push(classifyPreviewItem({
184
+ key: name,
185
+ label: name,
186
+ oldValue: baseStores[name]?.state,
187
+ newValue: targetStores[name].state,
188
+ inOld: inBase,
189
+ inNew: true
190
+ }, {
191
+ tooLargeReason: "store too large to render a diff — restore still applies it"
192
+ }));
193
+ }
194
+ if (preservedFns > 0) {
195
+ notes.push(`${preservedFns} store action function${preservedFns === 1 ? "" : "s"} preserved on restore (not diffed)`);
196
+ }
197
+ return {
198
+ items,
199
+ notes
200
+ };
201
+ }
202
+ registerStateSnapshotProvider({
203
+ id: "zustand",
204
+ label: "Zustand",
205
+ status,
206
+ capture,
207
+ restore,
208
+ previewItems
209
+ });
@@ -1 +1,74 @@
1
- "use strict";export const ZUSTAND_TEST_ID="buoy-zustand";export function slugForTestID(e){return String(e??"").toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"unknown"}export function changeRowTestID(e,t){return`buoy-zustand-row-${slugForTestID(e)}-${t}`}export function storeRowTestID(e){return`buoy-zustand-store-${slugForTestID(e)}`}export function buildChangeRowTestIDs(e){const t=new Map,o=new Map;for(const n of e){const e=(t.get(n.storeName)??0)+1;t.set(n.storeName,e),o.set(n.id,changeRowTestID(n.storeName,e))}return o}
1
+ "use strict";
2
+
3
+ /**
4
+ * Zustand tool test handles.
5
+ *
6
+ * Every tappable and every value the e2e suite reads back is addressed
7
+ * `buoy-zustand-<thing>`. Two rules in here need explaining.
8
+ *
9
+ * **A change row's handle carries an ORDINAL, and it counts per STORE.** The
10
+ * Events tool can address a row by what it is about, because an event is a
11
+ * thing that happened to a named subject — one key, one URL, one atom. A
12
+ * zustand state change is not: a store that a screen writes to fifty times
13
+ * produces fifty separate, equally real rows. Addressing them by store name
14
+ * alone would give fifty views one handle, and Detox fails a *matcher* that
15
+ * hits more than one element — so a suite would break on `toExist()`, before
16
+ * any assertion, with an error that says nothing about duplicate changes.
17
+ *
18
+ * So the handle is `buoy-zustand-row-<slug(storeName)>-<n>`, where n counts
19
+ * back from the newest row of that store (1 = newest). Deliberately relative:
20
+ * a state change has no identity a host could know in advance — `id` is a
21
+ * timestamp plus a per-process counter whose value depends on how many
22
+ * changes the app happened to make before the test looked. What a test CAN
23
+ * know is "the set I just fired is the newest change to that store". Read and
24
+ * tap in one beat: a row's ordinal moves the instant another change lands.
25
+ *
26
+ * **A store row's handle does NOT.** A store is registered once under a name
27
+ * that is unique by construction (`zustandStateStore.registerStore` returns
28
+ * early on a name it already holds), so `buoy-zustand-store-<slug(name)>` is
29
+ * as stable as the store itself — and unlike a change, it is a thing the host
30
+ * knows the name of before the app has booted.
31
+ *
32
+ * The suite mirrors these rules in CommonJS (`example-dev-build/utils/
33
+ * zustandBench.js`) and asserts the two agree — Detox runs in Node on the host
34
+ * and cannot import TypeScript.
35
+ */
36
+
37
+ /** Prefix every handle in this tool shares. */
38
+ export const ZUSTAND_TEST_ID = "buoy-zustand";
39
+
40
+ /** `zbenchWatch` → `zbenchwatch`. Shared with every Buoy tool. */
41
+ export function slugForTestID(value) {
42
+ return String(value ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "unknown";
43
+ }
44
+
45
+ /**
46
+ * Handle for one change row: `buoy-zustand-row-zbenchwatch-1`.
47
+ *
48
+ * `ordinal` is 1-based and counts back from the newest row of this store.
49
+ */
50
+ export function changeRowTestID(storeName, ordinal) {
51
+ return `${ZUSTAND_TEST_ID}-row-${slugForTestID(storeName)}-${ordinal}`;
52
+ }
53
+
54
+ /** Handle for one store row in the browser: `buoy-zustand-store-zbenchwatch`. */
55
+ export function storeRowTestID(storeName) {
56
+ return `${ZUSTAND_TEST_ID}-store-${slugForTestID(storeName)}`;
57
+ }
58
+
59
+ /**
60
+ * Row handle per change id for one rendered list, newest-first.
61
+ *
62
+ * Built once for the whole list rather than per row, because an ordinal is a
63
+ * fact about the list and a row cannot compute its own.
64
+ */
65
+ export function buildChangeRowTestIDs(changes) {
66
+ const seen = new Map();
67
+ const handles = new Map();
68
+ for (const change of changes) {
69
+ const ordinal = (seen.get(change.storeName) ?? 0) + 1;
70
+ seen.set(change.storeName, ordinal);
71
+ handles.set(change.id, changeRowTestID(change.storeName, ordinal));
72
+ }
73
+ return handles;
74
+ }