@decocms/start 2.21.0 → 2.23.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.
@@ -239,6 +239,88 @@ Each item carries a status: ⬜ pending, 🟡 in progress, ✅ done, 🚫 blocke
239
239
 
240
240
  > Append-only. Each entry: date, what we found, where it impacts the plan.
241
241
 
242
+ ### 2026-05-01 — Wave 15-A double-check exposed a self-perpetuating template→audit loop
243
+
244
+ - **Q1 (sites→packages promotion completeness):** Two subagents
245
+ swept `casaevideo-storefront` + `baggagio-tanstack`. The big
246
+ A-list icebergs are caught — but **5 cross-site duplications
247
+ satisfying D4** slipped through (`useSuggestions`, `useOffer`
248
+ forks, `useVariantPossibilities` forks, site-local copies of
249
+ framework `clx` / `useSendEvent`, three competing `Picture`
250
+ APIs). Plus 4 migration debts where the framework already has
251
+ the answer (`useCart` factory not adopted in casaevideo,
252
+ `runtime.ts` inline proxy still scaffolded, location matcher
253
+ duplication, inline cookie helpers).
254
+ - **Q2 (script/skill coverage of what we shipped):** Worse. The
255
+ migration script's templates were **scaffolding code that the
256
+ audit's `--fix` then removed** — the textbook
257
+ self-perpetuating loop. `templates/vite-config.ts` was emitting
258
+ `site-manual-chunks` + `deco-stub-meta-gen` (both already
259
+ inside `decoVitePlugin()`). `templates/server-entry.ts` was
260
+ emitting a 47-line `createNestedInvokeProxy` body (already in
261
+ `@decocms/start/sdk`). The factories shipped in W12 (`createUseUser`
262
+ / `createUseWishlist`) had **zero skill mentions** — the
263
+ pre-W12 manual approach was still the canonical doc. Cookie
264
+ passthrough helpers in `cookiePassthrough.ts` were **half-shipped**:
265
+ the deco-start side compiled, the apps-start providers it
266
+ references in its own docstring don't exist, and the migration
267
+ script never wired either.
268
+ - **Decided: ship Wave 15-A as a single PR.** Drop the obsolete
269
+ emissions (G1/G2/G4/G5), expand `dead-runtime-shim` to catch
270
+ both the legacy inline shape (with `Runtime` export) and the
271
+ Wave-15-A canonical re-export shape (skip — desired form),
272
+ publish a `platform-hooks-factories.md` skill that supersedes
273
+ the stale README, and update plan + journal.
274
+ - **Defer to Wave 15-B / 16:** (G3) promotion of the
275
+ `invoke.gen.ts` 170-LOC server-fn block to apps-start needs
276
+ research on TanStack Start's compiler scanning behaviour
277
+ (whether `createServerFn` can be transformed when it lives in
278
+ a node_module). (H1) full cookie-passthrough provider wiring
279
+ (`setRequestCookieProvider` / `setResponseCookieForwarder` in
280
+ apps-start, auto-wire in `setup.ts`) needs design. The 5
281
+ cross-site convergence promotions (`useSuggestions`,
282
+ `useOffer` factory, `Picture` unification, `clx`/`useSendEvent`
283
+ redirects, `relative()` extension) are now in the priority-2
284
+ backlog, sequenced after Wave 15-A merges.
285
+ - **The double-check itself is a useful primitive.** "Did we
286
+ ship script/skill/audit coverage for everything we built?" run
287
+ against the framework + apps inventory consistently surfaces
288
+ these loops. Codify it: when promoting a new factory or
289
+ helper, the PR checklist must include "matching template
290
+ emit", "matching audit-rule expansion", "matching skill doc
291
+ entry". This is the kind of self-check that prevents the next
292
+ 16-month-old stale skill.
293
+
294
+ ### 2026-05-01 — Wave 14-A rescoped from three codemods to one based on real als data
295
+
296
+ - **Pre-data plan vs post-data plan.** The plan called for three
297
+ htmx codemods (`event-handler`, `form-swap`, `click-swap`).
298
+ After running `deco-htmx-analyze` against als-storefront's
299
+ actual code (210 occurrences across 133 files), only the
300
+ `event-handler` bucket (88 occurrences, 42 %) genuinely admits
301
+ a mechanical rewrite — the other buckets need per-call-site
302
+ product decisions a codemod cannot encode. **Decided: ship
303
+ one codemod (W14-A: `htmx-on-event-rename`), defer the other
304
+ two to W15+.** Rationale captured in the Wave 14 — discoveries
305
+ block.
306
+ - **Codemod shape generalises:** rename + preserve body +
307
+ conditional file-level TODO. Three outputs, one mechanical,
308
+ one verbatim, one conditional on body-content heuristics. This
309
+ is the shape any future per-pattern codemod should target.
310
+ - **Smoke against the real source tree validated the design in
311
+ five minutes.** 754 files scanned, 71 changed, 98 renames, 67
312
+ TODO injections (94 % of changed files). Without that smoke
313
+ step we'd have shipped blind on edge cases like multi-line
314
+ values, mixed standard + lifecycle hooks on the same element,
315
+ and the colon-vs-dash variants both showing up in the same
316
+ file.
317
+ - **The codemod + audit pair closes another loop.** Same shape
318
+ as W12 (D3 throwing stubs + audit `--fix` for swap-able
319
+ stubs). The codemod removes the mechanical half of the htmx
320
+ surface; the `htmx-residue` audit catches the surviving half
321
+ in CI. Engineers can never silently ship a half-rewritten
322
+ file.
323
+
242
324
  ### 2026-05-01 — als-storefront surfaces the htmx track + policy reset
