@decocms/start 2.12.0 → 2.14.0

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 (27) hide show
  1. package/.agents/skills/deco-to-tanstack-migration/references/post-migration-cleanup.md +32 -6
  2. package/CLAUDE.md +1 -1
  3. package/package.json +1 -1
  4. package/scripts/migrate/phase-cleanup-audit.test.ts +137 -0
  5. package/scripts/migrate/phase-cleanup-audit.ts +105 -0
  6. package/scripts/migrate/post-cleanup/rules.ts +77 -6
  7. package/scripts/migrate/post-cleanup/runner.test.ts +123 -2
  8. package/scripts/migrate/post-cleanup/shim-classify.test.ts +352 -0
  9. package/scripts/migrate/post-cleanup/shim-classify.ts +246 -0
  10. package/scripts/migrate.ts +36 -8
  11. package/.cursor/skills/deco-to-tanstack-migration/SKILL.md +0 -655
  12. package/.cursor/skills/deco-to-tanstack-migration/references/codemod-commands.md +0 -174
  13. package/.cursor/skills/deco-to-tanstack-migration/references/commerce/README.md +0 -78
  14. package/.cursor/skills/deco-to-tanstack-migration/references/deco-framework/README.md +0 -174
  15. package/.cursor/skills/deco-to-tanstack-migration/references/gotchas.md +0 -834
  16. package/.cursor/skills/deco-to-tanstack-migration/references/imports/README.md +0 -70
  17. package/.cursor/skills/deco-to-tanstack-migration/references/platform-hooks/README.md +0 -121
  18. package/.cursor/skills/deco-to-tanstack-migration/references/post-migration-cleanup.md +0 -231
  19. package/.cursor/skills/deco-to-tanstack-migration/references/signals/README.md +0 -220
  20. package/.cursor/skills/deco-to-tanstack-migration/references/vite-config/README.md +0 -103
  21. package/.cursor/skills/deco-to-tanstack-migration/templates/package-json.md +0 -75
  22. package/.cursor/skills/deco-to-tanstack-migration/templates/root-route.md +0 -127
  23. package/.cursor/skills/deco-to-tanstack-migration/templates/router.md +0 -96
  24. package/.cursor/skills/deco-to-tanstack-migration/templates/setup-ts.md +0 -148
  25. package/.cursor/skills/deco-to-tanstack-migration/templates/vite-config.md +0 -197
  26. package/.cursor/skills/deco-to-tanstack-migration/templates/worker-entry.md +0 -67
  27. /package/{.cursor → .agents}/skills/deco-to-tanstack-migration/references/server-functions/README.md +0 -0
