@decocms/start 2.18.0 → 2.20.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.
@@ -17,7 +17,8 @@ of which sections below actually apply to your codebase:
17
17
  npx -p @decocms/start deco-post-cleanup
18
18
 
19
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.
20
+ # Safe rules: dead-lib-shims, dead-runtime-shim, local-widgets-types,
21
+ # vtex-shim-regression (swap subset), obsolete-vite-plugins.
21
22
  # Other rules stay detect-only — they require human judgment.
22
23
  npx -p @decocms/start deco-post-cleanup --fix
23
24
 
@@ -29,18 +30,23 @@ npx -p @decocms/start deco-post-cleanup --json
29
30
  ```
30
31
 
31
32
  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.
33
+ suggested fix for each finding. With `--fix`, the safe rules
34
+ auto-apply: `rm` for dead files, regex-anchored import rewrites for
35
+ shadowed shims (`local-widgets-types`, `dead-runtime-shim`), the swap
36
+ subset of `vtex-shim-regression`, and JS-aware removal of obsolete
37
+ inline plugin literals from `vite.config.ts`. The output explicitly
38
+ tags rules that require manual work as `(0 fixed, manual)`, so you
39
+ always know what's left after auto-fix runs.
37
40
 
38
41
  Real-world signal: on baggagio, `--fix` produced a byte-identical
39
42
  diff to the manual cleanup PR a human had just made (45 files,
40
43
  +45/-53). On casaevideo-storefront (production), the audit caught
41
44
  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.
45
+ detect — `--fix` covers the swap subset of those automatically since
46
+ `>= 2.16.0`. On the same site's `vite.config.ts`, `--fix` removes
47
+ both obsolete inline plugins (`site-manual-chunks` +
48
+ `deco-stub-meta-gen`) cleanly — ~74 LOC / 2.5 KB gone, attached
49
+ comments included.
44
50
 
45
51
  ## 1. Delete unused `src/lib/*` shims
46
52
 
@@ -115,8 +121,14 @@ The framework's `decoVitePlugin()` now handles both:
115
121
  old split caused circular-dep load-order crashes — every site overrode it)
116
122
  - `meta.gen.{json,ts}` is stubbed on the client by default
117
123
 
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).
124
+ Delete both inline plugins from the site's `vite.config.ts`. Since
125
+ `@decocms/start >= 2.19.0`, `deco-post-cleanup --fix` does this for
126
+ you — it walks the AST with brace-balanced parsing (template literals
127
+ and nested `{}` inside `config()`/`load()` bodies don't trip it up),
128
+ removes the literal **plus its trailing comma + attached `// ...`
129
+ comment block**, and is idempotent (rerunning is a no-op). Block
130
+ comments are left alone. Verify the production build still succeeds
131
+ (`vite build` in the site repo).
120
132
 
121
133
  ## 3. Drop the `runtime.ts` `invoke` shim
122
134
 
@@ -688,27 +688,83 @@ Releases shipped from Wave 6:
688
688
  - `@decocms/apps@1.7.0` — adds `vtex/hooks/createUseCart` factory
689
689
  - `@decocms/start@2.8.0` (compile phase) → `2.9.0` (template shim) → `2.10.0` (per-site config)
690
690
 
691
- ### Wave 12 (kicked off 2026-05-01 after D1–D5 sign-off) — Priority 1 (framework + commerce)
691
+ ### Wave 12 (kicked off 2026-05-01 after D1–D5 sign-off) — Priority 1 (framework + commerce) — ✅ **COMPLETE**
692
692
 
693
693
  After surfacing als-storefront as the third migration target (heavy on
694
694
  htmx, ~120 hx-* files, prior als-tanstack attempt thrown away), the
695
- "wait for 3rd site" deferrals collapse. Wave 12 ships the abstractions
696
- that als + casaevideo + baggagio have already justified, plus the
697
- audit `--fix` work D3 forces us into.
698
-
699
- **Planned PRs (will be filled in as they ship):**
700
-
701
- - **W12-A** apps-start: `createUseUser` factory (mirrors `createUseCart` from #32)
702
- - **W12-B** apps-start: `createUseWishlist` factory (same pattern)
703
- - **W12-C** deco-start: throwing stubs in `lib-utils.ts` template + per-stub message linking to skill (D3 implementation)
704
- - **W12-D** deco-start: audit `--fix` for `toProduct` swap (uses #121's `meta.fixHints`)
705
- - **W12-E** deco-start: audit `--fix` for `withSegmentCookie` swap
706
- - **W12-F** deco-start: audit `--fix` for `obsolete-vite-plugins` rule (mechanical cleanup)
707
- - **W12-G** apps-start (or deco-start CLAUDE.md cross-link): per-repo pointer to `migration-tooling-policy.mdc` so the constitutional rule is discoverable from any consumer repo
708
- - **W12-H** deco-start: cleanup phase scaffolds `.cursor/rules/migration-policy-pointer.mdc` in target site, pointing at the canonical rule (D1/D4 enforcement at site level)
709
-
710
- Wave 12 ships in priority-1 order; Wave 13 only starts when the
711
- foundation is in place.
695
+ "wait for 3rd site" deferrals collapse. Wave 12 shipped the abstractions
696
+ that als + casaevideo + baggagio had already justified, plus the
697
+ audit `--fix` work D3 forces us into. **9 PRs across `deco-start` and
698
+ `apps-start`, all merged.**
699
+
700
+ **Shipped PRs:**
701
+
702
+ - **W12-A + W12-B** [`apps-start#33`](https://github.com/decocms/apps-start/pull/33) — `feat(vtex/hooks): add createUseUser + createUseWishlist factories` **MERGED**.
703
+ Mirrors the `createUseCart` shape: invoke-driven legacy state machine, signal-shaped public API (`.value`), independent instances per call. `createUseWishlist` also exposes the `legacyAddArgsToCanonical` and `findWishlistEntry` helpers so site code can keep its old `productId`/`productGroupId` swap convention while routing through the canonical `vtex.actions.{addToWishlist, removeFromWishlist}` signature. New unit tests assert factory shape, instance independence, the arg-swap convention, and the entry-finder helper.
704
+ - **(Lint unblock)** [`apps-start#34`](https://github.com/decocms/apps-start/pull/34) `chore(lint): tighten shopify storefront.graphql.gen.ts types + biome formatting` ✅ **MERGED**.
705
+ Cleared pre-existing `noExplicitAny` failures in `shopify/utils/storefront/storefront.graphql.gen.ts` plus formatting drift in `vtex/__tests__/client-segment-cookie.test.ts` so subsequent Wave 12 PRs in `apps-start` could land on a green CI baseline. Replaced loose `any` types with a structured `ProductFilter` shape (derived from real consumers) and `unknown` elsewhere; ran `biome check --write` to fix import order + formatting.
706
+ - **W12-C** [`deco-start#123`](https://github.com/decocms/deco-start/pull/123) `feat(migrate): D3 — generated stubs throw at runtime` **MERGED**, released as `@decocms/start@2.16.0`.
707
+ Implements **D3** verbatim. The migration-time stubs in `lib-utils.ts` for `toProduct`, `getISCookiesFromBag`, `getSegmentFromBag`, `withSegmentCookie` no longer return identity-cast values, empty objects, or empty `Headers` — they now `throw new Error(STUB_MSG)` with a per-symbol message that names the canonical replacement, the canonical signature, and the `deco-post-cleanup --fix` invocation. Tests assert the throwing bodies + that other functional helpers (e.g. `parseCookie`, `isFilterParam`) are untouched.
708
+ - **W12-D + W12-E** [`deco-start#124`](https://github.com/decocms/deco-start/pull/124) `feat(audit): vtex-shim-regression --fix for swap-able symbols` **MERGED**, released as `@decocms/start@2.17.0`.
709
+ Auto-fixes the swap subset of the regression rule: when every imported symbol from a `~/lib/vtex-*` shim has a `kind: "swap"` hint pointing to the same canonical module, the rule rewrites the `from "..."` clause to canonical and leaves the named-import list (including `as`-aliases) verbatim. Mixed swap + refactor surfaces, and shims that mix stubs with real impls (e.g. `isFilterParam`), are deliberately left for manual fix. 6 new tests + skill doc § 5 update.
710
+ - **W12-i** [`deco-start#125`](https://github.com/decocms/deco-start/pull/125) `feat(migrate): scaffold useUser + useWishlist as factory shims (vtex)` ✅ **MERGED**.
711
+ Updates the migration script's `hooks.ts` template so freshly migrated VTEX sites get 3-line factory shims (`export const { useUser, resetUser } = createUseUser({ invoke })`) instead of 200-LOC singletons that were copy-pasted by the old migration. Non-VTEX sites still get the legacy stubs but with docstrings pointing at the factories for parity context. 4 new tests cover both branches and a line-count budget.
712
+ - **W12-F** [`deco-start#127`](https://github.com/decocms/deco-start/pull/127) — `feat(audit): obsolete-vite-plugins --fix` ✅ **MERGED**.
713
+ JS-aware applyFix for the rule. Walks `vite.config.ts` with a brace-counter that skips strings, template literals (including `${...}` interpolation), and line/block comments, so nested `{}` inside `config()` / `load()` / `resolveId()` bodies do not throw off boundary detection. Removes the inline literal + trailing `,\n` + the contiguous block of `// ...` comments immediately attached above. Idempotent. 7 new tests + smoke verified against real casaevideo `vite.config.ts` (162 LOC → both plugins gone, 2503 bytes / ~74 LOC removed, structurally identical to baggagio's already-clean shape, post-fix audit returns 0 findings).
714
+ - **W12-G** [`apps-start#35`](https://github.com/decocms/apps-start/pull/35) — `docs: add AGENTS.md cross-linking the canonical migration policy` ✅ **MERGED**.
715
+ Adds an AGENTS.md to `apps-start` so any agent or contributor opening that repo knows the canonical migration policy lives in `decocms/deco-start` and what D1–D5 mean specifically inside `apps-start` (especially D4: site-local apps live in the *site*, not in `apps-start`). Architecture overview + cross-link table.
716
+ - **W12-H** [`deco-start#126`](https://github.com/decocms/deco-start/pull/126) — `feat(migrate): scaffold migration-tooling-policy pointer rule` ✅ **MERGED**, released as `@decocms/start@2.18.0`.
717
+ Migration scaffold phase now writes `.cursor/rules/migration-tooling-policy.mdc` into every newly migrated site. The pointer is `alwaysApply: true`, links to the canonical rule and plan in `decocms/deco-start`, includes a one-line-per-decision D1–D5 table scoped to the site, and points at the `deco-post-cleanup --fix` / `--strict` commands. Length budget under 3 KB so it stays a pointer, not a copy. 8 new tests.
718
+
719
+ Wave 12 ships in priority-1 order; Wave 13 starts now.
720
+
721
+ ### Wave 12 — discoveries
722
+
723
+ - **D3 + audit `--fix` is a closed loop, not an either/or.** The
724
+ symmetry that `--fix` for swap-able stubs (W12-D/E) combined with
725
+ throwing stubs (W12-C) produces is significant: the moment a
726
+ migrated site runs anything that hits a stub'd symbol, it throws
727
+ with an actionable error pointing at the canonical replacement; the
728
+ same moment, `--fix` knows what `from "..."` clause to rewrite. The
729
+ user no longer has a "silent regression" failure mode.
730
+ - **Per-symbol fix-hint table now has 5 consumers.** It's read by:
731
+ the rule's prose `fix:` field, the rule's `meta.fixHints`
732
+ structured payload, the runtime stub error message, the `--fix`
733
+ rewriter (selects swap candidates), and the skill doc table. Adding
734
+ a 5th, 6th, Nth stub means appending one entry to `STUB_FIX_HINTS`
735
+ — every consumer picks up the new symbol for free.
736
+ - **Site-level policy enforcement at scaffold time, not runtime.**
737
+ W12-H ships the canonical D1–D5 policy *as a pointer* into every
738
+ new site. Cursor sessions in those sites load the rule with
739
+ `alwaysApply: true`, so they know the policy without us having to
740
+ pull a copy of the rule into each repo. Drift-free by construction
741
+ — the canonical rule changes upstream and the pointer keeps
742
+ pointing.
743
+ - **Brace-balanced parsing + comment attachment makes
744
+ `obsolete-vite-plugins` `--fix` safe at scale.** The casaevideo
745
+ smoke test confirmed the approach handles real-world vite configs
746
+ with multi-line plugins, attached comments describing the
747
+ workaround, template literals containing `}`, and nested
748
+ `rollupOptions`. Idempotency falls out of "rule found 0 plugins
749
+ → no findings → no fix actions". This is the pattern for any
750
+ future `--fix` that needs to surgically edit a file: extract a
751
+ span helper, write surface-level tests, smoke against a real
752
+ production file, ship.
753
+ - **`apps-start` had latent CI debt.** W12 surfaced pre-existing
754
+ `noExplicitAny` failures in `shopify/utils/storefront/storefront.graphql.gen.ts`
755
+ that had been failing on `main` for an unknown duration. The lint
756
+ unblock PR (`apps-start#34`) is the kind of "passing through" fix
757
+ that should land first whenever a CI gate is red. Don't paper
758
+ over it.
759
+ - **Factories migrate cleaner than templates.** Comparing
760
+ `apps-start#33` (factories) to `deco-start#125` (template that
761
+ *consumes* the factories), the factory PR is the larger artifact
762
+ but the template PR shipped 4 lines into each generated file.
763
+ Investing once in a well-shaped factory pays a 50:1 multiplier on
764
+ every site that runs the migration after that point. This is the
765
+ D4 promotion path working end-to-end: build it once at site level,
766
+ prove it on 2 sites, promote to `@decocms/apps`, then rewrite the
767
+ template to use the canonical.
712
768
 
713
769
  ### Wave 13 (htmx foundations — Priority 2 part 1) — planned
714
770
 
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "2.18.0",
3
+ "version": "2.20.0",
4
4
  "type": "module",
5
5
  "description": "Deco framework for TanStack Start - CMS bridge, admin protocol, hooks, schema generation",
6
6
  "main": "./src/index.ts",
7
7
  "bin": {
8
8
  "deco-migrate": "./scripts/migrate.ts",
9
- "deco-post-cleanup": "./scripts/migrate-post-cleanup.ts"
9
+ "deco-post-cleanup": "./scripts/migrate-post-cleanup.ts",
10
+ "deco-htmx-analyze": "./scripts/htmx-analyze.ts"
10
11
  },
11
12
  "exports": {
12
13
  ".": "./src/index.ts",
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * HTMX surface analyzer — CLI entry point.
4
+ *
5
+ * Inventories the `hx-*` surface of a Deco storefront so the engineer
6
+ * (or the next codemod wave) knows exactly what shapes are out there
7
+ * before starting the rewrite to React. Per D2 in the migration
8
+ * tooling policy, all htmx is rewritten on migration; no runtime is
9
+ * shipped in `@decocms/start`.
10
+ *
11
+ * Usage (from a site directory):
12
+ * npx -p @decocms/start deco-htmx-analyze
13
+ * npx -p @decocms/start deco-htmx-analyze --source /path/to/site
14
+ * npx -p @decocms/start deco-htmx-analyze --json
15
+ *
16
+ * Options:
17
+ * --source <dir> Site directory to analyze (default: current directory)
18
+ * --json Emit machine-readable JSON instead of pretty text
19
+ * --top <n> Show top N files by occurrence count (default: 20)
20
+ * --help, -h Show this help
21
+ *
22
+ * Wave 13-A. Read-only. Codemods land in Wave 14.
23
+ */
24
+
25
+ import * as path from "node:path";
26
+ import { realFsAdapter } from "./migrate/post-cleanup/runner";
27
+ import {
28
+ analyzeHtmx,
29
+ type HtmxCategory,
30
+ type HtmxInventory,
31
+ } from "./migrate/analyzers/htmx-analyze";
32
+ import { banner, bold, cyan, gray, green, red, yellow } from "./migrate/colors";
33
+
34
+ interface CliOpts {
35
+ source: string;
36
+ json: boolean;
37
+ top: number;
38
+ help: boolean;
39
+ }
40
+
41
+ function parseArgs(args: string[]): CliOpts {
42
+ let source = ".";
43
+ let json = false;
44
+ let top = 20;
45
+ let help = false;
46
+ for (let i = 0; i < args.length; i++) {
47
+ switch (args[i]) {
48
+ case "--source":
49
+ source = args[++i];
50
+ break;
51
+ case "--json":
52
+ json = true;
53
+ break;
54
+ case "--top":
55
+ top = Number.parseInt(args[++i] ?? "20", 10);
56
+ if (Number.isNaN(top) || top < 0) top = 20;
57
+ break;
58
+ case "--help":
59
+ case "-h":
60
+ help = true;
61
+ break;
62
+ default:
63
+ console.error(`Unknown argument: ${args[i]}`);
64
+ process.exit(1);
65
+ }
66
+ }
67
+ return { source, json, top, help };
68
+ }
69
+
70
+ function printHelp(): void {
71
+ console.log(`deco-htmx-analyze
72
+
73
+ Inventory the htmx surface (hx-* attributes) of a Deco storefront.
74
+
75
+ Usage:
76
+ npx -p @decocms/start deco-htmx-analyze [options]
77
+
78
+ Options:
79
+ --source <dir> Site directory to analyze (default: cwd)
80
+ --json Emit machine-readable JSON
81
+ --top <n> Top N files by occurrence count (default: 20)
82
+ --help, -h Show this help
83
+
84
+ The output is read-only. Codemods that rewrite htmx to React are a
85
+ planned follow-up — see the deco-to-tanstack-migration skill for the
86
+ per-pattern rewrite recipes.
87
+ `);
88
+ }
89
+
90
+ const CATEGORY_DESCRIPTIONS: Record<HtmxCategory, string> = {
91
+ "event-handler": "hx-on:* with no fetch attr — pure client-side handler",
92
+ "form-swap": "hx-post on a <form> with hx-target/hx-swap",
93
+ "click-swap": "hx-get/hx-post on a button with hx-target",
94
+ "auto-fetch": "hx-trigger=keyup/intersect/etc on input or auto-fired element",
95
+ "oob-swap": "hx-swap-oob / hx-select-oob — out-of-band patches",
96
+ boost: "hx-boost=true — link prefetch, already-SPA in TanStack Start",
97
+ unmatched: "hx-* attribute set that didn't match a known pattern",
98
+ };
99
+
100
+ const CATEGORY_RECIPES: Record<HtmxCategory, string> = {
101
+ "event-handler":
102
+ "replace hx-on:click={useScript(...)} with onClick={() => { ... }}",
103
+ "form-swap":
104
+ "<form onSubmit> + useMutation/server function; render result with state",
105
+ "click-swap":
106
+ "setState/setView + conditional render, or sub-route via TanStack Router",
107
+ "auto-fetch":
108
+ "debounced state + useQuery; for intersect use IntersectionObserver",
109
+ "oob-swap":
110
+ "manual: out-of-band has no 1:1; refactor to broadcast event + listener",
111
+ boost: "replace <a hx-boost> with TanStack Router <Link> (already SPA)",
112
+ unmatched: "manual review",
113
+ };
114
+
115
+ const CATEGORY_ORDER: HtmxCategory[] = [
116
+ "event-handler",
117
+ "click-swap",
118
+ "form-swap",
119
+ "auto-fetch",
120
+ "boost",
121
+ "oob-swap",
122
+ "unmatched",
123
+ ];
124
+
125
+ function printText(inv: HtmxInventory, top: number): void {
126
+ banner("HTMX surface analysis");
127
+
128
+ if (inv.totalOccurrences === 0) {
129
+ console.log(green("✓ No hx-* attributes found.\n"));
130
+ return;
131
+ }
132
+
133
+ console.log(`${bold("Files with hx-* usage:")} ${inv.totalFiles}`);
134
+ console.log(`${bold("Total occurrences:")} ${inv.totalOccurrences}\n`);
135
+
136
+ console.log(bold("By category:"));
137
+ for (const cat of CATEGORY_ORDER) {
138
+ const count = inv.byCategory[cat];
139
+ if (count === 0) continue;
140
+ const label = `${cat.padEnd(15)}`;
141
+ const desc = gray(CATEGORY_DESCRIPTIONS[cat]);
142
+ console.log(` ${cyan(label)} ${String(count).padStart(4)} ${desc}`);
143
+ }
144
+
145
+ console.log(`\n${bold("Migration recipes:")}`);
146
+ for (const cat of CATEGORY_ORDER) {
147
+ if (inv.byCategory[cat] === 0) continue;
148
+ console.log(` ${cyan(cat.padEnd(15))} ${gray(CATEGORY_RECIPES[cat])}`);
149
+ }
150
+
151
+ console.log(`\n${bold(`Top ${top} files by occurrence:`)}`);
152
+ const slice = inv.files.slice(0, top);
153
+ const widest = Math.max(...slice.map((f) => f.file.length), 30);
154
+ for (const f of slice) {
155
+ const detail = CATEGORY_ORDER.filter((c) => f.byCategory[c] > 0)
156
+ .map((c) => `${c}=${f.byCategory[c]}`)
157
+ .join(", ");
158
+ console.log(
159
+ ` ${f.file.padEnd(widest)} ${String(f.total).padStart(3)} ${gray(detail)}`,
160
+ );
161
+ }
162
+
163
+ if (inv.files.length > top) {
164
+ console.log(gray(` …and ${inv.files.length - top} more file(s)`));
165
+ }
166
+
167
+ console.log(`\n${bold("Sample call sites:")}`);
168
+ for (const cat of CATEGORY_ORDER) {
169
+ const samples = inv.samples[cat];
170
+ if (samples.length === 0) continue;
171
+ console.log(` ${cyan(cat)}`);
172
+ for (const s of samples) {
173
+ const attrs = s.attrs.join(", ");
174
+ console.log(
175
+ ` ${gray(`${s.file}:${s.line}`)} <${s.tag}> [${attrs}]`,
176
+ );
177
+ }
178
+ }
179
+
180
+ const hasOob = inv.byCategory["oob-swap"] > 0;
181
+ const hasUnmatched = inv.byCategory.unmatched > 0;
182
+ if (hasOob || hasUnmatched) {
183
+ console.log();
184
+ if (hasOob) {
185
+ console.log(
186
+ yellow(
187
+ "⚠ oob-swap occurrences require manual rewrite — no 1:1 React equivalent.",
188
+ ),
189
+ );
190
+ }
191
+ if (hasUnmatched) {
192
+ console.log(
193
+ yellow(
194
+ "⚠ unmatched occurrences require manual review — see Sample call sites.",
195
+ ),
196
+ );
197
+ }
198
+ }
199
+ console.log();
200
+ }
201
+
202
+ function main(argv: string[]): number {
203
+ const opts = parseArgs(argv);
204
+ if (opts.help) {
205
+ printHelp();
206
+ return 0;
207
+ }
208
+
209
+ const sourceDir = path.resolve(opts.source);
210
+ const inv = analyzeHtmx(sourceDir, realFsAdapter);
211
+
212
+ if (opts.json) {
213
+ console.log(JSON.stringify(inv, null, 2));
214
+ } else {
215
+ printText(inv, opts.top);
216
+ }
217
+ return 0;
218
+ }
219
+
220
+ try {
221
+ process.exit(main(process.argv.slice(2)));
222
+ } catch (err) {
223
+ console.error(red(`✗ deco-htmx-analyze failed: ${(err as Error).message}`));
224
+ if (process.env.DEBUG) console.error((err as Error).stack);
225
+ process.exit(1);
226
+ }