243
325
 
244
326
  - **als-storefront is the third migration target and the first
@@ -834,12 +916,217 @@ analysis, rewrite recipes, and a "rewrite-complete" gate.
834
916
  rule means changing one file, getting `--strict` and `--json`
835
917
  for free.
836
918
 
837
- ### Wave 14 (htmx codemods + first als migration on 2.14+) — planned
838
-
839
- - **W14-A** deco-start: codemod `transforms/htmx-form-post-swap.ts` `<form hx-post={url} hx-target hx-swap>` → `useMutation` + state setter
840
- - **W14-B** deco-start: codemod `transforms/htmx-click-fetch-swap.ts` `<button hx-get={url}>` → onClick + invoke + state
841
- - **W14-C** deco-start: codemod `transforms/htmx-on-click-script.ts` `hx-on:click={useScript(...)}` `onClick` handler
842
- - **W14-D** als: rm -rf old als-tanstack, fresh `deco-migrate` run on 2.14+ with new htmx codemods. Per D5 (no --restart), this is the only restart UX.
919
+ ### Wave 14 (htmx codemod Priority 2 part 2) — ✅ **PARTIAL / RESCOPED**
920
+
921
+ After shipping the W13 htmx foundations and gathering real data
922
+ from als-storefront with `deco-htmx-analyze`, the planned three-codemod
923
+ scope was reduced to **one codemod** + **one inventory artefact**.
924
+ The other two codemods (form-swap, click-swap) were deferred to W15+,
925
+ to be designed *after* als migration data exposes which exact
926
+ attribute clusters dominate. **Rationale logged in W14 discoveries.**
927
+
928
+ **Shipped:**
929
+
930
+ - **W14-A** [`deco-start#132`](https://github.com/decocms/deco-start/pull/132) — `feat(migrate): htmx-on-event-rename codemod` ✅ **MERGED**, released as `@decocms/start@2.22.0`.
931
+ Adds `scripts/migrate/transforms/htmx-on-events.ts` to the migrate
932
+ `transforms/` pipeline. Mechanically rewrites `hx-on:event=` and
933
+ `hx-on-event=` (colon + dash variants) to the React equivalent
934
+ for every standard DOM event in `STANDARD_EVENT_MAP` (40 entries:
935
+ click, submit, change, input, key*, mouse*, focus*, drag*, touch*,
936
+ paste/copy/cut, scroll, wheel, load, contextmenu). Handler bodies
937
+ are preserved verbatim. **Idempotent** — running twice is a no-op.
938
+ Two safety hatches: htmx lifecycle events (`hx-on:htmx-*`) and
939
+ unknown custom events left alone (the `htmx-residue` audit catches
940
+ them); a single top-of-file MIGRATION TODO comment is injected
941
+ when the body references Fresh-only globals (`useScript(…)`,
942
+ `globalThis.window.STOREFRONT`, `STOREFRONT.…`) so engineers
943
+ don't ship a syntactically-clean file with broken runtime calls.
944
+ 29 unit tests + als-shaped fixtures (AddToBagButton, SearchInput,
945
+ RecoveryPassword form, Footer.tsx). 339/339 pass; typecheck clean.
946
+ htmx-rewrite skill § Pattern 1 cross-references the codemod.
947
+ - **W14-B** [`deco-start#132`](https://github.com/decocms/deco-start/pull/132) — captured the **als-storefront htmx inventory** in this plan (this section) as a fixture for future W15+ codemod design.
948
+
949
+ **Deferred (intentionally) — see Wave 14 discoveries:**
950
+
951
+ - ~~**W14-C** codemod `transforms/htmx-form-post-swap.ts`~~ — moved to W15+. The form-swap rewrite is genuinely non-mechanical (per-call-site decisions about optimistic vs pessimistic UI, where to surface loading state, which response handler shape). A speculative codemod would produce React skeletons that still need ~80 % manual work.
952
+ - ~~**W14-D** codemod `transforms/htmx-click-fetch-swap.ts`~~ — moved to W15+. Same logic; on top of that, choosing between local state machine vs sub-route is a routing-architecture decision that varies per page.
953
+
954
+ #### W14-A smoke + als inventory (captured 2026-05-01)
955
+
956
+ The W13-A `deco-htmx-analyze` CLI run against als-storefront's
957
+ production Fresh tree:
958
+
959
+ | Category | Count | % | Notes |
960
+ |---|---:|---:|---|
961
+ | `event-handler` | 88 | 42 % | **Codemoded by W14-A** — mechanical rename |
962
+ | `click-swap` | 64 | 30 % | Manual (W15+) — needs state vs sub-route decision |
963
+ | `form-swap` | 20 | 10 % | Manual (W15+) — needs `useMutation` shape decision |
964
+ | `auto-fetch` | 9 | 4 % | Manual — debounced state + `useQuery` |
965
+ | `oob-swap` | 8 | 4 % | Manual — no 1:1 React equivalent |
966
+ | `unmatched` | 21 | 10 % | Mostly typed-generic noise (`<string>` from `Map<string,X>`) |
967
+ | **Total** | **210** | | across 133 files |
968
+
969
+ W14-A codemod sweep against the same tree (754 ts/tsx files):
970
+
971
+ | Metric | Value |
972
+ |---|---:|
973
+ | Files scanned | 754 |
974
+ | Files changed | 71 |
975
+ | Total `hx-on:*` attributes renamed | 98 |
976
+ | Files getting the MIGRATION TODO | 67 (94 % of changed) |
977
+
978
+ The 98 vs 88 discrepancy is expected: the analyzer counts attribute
979
+ *clusters* per element (an `<input hx-post hx-target hx-on:change>`
980
+ classifies as one `auto-fetch`); the codemod counts individual
981
+ `hx-on:*` attribute renames (the same element gets one rename
982
+ plus the `auto-fetch` cluster left intact for the engineer to
983
+ finish). Net effect: ~98 mechanical wins, leaving ~112 cluster
984
+ rewrites (click-swap + form-swap + auto-fetch + oob-swap +
985
+ unmatched) for the engineer — matching the manual rewrite
986
+ recipes in `references/htmx-rewrite.md`.
987
+
988
+ ### Wave 14 — discoveries
989
+
990
+ - **Speculative codemods are over-engineering; data-driven scope
991
+ is better.** The pre-data plan said three codemods (event-handler,
992
+ form-swap, click-swap). After running `deco-htmx-analyze` against
993
+ als-storefront's actual code, only the event-handler bucket
994
+ (88 occurrences, 42 % of the surface) genuinely admits a
995
+ mechanical rewrite. The other two buckets need per-call-site
996
+ product decisions (state machine vs sub-route, optimistic vs
997
+ pessimistic UI, response-handler shape) that a codemod cannot
998
+ encode without producing React skeletons that still need ~80 %
999
+ manual work — net negative versus the recipe in
1000
+ `references/htmx-rewrite.md`. **New rule: codemods come *after*
1001
+ the analyzer data, not before.**
1002
+ - **The smoke-against-real-site step is the design feedback loop.**
1003
+ Running the codemod against als's full 754-file tree (98
1004
+ renames, 71 files changed, 67 with TODO injection) validated
1005
+ three things in five minutes: (a) the rename surface matches
1006
+ the inventory (98 vs 88 ratio explained), (b) the TODO
1007
+ injection rate is high (94 %) — the marker is essential, not
1008
+ defensive, (c) the codemod is idempotent at scale (re-running
1009
+ produces zero diffs). Without this step we'd ship blind.
1010
+ - **The three-output codemod shape (rename + preserve body +
1011
+ conditional TODO) generalises.** Same shape any future
1012
+ per-pattern codemod should target: do the mechanical part,
1013
+ preserve the human-decision-required part, leave a single
1014
+ file-level marker the engineer can grep for. Over-eager
1015
+ body rewriting is what produces the ~80 % manual cleanup load
1016
+ that justifies leaving form-swap / click-swap codemods out for
1017
+ now.
1018
+ - **`htmx-residue` audit + W14-A codemod close another loop.**
1019
+ Same pattern as W12 (D3 throwing stubs + audit `--fix` for
1020
+ swap-able stubs). The codemod removes the easy half of the
1021
+ htmx surface; the audit catches the surviving half. Engineers
1022
+ can never accidentally ship a half-rewritten file: the
1023
+ attribute is either gone (codemod ran, body might still need
1024
+ work — TODO), or it's still there (audit fires in CI).
1025
+ - **als-storefront's profile probably generalises to other htmx
1026
+ sites.** 42 % event-handler is a strong skew toward
1027
+ trivially-mechanical rewrites; even if other sites differ,
1028
+ this codemod alone removes the largest single bucket. If a
1029
+ future site shows 80 % click-swap, *that* would be the cue to
1030
+ build the click-swap codemod — not pre-emptively now.
1031
+ - **Pipeline order matters.** Codemod runs after `transformJsx`
1032
+ (which renames `class` → `className` and `onInput` → `onChange`)
1033
+ and before `transformFreshApis` (which removes `useScript`
1034
+ imports). If `transformFreshApis` ran first, the codemod's
1035
+ TODO marker would still fire (we look for `useScript(` calls,
1036
+ not the import), but the import-removal would create dead
1037
+ references. Order is correct.
1038
+
1039
+ ### Wave 15-A (close template→audit loops + factories skill — Priority 2 follow-on) — 🟡 **IN FLIGHT**
1040
+
1041
+ Triggered by the double-check audit on 2026-05-01: subagent sweeps over
1042
+ casaevideo-storefront + baggagio-tanstack revealed (a) the migration
1043
+ template was scaffolding code that the audit's `--fix` then removed,
1044
+ and (b) the W12 factory hooks (`createUseUser`, `createUseWishlist`)
1045
+ had no skill coverage. Wave 15-A closes both loops in one PR.
1046
+
1047
+ **Shipped (one PR against `decocms/deco-start`):**
1048
+
1049
+ 37. `feat(migrate): close template→audit loops for vite plugins, runtime, cookies, branding + factories skill` 🟡 **WAITING ON CI**.
1050
+ - **`templates/vite-config.ts`** — drop `site-manual-chunks` and
1051
+ `deco-stub-meta-gen` plugin emissions. Both already live in
1052
+ `decoVitePlugin()` (`src/vite/plugin.js`). The audit's
1053
+ `obsolete-vite-plugins --fix` was undoing the template's own
1054
+ output; now the template emits clean, the audit catches
1055
+ regressions in legacy sites.
1056
+ - **`templates/server-entry.ts` `generateRuntime()`** — replace
1057
+ the 47-line inline `createNestedInvokeProxy` body with a 6-line
1058
+ re-export from `@decocms/start/sdk`. Sites keep
1059
+ `import { invoke } from "~/runtime"` and `Runtime.invoke`
1060
+ shapes. A2 of the original investigation finally lands at the
1061
+ template layer (was previously only patched post-migration by
1062
+ the audit).
1063
+ - **`templates/server-entry.ts` `generateInvoke()` (VTEX path)**
1064
+ — replace inline `mergeSetCookies` helper with
1065
+ `forwardResponseCookies` from
1066
+ `@decocms/start/sdk/cookiePassthrough`. The framework helper
1067
+ already shipped with try/catch for build-time safety.
1068
+ `getVtexCookies` stays inline (it's auth-specific filtering, not
1069
+ generic passthrough — see Wave 15-B/16 for full provider wiring
1070
+ under H1).
1071
+ - **`templates/routes.ts` + `templates/commerce-loaders.ts`** —
1072
+ replace casaevideo-specific branding leaks ("Tudo para sua
1073
+ casa…" tagline, "O melhor site de compras online…"
1074
+ `productListPageCollection` SEO description) with
1075
+ `${siteTitle}`-derived defaults plus `MIGRATION TODO` markers
1076
+ pointing at the per-site customization spot. CMS `Site.seo`
1077
+ overrides the defaults at runtime so leaving them visible in
1078
+ pre-resolution states is the safe behaviour.
1079
+ - **Audit rule expansion: `dead-runtime-shim`** — previously only
1080
+ flagged when exports were exactly `{ invoke }` or
1081
+ `{ invoke, createNestedInvokeProxy }`. Updated to detect (a)
1082
+ inline `createNestedInvokeProxy` body via regex (catches the
1083
+ legacy 47-line shape **with** `Runtime` export — which the old
1084
+ heuristic missed entirely; this is the shape every existing
1085
+ VTEX site has) and (b) skip the new Wave-15-A canonical
1086
+ re-export shape (where `import invoke from
1087
+ @decocms/start/sdk` is present and no inline proxy body
1088
+ exists). Auto-fix is gated by `safeToAutoFix` metadata: legacy
1089
+ shim shapes get the rewrite + delete; sites that mix the proxy
1090
+ with custom helpers get a warning only. Three new tests.
1091
+ **Verified against casaevideo-storefront**: now flags
1092
+ `[invoke, Runtime] inline createNestedInvokeProxy body` (was
1093
+ missed entirely before).
1094
+ - **Skill: `references/platform-hooks-factories.md`** — new
1095
+ canonical doc covering `createUseCart` / `createUseUser` /
1096
+ `createUseWishlist`. Replaces the pre-W12 manual approach of
1097
+ hand-rolling 200+ LOC of `createServerFn` wrappers per site.
1098
+ Documents the 5-line shim shape, why factories instead of
1099
+ direct hook imports (state isolation per site), non-VTEX
1100
+ stubs using `@decocms/start/sdk/signal`, and the migration
1101
+ path off the manual approach.
1102
+ - **Skill update: `references/platform-hooks/README.md`** —
1103
+ retained as legacy reference but now opens with a
1104
+ "deprecated, see canonical" header pointing at the new doc.
1105
+ The pre-W12 `createServerFn` examples are kept for sites that
1106
+ haven't migrated to factories yet.
1107
+ - **`SKILL.md` index update** — Phase 5 entry now points to the
1108
+ factories doc; reference table lists both new + legacy paths.
1109
+ - 342 → 345 tests pass, typecheck clean, smoke against casaevideo
1110
+ + baggagio confirms expanded rule fires correctly on legacy
1111
+ shapes and stays silent on baggagio (no `runtime.ts` file there).
1112
+
1113
+ **Deferred to Wave 15-B / 16 (intentionally — see discoveries journal):**
1114
+
1115
+ - **G3** — promote the 170-LOC `invoke.gen.ts` VTEX `createServerFn`
1116
+ wrappers into `@decocms/apps/vtex/server-fns`. Needs research on
1117
+ whether TanStack Start's compiler can transform `createServerFn`
1118
+ call sites that live inside a node_module. Not safe to ship blind.
1119
+ - **H1** — full cookie-passthrough provider wiring
1120
+ (`setRequestCookieProvider` / `setResponseCookieForwarder` in
1121
+ apps-start, auto-wire in `templates/setup.ts`). The
1122
+ `cookiePassthrough.ts` docstring already references this design but
1123
+ the apps-start side doesn't exist. Needs a design pass to scope
1124
+ what calls inside `vtex/utils/fetch.ts` need the provider hook
1125
+ (currently each call site forwards cookies manually).
1126
+ - **Cross-site convergence promotions** (5 items) — `useSuggestions`,
1127
+ `useOffer` factory, `Picture` API unification, redirect
1128
+ `useSendEvent`/`clx`/location-matcher imports, `relative()`
1129
+ SKU-stripping extension. Sequenced after 15-A merges.
843
1130
 
844
1131
  ### Wave 15+ (htmx cleanup PRs on als + propagation to other sites) — Priority 3 / 4
845
1132
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/start",
3
- "version": "2.21.0",
3
+ "version": "2.23.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",
@@ -9,6 +9,7 @@ import { transformFreshApis } from "./transforms/fresh-apis";
9
9
  import { transformDenoIsms } from "./transforms/deno-isms";
10
10
  import { transformTailwind } from "./transforms/tailwind";
11
11
  import { transformDeadCode } from "./transforms/dead-code";
12
+ import { transformHtmxOnEvents } from "./transforms/htmx-on-events";
12
13
  import { createSectionConventionsTransform } from "./transforms/section-conventions";
13
14
 
14
15
  /** Map of section path → metadata, populated per-run */
@@ -54,10 +55,15 @@ function applyTransforms(content: string, filePath: string, ctx?: MigrationConte
54
55
  return { content, changed: false, notes: [] };
55
56
  }
56
57
 
57
- // Pipeline: imports → jsx → fresh-apis → dead-code → deno-isms → tailwind
58
+ // Pipeline: imports → jsx → htmx-on-events → fresh-apis → dead-code → deno-isms → tailwind
59
+ // htmx-on-events runs after jsx (which renames class/onChange) and
60
+ // before fresh-apis (which removes useScript imports the htmx
61
+ // codemod's TODO might still reference). The codemod is a no-op on
62
+ // files without hx-on, so it never adds latency to non-htmx sites.
58
63
  const pipeline: Array<{ name: string; fn: (content: string) => TransformResult }> = [
59
64
  { name: "imports", fn: (c) => transformImports(c, ctx?.islandWrapperTargets) },
60
65
  { name: "jsx", fn: transformJsx },
66
+ { name: "htmx-on-events", fn: transformHtmxOnEvents },
61
67
  { name: "fresh-apis", fn: transformFreshApis },
62
68
  { name: "dead-code", fn: (c) => transformDeadCode(c, ctx?.platform) },
63
69
  { name: "deno-isms", fn: transformDenoIsms },
@@ -462,6 +462,31 @@ function findAttachedLeadingComments(content: string, openIdx: number): number {
462
462
  /* Rule 3 — dead `src/runtime.ts` invoke shim */
463
463
  /* ------------------------------------------------------------------ */
464
464
 
465
+ /**
466
+ * Detection covers two shapes of `src/runtime.ts`:
467
+ *
468
+ * 1. Legacy inline proxy (pre-Wave 15-A migration template) — defines
469
+ * `createNestedInvokeProxy` plus `invoke` and `Runtime` constants.
470
+ * The whole 40-50 LOC body duplicates `@decocms/start/sdk`'s `invoke`.
471
+ *
472
+ * 2. Simple re-export shim — the file only re-exports `invoke` /
473
+ * `createNestedInvokeProxy` (no inline proxy body, but also not yet
474
+ * pointing at `@decocms/start/sdk`).
475
+ *
476
+ * Both should be replaced with `import { invoke } from "@decocms/start/sdk"`
477
+ * at every callsite, and the file deleted. The Wave 15-A migration template
478
+ * scaffolds a thin re-export form that's also acceptable (re-exports
479
+ * `invoke` from `@decocms/start/sdk` and rebuilds `Runtime = { invoke }`);
480
+ * we explicitly skip it via the "imports invoke from @decocms/start/sdk
481
+ * AND no inline proxy" check below.
482
+ */
483
+ const INLINE_PROXY_RE =
484
+ /(?:function|const)\s+createNestedInvokeProxy\b|new\s+Proxy\s*\(\s*Object\.assign\s*\(\s*async\s*\(\s*props/;
485
+ const FRAMEWORK_INVOKE_IMPORT_RE =
486
+ /import\s+\{[^}]*\binvoke\b[^}]*\}\s+from\s+['"]@decocms\/start(?:\/sdk)?['"]/;
487
+
488
+ const ALLOWED_RUNTIME_EXPORTS = new Set(["invoke", "createNestedInvokeProxy", "Runtime"]);
489
+
465
490
  const ruleDeadRuntimeShim: Rule = {
466
491
  id: "dead-runtime-shim",
467
492
  title: "Dead src/runtime.ts invoke shim",
@@ -469,24 +494,54 @@ const ruleDeadRuntimeShim: Rule = {
469
494
  const abs = `${siteDir}/src/runtime.ts`;
470
495
  if (!fs.exists(abs)) return [];
471
496
  const content = fs.readText(abs);
472
- // Heuristic: if the file's only meaningful exports are `invoke` /
473
- // `createNestedInvokeProxy`, it's purely a shim.
497
+
498
+ const hasInlineProxy = INLINE_PROXY_RE.test(content);
499
+ const reExportsFromFramework = FRAMEWORK_INVOKE_IMPORT_RE.test(content);
474
500
  const exports = extractExports(content);
475
- const onlyInvokeShim =
476
- exports.length > 0 && exports.every((e) => ["invoke", "createNestedInvokeProxy"].includes(e));
477
- if (!onlyInvokeShim) return [];
501
+ const onlyKnownInvokeExports =
502
+ exports.length > 0 && exports.every((e) => ALLOWED_RUNTIME_EXPORTS.has(e));
503
+
504
+ // Wave 15-A canonical template: re-exports invoke from @decocms/start/sdk
505
+ // and exposes `Runtime = { invoke }` for legacy callers. No inline proxy
506
+ // body. This is the desired shape — skip.
507
+ if (reExportsFromFramework && !hasInlineProxy) return [];
508
+
509
+ // Site-specific helpers alongside invoke: don't flag — the file has its
510
+ // own purpose beyond shimming. (Old behavior preserved.)
511
+ if (!hasInlineProxy && !onlyKnownInvokeExports) return [];
512
+
513
+ const exportSummary = exports.length > 0 ? exports.join(", ") : "(re-exports only)";
514
+ const flavor = hasInlineProxy ? "inline createNestedInvokeProxy body" : "shim re-exports";
515
+ // Only safe to auto-delete when exports are pure invoke surface; if a
516
+ // legacy file mixes the inline proxy with custom helpers, we still flag
517
+ // it but skip the destructive fix.
518
+ const safeToAutoFix = onlyKnownInvokeExports;
519
+
478
520
  return [
479
521
  {
480
522
  rule: "dead-runtime-shim",
481
523
  severity: "info",
482
524
  file: "src/runtime.ts",
483
- message: `Only re-exports invoke (${exports.join(", ")}) — replace with @decocms/start/sdk`,
484
- fix: 'rg -l "from \\"~/runtime\\"" src/ | xargs sed -i \'\' \'s|from "~/runtime"|from "@decocms/start/sdk"|g\' && rm src/runtime.ts',
525
+ message: safeToAutoFix
526
+ ? `${flavor} [${exportSummary}] replace with @decocms/start/sdk`
527
+ : `${flavor} [${exportSummary}] — manual review: file mixes the runtime proxy with site-specific exports`,
528
+ fix: safeToAutoFix
529
+ ? 'rg -l "from \\"~/runtime\\"" src/ | xargs sed -i \'\' \'s|from "~/runtime"|from "@decocms/start/sdk"|g\' && rm src/runtime.ts'
530
+ : "Move the inline `createNestedInvokeProxy` body to call @decocms/start/sdk's `invoke`; relocate site-specific helpers to a dedicated module before deleting src/runtime.ts",
531
+ meta: {
532
+ hasInlineProxy,
533
+ exports,
534
+ safeToAutoFix,
535
+ },
485
536
  },
486
537
  ];
487
538
  },
488
539
  applyFix(ctx, findings, writer): FixAction[] {
489
540
  if (findings.length === 0) return [];
541
+ // Honor the per-finding safety gate emitted by run() — never auto-delete
542
+ // a runtime.ts that mixes the proxy with site-specific helpers.
543
+ const safe = findings.every((f) => f.meta?.safeToAutoFix !== false);
544
+ if (!safe) return [];
490
545
  const updated = rewriteImportSpec(ctx, writer, "~/runtime", "@decocms/start/sdk");
491
546
  writer.deleteFile(`${ctx.siteDir}/src/runtime.ts`);
492
547
  return [
@@ -227,9 +227,11 @@ describe("rule: dead-runtime-shim", () => {
227
227
  const r = report.rules.find((r) => r.rule === "dead-runtime-shim")!;
228
228
  expect(r.findings).toHaveLength(1);
229
229
  expect(r.findings[0].file).toBe("src/runtime.ts");
230
+ expect(r.findings[0].meta?.safeToAutoFix).toBe(true);
231
+ expect(r.findings[0].meta?.hasInlineProxy).toBe(true);
230
232
  });
231
233
 
232
- it("does not flag a runtime.ts that exports site-specific helpers", () => {
234
+ it("does not flag a runtime.ts that exports site-specific helpers (no inline proxy)", () => {
233
235
  const fs = makeFs({
234
236
  "/site/src/runtime.ts": "export const invoke = {};\nexport const customHelper = () => 1;\n",
235
237
  });
@@ -237,6 +239,80 @@ describe("rule: dead-runtime-shim", () => {
237
239
  const r = report.rules.find((r) => r.rule === "dead-runtime-shim")!;
238
240
  expect(r.findings).toEqual([]);
239
241
  });
242
+
243
+ it("does NOT flag the Wave 15-A canonical re-export shape", () => {
244
+ // The migration template now scaffolds a thin re-export from
245
+ // @decocms/start/sdk plus a Runtime alias. No inline proxy body.
246
+ const fs = makeFs({
247
+ "/site/src/runtime.ts":
248
+ 'import { invoke } from "@decocms/start/sdk";\nexport { invoke };\nexport const Runtime = { invoke };\n',
249
+ });
250
+ const report = runAudit(SITE, fs);
251
+ const r = report.rules.find((r) => r.rule === "dead-runtime-shim")!;
252
+ expect(r.findings).toEqual([]);
253
+ });
254
+
255
+ it("flags the legacy 47-line inline createNestedInvokeProxy body (with Runtime export)", () => {
256
+ // The pre-Wave-15-A migration template emitted a full Proxy body
257
+ // alongside `Runtime = { invoke }`. The earlier rule heuristic
258
+ // missed this shape because `Runtime` was not in its allowlist.
259
+ const fs = makeFs({
260
+ "/site/src/runtime.ts": `
261
+ function createNestedInvokeProxy(path: string[] = []): any {
262
+ return new Proxy(
263
+ Object.assign(async (props: any) => {
264
+ const key = path.join("/");
265
+ const response = await fetch(\`/deco/invoke/\${key}\`, {
266
+ method: "POST",
267
+ headers: { "Content-Type": "application/json" },
268
+ body: JSON.stringify(props ?? {}),
269
+ });
270
+ return response.json();
271
+ }, {}),
272
+ {
273
+ get(_target: any, prop: string) {
274
+ if (prop === "then") return undefined;
275
+ return createNestedInvokeProxy([...path, prop]);
276
+ },
277
+ },
278
+ );
279
+ }
280
+
281
+ export const invoke = createNestedInvokeProxy() as any;
282
+ export const Runtime = { invoke };
283
+ `,
284
+ });
285
+ const report = runAudit(SITE, fs);
286
+ const r = report.rules.find((r) => r.rule === "dead-runtime-shim")!;
287
+ expect(r.findings).toHaveLength(1);
288
+ expect(r.findings[0].meta?.hasInlineProxy).toBe(true);
289
+ expect(r.findings[0].meta?.safeToAutoFix).toBe(true);
290
+ expect(r.findings[0].message).toContain("inline createNestedInvokeProxy body");
291
+ });
292
+
293
+ it("flags but does NOT auto-fix when inline proxy coexists with site-specific helpers", () => {
294
+ // Defensive: if a site has hand-tuned the runtime file with extra
295
+ // exports beyond invoke/Runtime, deletion would lose data. Surface
296
+ // the issue but skip the destructive fix.
297
+ const fs = makeFs({
298
+ "/site/src/runtime.ts": `
299
+ function createNestedInvokeProxy(path: string[] = []): any {
300
+ return new Proxy(Object.assign(async (props: any) => {}, {}), {});
301
+ }
302
+ export const invoke = createNestedInvokeProxy();
303
+ export const trackPageView = () => console.log("custom tracker");
304
+ `,
305
+ });
306
+ const report = runAudit(SITE, fs);
307
+ const r = report.rules.find((r) => r.rule === "dead-runtime-shim")!;
308
+ expect(r.findings).toHaveLength(1);
309
+ expect(r.findings[0].meta?.hasInlineProxy).toBe(true);
310
+ expect(r.findings[0].meta?.safeToAutoFix).toBe(false);
311
+ expect(r.findings[0].message).toContain("manual review");
312
+ // applyFix should be a no-op when safeToAutoFix is false — verified
313
+ // implicitly by the runner test for --fix below; here we only
314
+ // assert the metadata gate.
315
+ });
240
316
  });
241
317
 
242
318
  describe("rule: site-local-with-globals", () => {
@@ -204,7 +204,8 @@ export function generateCommerceLoaders(ctx: MigrationContext): string {
204
204
  lines.push(` breadcrumb: createBreadcrumbFromPath(url.pathname, url, collection.name) ?? {},`);
205
205
  lines.push(` seo: {`);
206
206
  lines.push(` title: collection.name,`);
207
- lines.push(` description: "O melhor site de compras online para sua casa: compre itens de cozinha, móveis para sala e escritório, acessórios de tecnologia e mais. Clique já!",`);
207
+ lines.push(` // MIGRATION TODO: replace with site-specific category description`);
208
+ lines.push(` description: collection.name,`);
208
209
  lines.push(` noIndexing: false,`);
209
210
  lines.push(` canonical: url.toString(),`);
210
211
  lines.push(` },`);
@@ -67,8 +67,10 @@ import { DecoRootLayout } from "@decocms/start/hooks";
67
67
  // @ts-ignore Vite ?url import
68
68
  import appCss from "../styles/app.css?url";
69
69
 
70
- const DEFAULT_DESCRIPTION =
71
- "${siteTitle} - Tudo para sua casa com os melhores preços.";
70
+ // MIGRATION TODO: customize description, OG image, and locale for ${siteTitle}.
71
+ // The migration scaffold leaves a generic default so it never falls through;
72
+ // CMS \`Site.seo\` overrides this once block resolution kicks in.
73
+ const DEFAULT_DESCRIPTION = "${siteTitle}";
72
74
 
73
75
  export const Route = createRootRoute({
74
76
  head: () => ({
@@ -108,11 +110,14 @@ function generateIndex(ctx: MigrationContext, siteTitle: string): string {
108
110
  import { cmsHomeRouteConfig, deferredSectionLoader } from "@decocms/start/routes";
109
111
  import { DecoPageRenderer } from "@decocms/start/hooks";
110
112
 
113
+ // MIGRATION TODO: customize defaultTitle / defaultDescription / fallback
114
+ // copy below for ${siteTitle}. CMS \`Site.seo\` overrides these once block
115
+ // resolution kicks in, so leaving the migration scaffold defaults is safe
116
+ // but visible in pre-block-resolution states.
111
117
  export const Route = createFileRoute("/")({
112
118
  ...cmsHomeRouteConfig({
113
- defaultTitle: "${siteTitle} - Tudo para sua casa",
114
- defaultDescription:
115
- "${siteTitle} - Tudo para sua casa com os melhores preços.",
119
+ defaultTitle: "${siteTitle}",
120
+ defaultDescription: "${siteTitle}",
116
121
  siteName: "${siteTitle}",
117
122
  }),
118
123
  component: HomePage,
@@ -126,8 +131,7 @@ function HomePage() {
126
131
  <div className="min-h-screen flex items-center justify-center">
127
132
  <div className="text-center">
128
133
  <h1 className="text-4xl font-bold mb-4">${siteTitle}</h1>
129
- <p className="text-lg text-base-content/60">Tudo para sua casa</p>
130
- <p className="text-sm text-base-content/40 mt-2">Nenhuma página CMS encontrada para /</p>
134
+ <p className="text-sm text-base-content/40 mt-2">No CMS page registered for /</p>
131
135
  </div>
132
136
  </div>
133
137
  );
@@ -152,11 +156,12 @@ function generateCatchAll(ctx: MigrationContext, siteTitle: string): string {
152
156
  import { cmsRouteConfig, deferredSectionLoader } from "@decocms/start/routes";
153
157
  import { DecoPageRenderer } from "@decocms/start/hooks";
154
158
 
159
+ // MIGRATION TODO: customize defaultTitle / defaultDescription for ${siteTitle}
160
+ // (CMS \`Site.seo\` overrides these per-page once block resolution kicks in).
155
161
  const routeConfig = cmsRouteConfig({
156
162
  siteName: "${siteTitle}",
157
- defaultTitle: "${siteTitle} - Tudo para sua casa",
158
- defaultDescription:
159
- "${siteTitle} - Tudo para sua casa com os melhores preços.",
163
+ defaultTitle: "${siteTitle}",
164
+ defaultDescription: "${siteTitle}",
160
165
  ignoreSearchParams: ["skuId"],
161
166
  });
162
167