@@ -1,70 +0,0 @@
1
- # Preact -> React Import Migration
2
-
3
- Mechanical find-and-replace. Safe to automate with `sed`.
4
-
5
- ## Replacements
6
-
7
- | Find | Replace |
8
- |------|---------|
9
- | `from "preact/hooks"` | `from "react"` |
10
- | `from "preact/compat"` | `from "react"` |
11
- | `from "preact"` | `from "react"` |
12
-
13
- ## Special Cases
14
-
15
- ### ComponentChildren -> ReactNode
16
-
17
- Preact's `ComponentChildren` maps to React's `ReactNode`:
18
-
19
- ```typescript
20
- // OLD
21
- import type { ComponentChildren } from "preact";
22
-
23
- // NEW
24
- import type { ReactNode as ComponentChildren } from "react";
25
- ```
26
-
27
- If you want to modernize fully, rename `ComponentChildren` to `ReactNode` across the codebase.
28
-
29
- ### JSX type
30
-
31
- ```typescript
32
- // OLD
33
- import type { JSX } from "preact";
34
-
35
- // NEW (works unchanged)
36
- import type { JSX } from "react";
37
- ```
38
-
39
- ### FunctionalComponent -> FC
40
-
41
- ```typescript
42
- // OLD
43
- const Foo: preact.FunctionalComponent<Props> = ...
44
-
45
- // NEW
46
- import React from "react";
47
- const Foo: React.FC<Props> = ...
48
- ```
49
-
50
- ## Automation
51
-
52
- ```bash
53
- # Bulk replace (macOS sed)
54
- find src/ -name '*.ts' -o -name '*.tsx' | xargs sed -i '' \
55
- -e 's|from "preact/hooks"|from "react"|g' \
56
- -e 's|from "preact/compat"|from "react"|g'
57
-
58
- # Bare preact requires care (don't match preact/hooks or preact/compat)
59
- find src/ -name '*.ts' -o -name '*.tsx' | xargs sed -i '' \
60
- 's|from "preact"|from "react"|g'
61
- ```
62
-
63
- Then handle `ComponentChildren` files individually.
64
-
65
- ## Verification
66
-
67
- ```bash
68
- grep -r 'from "preact' src/ --include='*.ts' --include='*.tsx'
69
- # Should return ZERO matches
70
- ```
@@ -1,121 +0,0 @@
1
- # Platform Hooks Migration
2
-
3
- Platform hooks (useCart, useUser, useWishlist) are the most complex migration target because they have real business logic.
4
-
5
- ## Strategy
6
-
7
- All hooks are **site-local**. No Vite alias tricks. No compat layers.
8
-
9
- - Active platform hooks (VTEX for this store) -> `~/hooks/useCart.ts` with real implementation
10
- - Inactive platform hooks (Wake, Shopify, etc.) -> `~/hooks/platform/{name}.ts` with no-op stubs
11
- - Auth hooks -> `~/hooks/useUser.ts`, `~/hooks/useWishlist.ts`
12
-
13
- ## VTEX useCart (Real Implementation)
14
-
15
- ### Why Server Functions Are Required
16
-
17
- The storefront domain (e.g., `my-store.deco.site`) differs from the VTEX checkout domain (`account.vtexcommercestable.com.br`). Direct browser `fetch()` calls are blocked by CORS. Additionally, VTEX API credentials (`AppKey`/`AppToken`) must stay server-side.
18
-
19
- Use TanStack Start `createServerFn` to create server-side proxy functions that the client hook calls transparently.
20
-
21
- > See `references/server-functions/README.md` for the full pattern and all TypeScript pitfalls.
22
-
23
- ### Server Functions (`src/server/invoke.ts`)
24
-
25
- Wrap `@decocms/apps` VTEX actions in `createServerFn`. Always use `.inputValidator()` (not `.validator()`) and return `Promise<any>` to bypass `ValidateSerializable`:
26
-
27
- ```typescript
28
- import { createServerFn } from "@tanstack/react-start";
29
- import { getOrCreateCart } from "@decocms/apps/vtex/actions/checkout";
30
-
31
- // Preferred: wrap @decocms/apps actions (handles auth + API details internally)
32
- const _getOrCreateCart = createServerFn({ method: "POST" })
33
- .inputValidator((data: { orderFormId?: string }) => data)
34
- .handler(async ({ data }): Promise<any> => {
35
- const result = await getOrCreateCart(data.orderFormId);
36
- return (result as any).data;
37
- });
38
-
39
- export const invoke = {
40
- vtex: { actions: { getOrCreateCart: _getOrCreateCart } },
41
- } as const;
42
- ```
43
-
44
- ### Hook (~/hooks/useCart.ts)
45
-
46
- Key design decisions:
47
- - **Module-level singleton state** shared across all component instances
48
- - **Pub/sub pattern** (`_listeners` Set) for notifying React components of changes
49
- - **Cookie-based session**: reads/writes `checkout.vtex.com__orderFormId` on the **client** side (not VTEX's domain cookie)
50
- - Returns `cart` and `loading` with `.value` getter/setter for backward compat with Preact-era components
51
- - Lazy initialization: cart is fetched on first component mount, not on module load
52
- - Exports `itemToAnalyticsItem` for cart-specific analytics mapping
53
-
54
- ### Cross-Domain Checkout
55
-
56
- The minicart's "Finalizar Compra" button must link to the VTEX checkout domain with the `orderFormId` as a query parameter — the VTEX domain can't read the storefront's cookies:
57
-
58
- ```typescript
59
- const checkoutUrl = `https://secure.${STORE_DOMAIN}/checkout/?orderFormId=${orderFormId}`;
60
- ```
61
-
62
- ### VTEX Types (~/types/vtex.ts)
63
-
64
- Site-local types for VTEX-specific structures:
65
- - `OrderFormItem`, `SimulationOrderForm`, `Sla`, `SKU`, `VtexProduct`
66
-
67
- ## Inactive Platform Stubs
68
-
69
- For non-VTEX platforms, create minimal no-op files:
70
-
71
- ```typescript
72
- // ~/hooks/platform/wake.ts
73
- export function useCart() {
74
- return {
75
- cart: { value: null },
76
- loading: { value: false },
77
- addItem: async (_params: any) => {},
78
- updateItems: async (_params: any) => {},
79
- removeItem: async (_index: any) => {},
80
- };
81
- }
82
-
83
- export function useUser() {
84
- return {
85
- user: { value: null as { email?: string; name?: string } | null },
86
- loading: { value: false },
87
- };
88
- }
89
-
90
- export function useWishlist() {
91
- return {
92
- loading: { value: false },
93
- addItem: async (_props: any) => {},
94
- removeItem: async (_props: any) => {},
95
- getItem: (_props: any) => undefined as any,
96
- };
97
- }
98
- ```
99
-
100
- Create similar stubs for: `shopify.ts`, `linx.ts`, `vnda.ts`, `nuvemshop.ts`.
101
-
102
- Match the return shape to what each platform's AddToCartButton expects (some use `addItem`, others `addItems`).
103
-
104
- ## Import Rewrites
105
-
106
- ```bash
107
- sed -i '' 's|from "apps/vtex/hooks/useCart.ts"|from "~/hooks/useCart"|g'
108
- sed -i '' 's|from "apps/vtex/hooks/useUser.ts"|from "~/hooks/useUser"|g'
109
- sed -i '' 's|from "apps/vtex/hooks/useWishlist.ts"|from "~/hooks/useWishlist"|g'
110
- sed -i '' 's|from "apps/vtex/utils/types.ts"|from "~/types/vtex"|g'
111
- sed -i '' 's|from "apps/shopify/hooks/useCart.ts"|from "~/hooks/platform/shopify"|g'
112
- sed -i '' 's|from "apps/wake/hooks/useCart.ts"|from "~/hooks/platform/wake"|g'
113
- # etc. for all platforms
114
- ```
115
-
116
- ## Verification
117
-
118
- ```bash
119
- grep -r 'from "apps/' src/ --include='*.ts' --include='*.tsx'
120
- # Should return ZERO matches
121
- ```
@@ -1,231 +0,0 @@
1
- # Post-Migration Cleanup
2
-
3
- After the migration script runs and the site builds + boots, there's a
4
- recurring set of dead-code and boilerplate cleanup that every migrated
5
- site benefits from. Run this checklist before the first PR review, not
6
- after the site has been shipping for weeks.
7
-
8
- ## Run the audit first
9
-
10
- This whole checklist is now automated by the **`deco-post-cleanup`**
11
- audit script (added in `@decocms/start >= 2.11.0`, `--fix` mode in
12
- `>= 2.12.0`). Run it from the site repo to get a structured report
13
- of which sections below actually apply to your codebase:
14
-
15
- ```bash
16
- # Pretty text output, exits 0 unless --strict is passed
17
- npx -p @decocms/start deco-post-cleanup
18
-
19
- # Auto-apply mechanical fixes for the safe rules, then report what's left.
20
- # Safe rules: dead-lib-shims, dead-runtime-shim, local-widgets-types.
21
- # Other rules stay detect-only — they require human judgment.
22
- npx -p @decocms/start deco-post-cleanup --fix
23
-
24
- # Combine for CI: auto-fix safe rules, fail (exit 2) if warnings remain.
25
- npx -p @decocms/start deco-post-cleanup --fix --strict
26
-
27
- # Machine-readable JSON for dashboards
28
- npx -p @decocms/start deco-post-cleanup --json
29
- ```
30
-
31
- The audit covers all 7 rules below and prints the exact file path +
32
- suggested fix for each finding. With `--fix`, the three safe rules
33
- auto-apply (`rm` for dead files, regex-anchored import rewrites for
34
- shadowed shims). The output explicitly tags rules that require manual
35
- work as `(0 fixed, manual)`, so you always know what's left after
36
- auto-fix runs.
37
-
38
- Real-world signal: on baggagio, `--fix` produced a byte-identical
39
- diff to the manual cleanup PR a human had just made (45 files,
40
- +45/-53). On casaevideo-storefront (production), the audit caught
41
- six silent VTEX shim regressions that no `tsc --noEmit` run can
42
- detect — those still require manual cleanup until rule 5 gains a
43
- per-shim mapping table.
44
-
45
- ## 1. Delete unused `src/lib/*` shims
46
-
47
- The migration script's `templates/lib-utils.ts` generates 11 shim files
48
- under `src/lib/`. Most of them are NO-OP stubs intended as defensive
49
- bridges for signature mismatches. In practice many sites use zero of them
50
- because their loaders import directly from `@decocms/apps/vtex/utils/*`.
51
-
52
- ### How to detect
53
-
54
- ```bash
55
- # From repo root.
56
- for f in src/lib/*.ts; do
57
- base=$(basename "$f" .ts)
58
- symbols=$(grep -oE "^export (function|const|interface|type|class) [A-Za-z_][A-Za-z0-9_]*" "$f" | awk '{print $NF}')
59
- for s in $symbols; do
60
- # Search for the symbol anywhere in src/ outside src/lib/
61
- hits=$(rg -l "\\b$s\\b" src/ --type ts --type tsx -g '!src/lib/**')
62
- if [ -z "$hits" ]; then
63
- echo "DEAD: $f exports $s with zero external imports"
64
- fi
65
- done
66
- done
67
- ```
68
-
69
- If every export in a file is dead, delete the file. If `src/lib/` ends
70
- up empty, delete the directory too.
71
-
72
- ### Real-world data
73
-
74
- | Site | Files generated | Files used |
75
- |------|-----------------|-----------|
76
- | baggagio-tanstack | 11 | 0 (all dead) |
77
- | casaevideo-storefront | 11 | 1 (wrapped manually) |
78
-
79
- The files that tend to be dead in every site:
80
-
81
- - `vtex-client.ts` — type-only export, sites usually grab it from `@decocms/apps`
82
- - `vtex-fetch.ts` — `fetchSafe` wrapper, supplanted by `@decocms/apps/vtex/utils/fetch`
83
- - `vtex-id.ts` — manual `parseCookie`, usually shadowed by `~/sdk/orderForm`'s real one
84
- - `vtex-segment.ts` — NO-OP stubs returning empty; never useful
85
- - `vtex-intelligent-search.ts` — stubs returning `{}`; supplanted by apps
86
- - `vtex-transform.ts` — re-exports from `@decocms/apps/vtex/utils/transform` directly
87
- - `vtex-send-event.ts` — claims to mirror an unreleased apps export; almost never imported
88
-
89
- The files that occasionally stay used:
90
-
91
- - `http-utils.ts` — `createHttpClient` proxy bridge for sites with custom
92
- HTTP clients
93
- - `graphql-utils.ts` — same shape for GraphQL
94
- - `fetch-utils.ts` — single `STALE` cache header constant (very small)
95
- - `filter-navigate.ts` — VTEX filter URL string transformer
96
-
97
- If `apps/utils/*` imports never appear in your Fresh source, ALL FOUR of
98
- the latter are also dead.
99
-
100
- ## 2. Drop inline vite plugins that are now framework-provided
101
-
102
- Two plugins that older site templates inline are obsolete on
103
- `@decocms/start >= 2.5.0`:
104
-
105
- ```ts
106
- // site-manual-chunks — overrides framework default chunking
107
- { name: "site-manual-chunks", config(_cfg, { command }) { ... ~25 lines ... } }
108
-
109
- // deco-stub-meta-gen — stubs admin schema on client
110
- { name: "deco-stub-meta-gen", enforce: "pre", resolveId(...), load(...) }
111
- ```
112
-
113
- The framework's `decoVitePlugin()` now handles both:
114
- - `manualChunks` no longer splits `@decocms/start` / `@decocms/apps` (the
115
- old split caused circular-dep load-order crashes — every site overrode it)
116
- - `meta.gen.{json,ts}` is stubbed on the client by default
117
-
118
- Delete both inline plugins from the site's `vite.config.ts`. Verify the
119
- production build still succeeds (`vite build` in the site repo).
120
-
121
- ## 3. Drop the `runtime.ts` `invoke` shim
122
-
123
- Older migrations create `src/runtime.ts` with a manual `createNestedInvokeProxy`
124
- implementation (~45 lines). The framework's `@decocms/start/sdk` now exports
125
- both `invoke` (default singleton) and `createAppInvoke` (for typed app-scoped
126
- proxies). Replace the file with a re-export, or delete it and update import
127
- sites:
128
-
129
- ```diff
130
- - import { invoke } from "~/runtime";
131
- + import { invoke } from "@decocms/start/sdk";
132
- ```
133
-
134
- If `~/runtime` was only used for `invoke`, delete the file entirely. If
135
- it had additional helpers, keep it but trim it down to those.
136
-
137
- ## 4. Drop site-local `withSiteGlobals` workaround
138
-
139
- If your site's `cmsRouteConfig` has a `cmsRouteWithGlobals` wrapper that
140
- manually merges `site.global` sections into the page sections list,
141
- delete it and use `@decocms/start/routes`'s opt-in helper:
142
-
143
- ```ts
144
- import { cmsRouteConfig, withSiteGlobals } from "@decocms/start/routes";
145
-
146
- export const Route = createFileRoute(...)({
147
- ...cmsRouteConfig({
148
- ...withSiteGlobals,
149
- // your route options
150
- }),
151
- });
152
- ```
153
-
154
- The site-side wrapper is typically ~390 LOC; the framework helper is
155
- opt-in and tested.
156
-
157
- ## 5. Verify `vtex-* shim regression` is not still happening
158
-
159
- Older versions of the migration script's `phase-cleanup` had a bug where
160
- it actively rewrote valid `@decocms/apps/vtex/utils/*` and
161
- `@decocms/apps/vtex/client` imports back to the dead `~/lib/vtex-*` shims.
162
- Confirm your loaders import direct from `@decocms/apps`:
163
-
164
- ```bash
165
- rg "from ['\"]~/lib/vtex-" src/
166
- # Expected: 0 hits (or only site-specific reasons you can articulate)
167
- ```
168
-
169
- If you see hits, update the imports to point at `@decocms/apps/vtex/...`
170
- directly (or the corresponding `commerce/utils/*` if it's a generic
171
- utility). Your runtime behavior gets MUCH better — segment cookies, IS
172
- cookies, VTEX session auth all start working again instead of being
173
- silently stubbed to `{}` / `null`.
174
-
175
- ## 6. Drop `src/types/widgets.ts` — framework owns it
176
-
177
- Older migrations scaffold a local `src/types/widgets.ts` containing 8
178
- string-aliased widget types (`ImageWidget`, `HTMLWidget`, …). The
179
- framework now exports the same set (plus `TextArea`) at
180
- `@decocms/start/types/widgets`, and the schema generator detects the
181
- widgets via type-text matching, so the local file is purely
182
- duplicated boilerplate.
183
-
184
- ```bash
185
- # Quick check
186
- rg -n "from ['\"]~/types/widgets['\"]" src/ | wc -l # >0 → cleanup applies
187
- ```
188
-
189
- Replace all imports in one pass:
190
-
191
- ```bash
192
- # macOS / BSD sed: drop the empty quotes after -i
193
- rg -l "from ['\"]~/types/widgets['\"]" src/ \
194
- | xargs sed -i '' "s|from ['\"]~/types/widgets['\"]|from \"@decocms/start/types/widgets\"|g"
195
- ```
196
-
197
- Then delete the now-orphan local file:
198
-
199
- ```bash
200
- rm src/types/widgets.ts
201
- ```
202
-
203
- Confirm `tsc --noEmit` is still clean — the framework version is a
204
- strict superset of what the migration script generated.
205
-
206
- ## 7. Search for orphan `TODO: move into framework` comments
207
-
208
- Real sites accumulate `TODO` comments like `// TODO: move into decoVitePlugin
209
- in next @decocms/start release`. These are roadmap items the framework
210
- team should pick up, but they often go stale.
211
-
212
- ```bash
213
- rg -n "TODO.*deco|TODO.*framework|TODO.*move into" src/ vite.config.ts
214
- ```
215
-
216
- For each hit, decide:
217
- - Has the framework feature shipped? → migrate to it now and delete the comment
218
- - Is it deferred indefinitely? → file a tracking issue and link from the comment
219
- - Is it obsolete? → delete the comment
220
-
221
- ## Verification checklist
222
-
223
- After completing 1-7:
224
-
225
- - [ ] `npm run typecheck` baseline matches pre-cleanup count (no new errors)
226
- - [ ] `npm run dev` starts and `/`, `/some-pdp/p`, `/s?q=foo` all render
227
- - [ ] `npm run build` succeeds with no chunk-load crashes
228
- - [ ] Smoke a logged-in PDP to confirm session cookies and segment auth
229
- work (this is what the `~/lib/vtex-*` regression silently broke)
230
- - [ ] `git diff --stat` shows only deletions or framework-helper substitutions
231
- — no new site-local logic added
@@ -1,220 +0,0 @@
1
- # @preact/signals -> TanStack Store Migration
2
-
3
- Two distinct patterns need different handling.
4
-
5
- ## Pattern A: Component Hooks (useSignal, useComputed)
6
-
7
- These are component-local state. Replace with React hooks directly.
8
-
9
- ### useSignal -> useState
10
-
11
- ```typescript
12
- // OLD
13
- import { useSignal } from "@preact/signals";
14
- const loading = useSignal(false);
15
- loading.value = true; // write
16
- if (loading.value) { ... } // read
17
-
18
- // NEW
19
- import { useState } from "react";
20
- const [loading, setLoading] = useState(false);
21
- setLoading(true); // write
22
- if (loading) { ... } // read
23
- ```
24
-
25
- Setter naming convention: `set` + capitalized variable name.
26
-
27
- ### useComputed -> useMemo
28
-
29
- ```typescript
30
- // OLD
31
- import { useComputed } from "@preact/signals";
32
- const isValid = useComputed(() => name.value.length > 0);
33
- return <div>{isValid.value}</div>;
34
-
35
- // NEW
36
- import { useMemo } from "react";
37
- const isValid = useMemo(() => name.length > 0, [name]);
38
- return <div>{isValid}</div>;
39
- ```
40
-
41
- ### Automation Tips
42
-
43
- - `useSignal`/`useComputed` changes are NOT safe for bulk sed (variable names, setter names, `.value` removal all differ per file)
44
- - Process each file individually: read, identify variable names, transform
45
- - Watch for: toggle patterns (`x.value = !x.value` -> `setX(prev => !prev)`), object state, conditional assignments
46
-
47
- ## Pattern B: Module-Level Signals (Global State)
48
-
49
- These create shared state across components. Use `@tanstack/store`.
50
-
51
- ### Create ~/sdk/signal.ts
52
-
53
- ```typescript
54
- import { Store } from "@tanstack/store";
55
- import { useSyncExternalStore, useMemo } from "react";
56
-
57
- export interface Signal<T> {
58
- readonly store: Store<T>;
59
- value: T;
60
- peek(): T;
61
- subscribe(fn: () => void): () => void;
62
- }
63
-
64
- export function signal<T>(initialValue: T): Signal<T> {
65
- const store = new Store<T>(initialValue);
66
- return {
67
- store,
68
- get value() { return store.state; },
69
- set value(v: T) { store.setState(() => v); },
70
- peek() { return store.state; },
71
- subscribe(fn) {
72
- // CRITICAL: @tanstack/store@0.9.x returns { unsubscribe: Function },
73
- // NOT a plain function. React's useSyncExternalStore and useEffect
74
- // cleanup both expect a bare function. Passing the object causes
75
- // "TypeError: destroy_ is not a function" at runtime.
76
- const sub = store.subscribe(() => fn());
77
- return typeof sub === "function" ? sub : sub.unsubscribe;
78
- },
79
- };
80
- }
81
-
82
- export function useSignal<T>(initialValue: T): Signal<T> {
83
- const sig = useMemo(() => signal(initialValue), []);
84
- useSyncExternalStore(
85
- (cb) => sig.subscribe(cb),
86
- () => sig.value,
87
- () => sig.value,
88
- );
89
- return sig;
90
- }
91
-
92
- export function useComputed<T>(fn: () => T): Signal<T> {
93
- const sig = useMemo(() => signal(fn()), []);
94
- return sig;
95
- }
96
-
97
- export function computed<T>(fn: () => T): Signal<T> {
98
- return signal(fn());
99
- }
100
-
101
- export function effect(fn: () => void | (() => void)): () => void {
102
- const cleanup = fn();
103
- return typeof cleanup === "function" ? cleanup : () => {};
104
- }
105
-
106
- export function batch(fn: () => void): void {
107
- fn();
108
- }
109
-
110
- export function useSignalEffect(fn: () => void | (() => void)): void {
111
- fn();
112
- }
113
-
114
- export type { Signal as ReadonlySignal };
115
- ```
116
-
117
- > **WARNING**: The `subscribe()` unwrapping is the single most critical line in
118
- > this file. Without it, every component using `useSignal` will crash with
119
- > "TypeError: J is not a function" (minified) or "TypeError: destroy_ is not a
120
- > function" (non-minified), which then cascades into React #419 hydration
121
- > failures and #130 undefined component errors across the entire page.
122
-
123
- ### Replace Imports
124
-
125
- ```bash
126
- # Safe for bulk sed
127
- sed -i '' 's|from "@preact/signals"|from "~/sdk/signal"|g'
128
- ```
129
-
130
- ### React Subscriptions
131
-
132
- The signal shim's `.value` getter/setter does NOT create automatic React subscriptions. Reading `signal.value` during render won't re-render when the signal changes. This is the #1 source of "it works in Preact but not React" bugs.
133
-
134
- Replace manual useEffect+subscribe boilerplate with `useStore`:
135
-
136
- ```typescript
137
- // OLD (manual subscription -- works but verbose)
138
- const { displayCart } = useUI();
139
- const [open, setOpen] = useState(false);
140
- useEffect(() => {
141
- setOpen(displayCart.value);
142
- return displayCart.subscribe(() => setOpen(displayCart.value));
143
- }, []);
144
-
145
- // NEW (useStore from @tanstack/react-store -- recommended)
146
- import { useStore } from "@tanstack/react-store";
147
- const { displayCart } = useUI();
148
- const open = useStore(displayCart.store);
149
- ```
150
-
151
- Write-only consumers (event handlers) don't need `useStore`:
152
- ```typescript
153
- // This still works -- .value setter backed by TanStack Store
154
- onClick={() => { displayCart.value = true; }}
155
- ```
156
-
157
- ### DaisyUI Drawer Workaround
158
-
159
- DaisyUI drawers use a hidden checkbox to toggle visibility. Since signal changes don't trigger React re-renders, the checkbox `checked` attribute never updates. Pragmatic workaround: directly toggle the DOM checkbox alongside the signal:
160
-
161
- ```typescript
162
- // After setting the signal, also toggle the drawer checkbox
163
- displayCart.value = true;
164
- const checkbox = document.getElementById("cart-drawer") as HTMLInputElement;
165
- if (checkbox) checkbox.checked = true;
166
- ```
167
-
168
- This is an interim pattern until all signal consumers use `useStore`. The `useStore` approach is the proper fix because it makes the Drawer component re-render, which updates the checkbox `checked` prop through React.
169
-
170
- ### Global State Hook Pattern (useCart, useUI)
171
-
172
- Hooks that manage global state (cart, UI drawers) need module-level singleton state with a subscription mechanism. The pattern:
173
-
174
- ```typescript
175
- let _state: CartData | null = null;
176
- const _listeners = new Set<() => void>();
177
-
178
- function notify() { _listeners.forEach((fn) => fn()); }
179
-
180
- export function useCart() {
181
- const [, forceUpdate] = useState(0);
182
-
183
- useEffect(() => {
184
- const listener = () => forceUpdate((n) => n + 1);
185
- _listeners.add(listener);
186
- return () => { _listeners.delete(listener); };
187
- }, []);
188
-
189
- return {
190
- cart: {
191
- get value() { return _state; },
192
- set value(v) { _state = v; notify(); },
193
- },
194
- // ... methods that update _state and call notify()
195
- };
196
- }
197
- ```
198
-
199
- Every component calling `useCart()` subscribes to changes, and state updates trigger re-renders across all subscribers. This replaces the Preact signals global reactivity.
200
-
201
- ## Signal Type References
202
-
203
- If components use `Signal<T>` as a prop type:
204
-
205
- ```typescript
206
- // OLD
207
- import { Signal } from "@preact/signals";
208
- interface Props { quantity: Signal<number>; }
209
-
210
- // NEW
211
- import type { ReactiveSignal } from "~/sdk/signal";
212
- interface Props { quantity: ReactiveSignal<number>; }
213
- ```
214
-
215
- ## Verification
216
-
217
- ```bash
218
- grep -r '@preact/signals' src/ --include='*.ts' --include='*.tsx'
219
- # Should return ZERO matches
220
- ```