@decocms/blocks-cli 7.5.1 → 7.5.3

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 (43) hide show
  1. package/package.json +2 -2
  2. package/scripts/generate-invoke.ts +10 -0
  3. package/scripts/generate-schema.ts +46 -15
  4. package/scripts/generate-sections.test.ts +67 -0
  5. package/scripts/generate-sections.ts +16 -2
  6. package/scripts/lib/codegenExclusions.test.ts +16 -6
  7. package/scripts/lib/codegenExclusions.ts +13 -1
  8. package/scripts/migrate/analyzers/loader-inventory.ts +1 -1
  9. package/scripts/migrate/phase-analyze.ts +3 -3
  10. package/scripts/migrate/phase-cleanup.test.ts +70 -1
  11. package/scripts/migrate/phase-cleanup.ts +47 -26
  12. package/scripts/migrate/phase-report.ts +7 -7
  13. package/scripts/migrate/phase-scaffold.ts +4 -4
  14. package/scripts/migrate/phase-transform.ts +1 -1
  15. package/scripts/migrate/phase-verify.ts +6 -5
  16. package/scripts/migrate/post-cleanup/rules.ts +55 -44
  17. package/scripts/migrate/post-cleanup/runner.test.ts +58 -34
  18. package/scripts/migrate/templates/cache-config.ts +4 -4
  19. package/scripts/migrate/templates/commerce-loaders.ts +10 -10
  20. package/scripts/migrate/templates/cursor-rules.test.ts +6 -0
  21. package/scripts/migrate/templates/cursor-rules.ts +12 -11
  22. package/scripts/migrate/templates/hooks.test.ts +7 -7
  23. package/scripts/migrate/templates/hooks.ts +10 -10
  24. package/scripts/migrate/templates/lib-utils.test.ts +2 -2
  25. package/scripts/migrate/templates/lib-utils.ts +4 -4
  26. package/scripts/migrate/templates/no-legacy-packages.test.ts +147 -0
  27. package/scripts/migrate/templates/package-json.ts +30 -10
  28. package/scripts/migrate/templates/routes.ts +8 -10
  29. package/scripts/migrate/templates/sdk-gen.ts +1 -1
  30. package/scripts/migrate/templates/section-loaders.ts +7 -7
  31. package/scripts/migrate/templates/server-entry.ts +37 -37
  32. package/scripts/migrate/templates/setup.ts +6 -6
  33. package/scripts/migrate/templates/types-gen.ts +2 -2
  34. package/scripts/migrate/templates/ui-components.ts +2 -2
  35. package/scripts/migrate/templates/vite-config.ts +12 -3
  36. package/scripts/migrate/transforms/fresh-apis.ts +5 -5
  37. package/scripts/migrate/transforms/imports.test.ts +138 -0
  38. package/scripts/migrate/transforms/imports.ts +65 -43
  39. package/scripts/migrate/transforms/jsx.ts +8 -0
  40. package/scripts/migrate/transforms/section-conventions.ts +1 -1
  41. package/scripts/migrate/types.ts +1 -1
  42. package/scripts/migrate-post-cleanup.ts +8 -8
  43. package/scripts/migrate.ts +11 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@decocms/blocks-cli",
3
- "version": "7.5.1",
3
+ "version": "7.5.3",
4
4
  "type": "module",
5
5
  "description": "Deco codegen (generate-blocks, generate-schema, generate-invoke) and Fresh-to-TanStack migration tooling",
6
6
  "repository": {
@@ -34,7 +34,7 @@
34
34
  "lint:unused": "knip"
35
35
  },
36
36
  "dependencies": {
37
- "@decocms/blocks": "7.5.1",
37
+ "@decocms/blocks": "7.5.3",
38
38
  "ts-morph": "^27.0.0",
39
39
  "tsx": "^4.22.5"
40
40
  },
@@ -21,6 +21,16 @@
21
21
  * compiled and shipped as part of the site's application code, so it lives
22
22
  * in `src/` alongside the code it's compiled with, not in `.deco/`.
23
23
  *
24
+ * This is empirically load-bearing, not taste (tested 2026-07-08 on a real
25
+ * site): with the file moved to `.deco/invoke.gen.ts`, the CLIENT half of
26
+ * the Start compiler works fine (stubs generated, IDs re-derived from the
27
+ * new path), but the SERVER half cannot resolve the split module back to an
28
+ * executable handler — every `/_serverFn/...` call returns an unhandled 500
29
+ * with no logged stack, i.e. every cart/session/MasterData action dies.
30
+ * Same site, file back under `src/`: identical RPC probe returns 200 with a
31
+ * real VTEX orderForm. Do not move this default without re-running that
32
+ * round-trip test end to end.
33
+ *
24
34
  * Usage (from site root):
25
35
  * npx tsx node_modules/@decocms/blocks-cli/scripts/generate-invoke.ts
26
36
  *
@@ -40,6 +40,17 @@ import { warnLegacyArtifact } from "./lib/legacyArtifact";
40
40
 
41
41
  // ---------------------------------------------------------------------------
42
42
  // CLI arg parsing
43
+ //
44
+ // Guarded by isMainModule() (defined below — hoisted, so it's callable up
45
+ // here) so that importing this module for its pure exports
46
+ // (definitionIdForPath, applyWidgetFormat, typeToJsonSchema — see
47
+ // generate-schema.test.ts) never reads argv or touches the filesystem.
48
+ // Without the guard, every import used to run an `fs.existsSync` check
49
+ // against the *importing process's* cwd and could print a legacy-artifact
50
+ // warning to stderr as an import-time side effect, unrelated to whatever
51
+ // the test actually wanted to exercise. generateMeta() (below) and the
52
+ // final write are themselves only reached inside `if (isMainModule())`, so
53
+ // these vars only need real values in that same case.
43
54
  // ---------------------------------------------------------------------------
44
55
  const argv = process.argv.slice(2);
45
56
  function arg(name: string, fallback: string): string {
@@ -47,20 +58,33 @@ function arg(name: string, fallback: string): string {
47
58
  return idx !== -1 && argv[idx + 1] ? argv[idx + 1] : fallback;
48
59
  }
49
60
 
50
- const SITE_NAMESPACE = arg("namespace", "site");
51
- const SITE_NAME = arg("site", "storefront");
52
- const FRAMEWORK_VERSION = arg("version", "1.0.0");
53
- const SECTIONS_REL = arg("sections", "src/sections");
54
- const LOADERS_REL = arg("loaders", "src/loaders");
55
- const APPS_REL = arg("apps", "src/apps");
56
- const SKIP_APPS = argv.includes("--skip-apps");
57
- const OUT_FILE_EXPLICIT = argv.includes("--out");
58
61
  const NEW_DEFAULT_OUT_REL = ".deco/meta.gen.json";
59
62
  const OLD_DEFAULT_OUT_REL = "src/server/admin/meta.gen.json";
60
- const OUT_REL = arg("out", NEW_DEFAULT_OUT_REL);
61
- const PLATFORM = arg("platform", "cloudflare");
62
- if (!OUT_FILE_EXPLICIT && fs.existsSync(path.resolve(process.cwd(), OLD_DEFAULT_OUT_REL))) {
63
- warnLegacyArtifact(OLD_DEFAULT_OUT_REL, NEW_DEFAULT_OUT_REL);
63
+
64
+ let SITE_NAMESPACE = "site";
65
+ let SITE_NAME = "storefront";
66
+ let FRAMEWORK_VERSION = "1.0.0";
67
+ let SECTIONS_REL = "src/sections";
68
+ let LOADERS_REL = "src/loaders";
69
+ let APPS_REL = "src/apps";
70
+ let SKIP_APPS = false;
71
+ let OUT_REL = NEW_DEFAULT_OUT_REL;
72
+ let PLATFORM = "cloudflare";
73
+
74
+ if (isMainModule()) {
75
+ SITE_NAMESPACE = arg("namespace", SITE_NAMESPACE);
76
+ SITE_NAME = arg("site", SITE_NAME);
77
+ FRAMEWORK_VERSION = arg("version", FRAMEWORK_VERSION);
78
+ SECTIONS_REL = arg("sections", SECTIONS_REL);
79
+ LOADERS_REL = arg("loaders", LOADERS_REL);
80
+ APPS_REL = arg("apps", APPS_REL);
81
+ SKIP_APPS = argv.includes("--skip-apps");
82
+ const outFileExplicit = argv.includes("--out");
83
+ OUT_REL = arg("out", NEW_DEFAULT_OUT_REL);
84
+ PLATFORM = arg("platform", PLATFORM);
85
+ if (!outFileExplicit && fs.existsSync(path.resolve(process.cwd(), OLD_DEFAULT_OUT_REL))) {
86
+ warnLegacyArtifact(OLD_DEFAULT_OUT_REL, NEW_DEFAULT_OUT_REL);
87
+ }
64
88
  }
65
89
 
66
90
  // ---------------------------------------------------------------------------
@@ -758,10 +782,17 @@ function findTsxFiles(dir: string): string[] {
758
782
  const results: string[] = [];
759
783
  if (!fs.existsSync(dir)) return results;
760
784
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
761
- if (isExcludedCodegenFile(entry.name)) continue;
762
785
  const full = path.join(dir, entry.name);
763
- if (entry.isDirectory()) results.push(...findTsxFiles(full));
764
- else if (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts")) results.push(full);
786
+ if (entry.isDirectory()) {
787
+ // The exclusion predicate targets generated/test *files* — a directory
788
+ // named e.g. `foo.gen.ts` is a real path segment and must still be walked.
789
+ results.push(...findTsxFiles(full));
790
+ } else if (
791
+ !isExcludedCodegenFile(entry.name) &&
792
+ (entry.name.endsWith(".tsx") || entry.name.endsWith(".ts"))
793
+ ) {
794
+ results.push(full);
795
+ }
765
796
  }
766
797
  return results;
767
798
  }
@@ -258,4 +258,71 @@ describe("generate-sections --registry", () => {
258
258
  const importResult = cp.spawnSync("npx", ["tsx", checkerFile], { encoding: "utf8" });
259
259
  expect(importResult.status, importResult.stderr).toBe(0);
260
260
  }, 30_000);
261
+
262
+ it("ends with exactly one trailing newline and no doubled blank line before the registry block (output hygiene)", () => {
263
+ fs.writeFileSync(
264
+ path.join(sectionsDir, "Hero.tsx"),
265
+ "export const sync = true\nexport default function Hero() { return null }\n",
266
+ );
267
+
268
+ const { code } = runGenerator([
269
+ "--sections-dir", sectionsDir,
270
+ "--out-file", outFile,
271
+ "--registry",
272
+ ]);
273
+ expect(code).toBe(0);
274
+
275
+ const generated = fs.readFileSync(outFile, "utf-8");
276
+ expect(generated.endsWith("\n")).toBe(true);
277
+ expect(generated.endsWith("\n\n")).toBe(false);
278
+ // No run of 3+ consecutive newlines (i.e. no blank-line pair) anywhere,
279
+ // in particular right before the registry doc comment.
280
+ expect(generated).not.toMatch(/\n{3,}/);
281
+ }, 30_000);
282
+ });
283
+
284
+ describe("generate-sections output hygiene (non-registry)", () => {
285
+ let tmpDir: string;
286
+ let sectionsDir: string;
287
+ let outFile: string;
288
+
289
+ beforeEach(() => {
290
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "generate-sections-hygiene-"));
291
+ sectionsDir = path.join(tmpDir, "sections");
292
+ outFile = path.join(tmpDir, "out", "sections.gen.ts");
293
+ fs.mkdirSync(sectionsDir, { recursive: true });
294
+ });
295
+
296
+ afterEach(() => {
297
+ fs.rmSync(tmpDir, { recursive: true, force: true });
298
+ });
299
+
300
+ it("ends with exactly one trailing newline without --registry", () => {
301
+ fs.writeFileSync(
302
+ path.join(sectionsDir, "Hero.tsx"),
303
+ "export const eager = true;\nexport default function Hero() { return null; }\n",
304
+ );
305
+
306
+ const { code } = runGenerator(["--sections-dir", sectionsDir, "--out-file", outFile]);
307
+ expect(code).toBe(0);
308
+
309
+ const generated = fs.readFileSync(outFile, "utf-8");
310
+ expect(generated.endsWith("\n")).toBe(true);
311
+ expect(generated.endsWith("\n\n")).toBe(false);
312
+ expect(generated).not.toMatch(/\n{3,}/);
313
+ }, 30_000);
314
+
315
+ it("ends with exactly one trailing newline when the sections dir is missing (empty-output early-exit path)", () => {
316
+ const missingSectionsDir = path.join(tmpDir, "does-not-exist");
317
+
318
+ const { code } = runGenerator([
319
+ "--sections-dir", missingSectionsDir,
320
+ "--out-file", outFile,
321
+ ]);
322
+ expect(code).toBe(0);
323
+
324
+ const generated = fs.readFileSync(outFile, "utf-8");
325
+ expect(generated.endsWith("\n")).toBe(true);
326
+ expect(generated.endsWith("\n\n")).toBe(false);
327
+ }, 30_000);
261
328
  });
@@ -248,7 +248,10 @@ lines.push("");
248
248
  // Lazy section-import registry — opt-in via --registry, built from every
249
249
  // scanned section file (not just convention-carrying `entries`).
250
250
  if (EMIT_REGISTRY) {
251
- lines.push("");
251
+ // NOTE: no extra `lines.push("")` here — the loadingFallbacks block above
252
+ // already pushed a single trailing blank line as its separator. Pushing
253
+ // another one here doubled the blank line before this comment block in
254
+ // every site's committed .deco/sections.gen.ts.
252
255
  lines.push("/**");
253
256
  lines.push(" * Lazy section registry — the Next.js/webpack equivalent of Vite's");
254
257
  lines.push(" * `import.meta.glob` scan over every file under ./sections, recursively.");
@@ -265,7 +268,18 @@ if (EMIT_REGISTRY) {
265
268
  }
266
269
 
267
270
  fs.mkdirSync(path.dirname(outFile), { recursive: true });
268
- fs.writeFileSync(outFile, lines.join("\n"));
271
+ // Output hygiene: several sections above push a trailing "" separator
272
+ // unconditionally (e.g. after the header comment, after the sync/fallback
273
+ // import blocks) — when a following section has nothing to emit (no sync
274
+ // imports, --registry off, etc.) those separators stack into a doubled
275
+ // blank line. Collapse any run of blank lines down to a single one, then
276
+ // normalize to exactly one trailing newline: without --registry, `lines`
277
+ // ends with a pushed "" separator (one trailing "\n" once joined); with
278
+ // --registry, the last pushed line is "};" (no trailing newline at all).
279
+ fs.writeFileSync(
280
+ outFile,
281
+ lines.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n*$/, "\n"),
282
+ );
269
283
 
270
284
  console.log(
271
285
  `Generated section metadata for ${entries.length} sections → ${path.relative(process.cwd(), outFile)}`,
@@ -9,14 +9,24 @@ describe("isExcludedCodegenFile", () => {
9
9
  "Hero.stories.tsx",
10
10
  "sections.gen.ts",
11
11
  "meta.gen.json",
12
+ // Bare names (no prefix before the dot) must be excluded too.
13
+ "test.ts",
14
+ "test.tsx",
15
+ "spec.tsx",
16
+ "stories.ts",
17
+ "gen.ts",
12
18
  ])("excludes %s", (name) => {
13
19
  expect(isExcludedCodegenFile(name)).toBe(true);
14
20
  });
15
21
 
16
- it.each(["Hero.tsx", "Product/SearchResult.tsx", "testimonials.tsx", "generic.ts"])(
17
- "keeps %s",
18
- (name) => {
19
- expect(isExcludedCodegenFile(name)).toBe(false);
20
- },
21
- );
22
+ it.each([
23
+ "Hero.tsx",
24
+ "Product/SearchResult.tsx",
25
+ // Marker word embedded mid-identifier (not its own dot-delimited
26
+ // segment) must stay INCLUDED, even with the bare-name matching above.
27
+ "testimonials.tsx",
28
+ "generic.ts",
29
+ ])("keeps %s", (name) => {
30
+ expect(isExcludedCodegenFile(name)).toBe(false);
31
+ });
22
32
  });
@@ -3,8 +3,20 @@
3
3
  * `generate-schema.ts` once emitted a site's co-located test file as a
4
4
  * section block (it scans every .ts/.tsx under the sections dir), so both
5
5
  * generators route their directory walks through this predicate.
6
+ *
7
+ * Matches both the usual `<name>.test.ts` co-located form and a bare
8
+ * `test.ts` / `spec.tsx` / `stories.ts` / `gen.ts` file (no prefix before
9
+ * the dot) — `(?:^|\.)` anchors the marker word to either the start of the
10
+ * filename or a preceding dot, so `testimonials.tsx` / `generic.ts` (marker
11
+ * word embedded mid-identifier, not its own dot-delimited segment) are
12
+ * correctly left INCLUDED.
13
+ *
14
+ * IMPORTANT: only ever apply this predicate to file entries, not
15
+ * directories, during a directory walk — a directory named e.g.
16
+ * `foo.gen.ts` is a real (if unusual) path segment, not a generated file,
17
+ * and must still be descended into.
6
18
  */
7
- const EXCLUDED_SUFFIX_RE = /\.(test|spec|stories|gen)\.(ts|tsx|js|jsx|json)$/;
19
+ const EXCLUDED_SUFFIX_RE = /(?:^|\.)(test|spec|stories|gen)\.(ts|tsx|js|jsx|json)$/;
8
20
 
9
21
  export function isExcludedCodegenFile(fileName: string): boolean {
10
22
  return EXCLUDED_SUFFIX_RE.test(fileName);
@@ -3,7 +3,7 @@ import * as path from "node:path";
3
3
  import type { MigrationContext, LoaderInfo, Platform } from "../types";
4
4
  import { log } from "../types";
5
5
 
6
- /** Well-known loaders that map directly to @decocms/apps equivalents */
6
+ /** Well-known loaders that map directly to @decocms/apps-* equivalents */
7
7
  const APPS_EQUIVALENTS: Record<string, string> = {
8
8
  "loaders/availableIcons.ts": "", // deleted
9
9
  "loaders/icons.ts": "", // deleted
@@ -255,7 +255,7 @@ function decideAction(
255
255
  if (SDK_DELETE.has(relPath)) {
256
256
  return {
257
257
  action: "delete",
258
- notes: "Use framework equivalent from @decocms/start or @decocms/apps",
258
+ notes: "Use framework equivalent from @decocms/blocks or @decocms/apps-vtex",
259
259
  };
260
260
  }
261
261
 
@@ -263,13 +263,13 @@ function decideAction(
263
263
  if (COMPONENT_DELETE.has(relPath)) {
264
264
  return {
265
265
  action: "delete",
266
- notes: "Scaffolded fresh from @decocms/apps re-exports",
266
+ notes: "Scaffolded fresh from @decocms/blocks re-exports",
267
267
  };
268
268
  }
269
269
 
270
270
  // cart/ directory → delete
271
271
  if (relPath.startsWith("sdk/cart/")) {
272
- return { action: "delete", notes: "Use @decocms/apps cart hooks" };
272
+ return { action: "delete", notes: "Use @decocms/apps-vtex cart hooks" };
273
273
  }
274
274
 
275
275
  // Non-platform cleanup is done in analyze() post-processing (needs ctx.platform)
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
- import { writeImportedLibShims } from "./phase-cleanup";
5
+ import { upgradeSectionRenderer, writeImportedLibShims } from "./phase-cleanup";
6
6
  import type { MigrationContext } from "./types";
7
7
 
8
8
  /**
@@ -139,3 +139,72 @@ describe("writeImportedLibShims (integration)", () => {
139
139
  }
140
140
  });
141
141
  });
142
+
143
+ describe("upgradeSectionRenderer (integration)", () => {
144
+ let tmpDir: string;
145
+
146
+ beforeEach(() => {
147
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "render-section-test-"));
148
+ fs.mkdirSync(path.join(tmpDir, "src", "components"), { recursive: true });
149
+ });
150
+
151
+ afterEach(() => {
152
+ fs.rmSync(tmpDir, { recursive: true, force: true });
153
+ });
154
+
155
+ it(
156
+ "adds the missing RenderSection import when a .Component call is " +
157
+ "converted to JSX usage and no import exists yet (regression: the " +
158
+ "inner check used to always be false)",
159
+ () => {
160
+ const file = path.join(tmpDir, "src", "components", "Nested.tsx");
161
+ fs.writeFileSync(
162
+ file,
163
+ `export function Nested({ block }: { block: any }) {\n` +
164
+ ` return <block.Component {...block?.props} />;\n` +
165
+ `}\n`,
166
+ );
167
+
168
+ upgradeSectionRenderer(makeCtx(tmpDir));
169
+
170
+ const result = fs.readFileSync(file, "utf-8");
171
+ expect(result).toContain(
172
+ `import { RenderSection } from "@decocms/blocks/hooks";`,
173
+ );
174
+ expect(result).toContain(`<RenderSection section={block} />`);
175
+ // Import must appear before its first usage.
176
+ expect(result.indexOf("import { RenderSection }")).toBeLessThan(
177
+ result.indexOf("<RenderSection"),
178
+ );
179
+ },
180
+ );
181
+
182
+ it("does not duplicate the import when one already exists", () => {
183
+ const file = path.join(tmpDir, "src", "components", "Nested.tsx");
184
+ fs.writeFileSync(
185
+ file,
186
+ `import { RenderSection } from "@decocms/blocks/hooks";\n\n` +
187
+ `export function Nested({ block }: { block: any }) {\n` +
188
+ ` return <block.Component {...block?.props} />;\n` +
189
+ `}\n`,
190
+ );
191
+
192
+ upgradeSectionRenderer(makeCtx(tmpDir));
193
+
194
+ const result = fs.readFileSync(file, "utf-8");
195
+ const importCount = (
196
+ result.match(/import\s*\{\s*RenderSection\s*\}\s*from/g) ?? []
197
+ ).length;
198
+ expect(importCount).toBe(1);
199
+ });
200
+
201
+ it("does not touch files with no SectionRenderer/.Component usage", () => {
202
+ const file = path.join(tmpDir, "src", "components", "Plain.tsx");
203
+ const original = `export const Plain = () => <div>hi</div>;\n`;
204
+ fs.writeFileSync(file, original);
205
+
206
+ upgradeSectionRenderer(makeCtx(tmpDir));
207
+
208
+ expect(fs.readFileSync(file, "utf-8")).toBe(original);
209
+ });
210
+ });
@@ -963,7 +963,7 @@ function convertDirectComponentCalls(src: string, onFix: (msg: string) => void):
963
963
  * Also converts direct `<section.Component {...props}/>` patterns to use
964
964
  * RenderSection for robustness.
965
965
  */
966
- function upgradeSectionRenderer(ctx: MigrationContext) {
966
+ export function upgradeSectionRenderer(ctx: MigrationContext) {
967
967
  rewriteFilesRecursive(ctx, path.join(ctx.sourceDir, "src"), (content, relPath) => {
968
968
  if (!relPath.endsWith(".tsx") && !relPath.endsWith(".ts")) return null;
969
969
 
@@ -971,12 +971,22 @@ function upgradeSectionRenderer(ctx: MigrationContext) {
971
971
  let changed = false;
972
972
 
973
973
  // 1. Replace `import { SectionRenderer } from "@decocms/start/hooks"`
974
- // with `import { RenderSection } from "@decocms/start/hooks"`
974
+ // with `import { RenderSection } from "@decocms/blocks/hooks"`
975
+ // (RenderSection is a distinct component from tanstack's
976
+ // SectionRenderer — it lives in @decocms/blocks/hooks, not
977
+ // @decocms/tanstack.)
978
+ //
979
+ // HANDOFF MARKER: the "@decocms/start/hooks" specifier this matches
980
+ // is written earlier in the same migration run by
981
+ // transforms/jsx.ts (the `.Component`/`.props` → `<SectionRenderer>`
982
+ // rewrite), which deliberately emits that pre-split package path as
983
+ // a same-run handoff to this function. If the transform/cleanup
984
+ // phase order ever changes, this match target must move with it.
975
985
  const sectionRendererImport =
976
986
  /import\s*\{([^}]*)\bSectionRenderer\b([^}]*)\}\s*from\s*["']@decocms\/start\/hooks["']/g;
977
987
  const newContent = result.replace(sectionRendererImport, (_m, before, after) => {
978
988
  changed = true;
979
- return `import {${before}RenderSection${after}} from "@decocms/start/hooks"`;
989
+ return `import {${before}RenderSection${after}} from "@decocms/blocks/hooks"`;
980
990
  });
981
991
  if (newContent !== result) {
982
992
  result = newContent;
@@ -998,9 +1008,17 @@ function upgradeSectionRenderer(ctx: MigrationContext) {
998
1008
  log(ctx, ` ${msg}: src/${relPath}`);
999
1009
  });
1000
1010
 
1001
- // 4. Add RenderSection import if we introduced usages but no import exists
1002
- if (changed && result.includes("<RenderSection") && !result.includes("RenderSection")) {
1003
- result = `import { RenderSection } from "@decocms/start/hooks";\n` + result;
1011
+ // 4. Add RenderSection import if we introduced usages but no import exists.
1012
+ // Must check for the actual import statement, not just the substring
1013
+ // "RenderSection" every `<RenderSection` JSX usage also contains that
1014
+ // substring, so a naive `!result.includes("RenderSection")` check is
1015
+ // always false once step 2/3 above have run and this never fires.
1016
+ const hasRenderSectionImport =
1017
+ /import\s*\{[^}]*\bRenderSection\b[^}]*\}\s*from\s*["']@decocms\/blocks\/hooks["']/.test(
1018
+ result,
1019
+ );
1020
+ if (changed && result.includes("<RenderSection") && !hasRenderSectionImport) {
1021
+ result = `import { RenderSection } from "@decocms/blocks/hooks";\n` + result;
1004
1022
  }
1005
1023
 
1006
1024
  if (!changed) return null;
@@ -1013,16 +1031,18 @@ function upgradeSectionRenderer(ctx: MigrationContext) {
1013
1031
  *
1014
1032
  * Historical context:
1015
1033
  * Earlier versions of this script rewrote `@decocms/apps/vtex/utils/*` imports
1016
- * to local `~/lib/vtex-*` shims because those modules did not exist yet in
1017
- * `@decocms/apps`. They now do (apps-start exports `./vtex/utils/*` and
1018
- * `./vtex/client` directly), so any further rewrite would actively REGRESS
1019
- * working direct imports back into NO-OP shims and silently break runtime
1020
- * behavior on every migrated site.
1034
+ * (the pre-split Fresh/Deno site's original import shape) to local
1035
+ * `~/lib/vtex-*` shims because those modules did not exist yet as
1036
+ * directly-importable subpaths. They now do the post-7.x split
1037
+ * `@decocms/apps-vtex` package exports `./utils/*` and `./client`
1038
+ * directly so any further rewrite would actively REGRESS working direct
1039
+ * imports back into NO-OP shims and silently break runtime behavior on
1040
+ * every migrated site.
1021
1041
  *
1022
1042
  * Scope kept here:
1023
1043
  * - `@decocms/apps/vtex/loaders/intelligentSearch/*` → inline stubs
1024
- * (loaders moved under `inline-loaders/` in apps-start; this is still a
1025
- * real path-mismatch fixup)
1044
+ * (loaders moved under `inline-loaders/` in `@decocms/apps-vtex`; this
1045
+ * is still a real path-mismatch fixup)
1026
1046
  * - `LabelledFuzzy` type + `mapLabelledFuzzyToFuzzy` helper inlined when
1027
1047
  * `intelligentSearch/productListingPage` is imported
1028
1048
  * - `createHttpClient<Type>(...)` → `createHttpClient(...)` (Proxy handles types)
@@ -1034,12 +1054,13 @@ function upgradeSectionRenderer(ctx: MigrationContext) {
1034
1054
  * What was removed:
1035
1055
  * The four `@decocms/apps/vtex/utils/* → ~/lib/vtex-*` rewrites that the
1036
1056
  * first-pass `transforms/imports.ts` (lines 50-52) already produces in the
1037
- * correct, direct form. See discovery notes in MIGRATION_TOOLING_PLAN.md.
1057
+ * correct, direct `@decocms/apps-vtex/utils/*` form. See discovery notes
1058
+ * in MIGRATION_TOOLING_PLAN.md.
1038
1059
  */
1039
1060
  function rewriteVtexUtilImports(ctx: MigrationContext) {
1040
1061
  // Intentionally empty — see docstring. First-pass `transforms/imports.ts`
1041
1062
  // already maps `apps/vtex/utils/*` and `apps/vtex/client` directly to the
1042
- // `@decocms/apps` equivalents; rewriting them again here would dead-shim them.
1063
+ // `@decocms/apps-vtex` equivalents; rewriting them again here would dead-shim them.
1043
1064
  const importRewrites: Array<{ pattern: RegExp; replacement: string; desc: string }> = [];
1044
1065
 
1045
1066
  rewriteFilesRecursive(ctx, path.join(ctx.sourceDir, "src"), (content, relPath) => {
@@ -1117,12 +1138,12 @@ function rewriteVtexUtilImports(ctx: MigrationContext) {
1117
1138
  log(ctx, ` Replaced inline getISCookiesFromBag stub → ~/lib/vtex-intelligent-search: src/${relPath}`);
1118
1139
  }
1119
1140
 
1120
- // Rewrite ~/utils/retry → @decocms/start/sdk/retry
1141
+ // Rewrite ~/utils/retry → @decocms/blocks/sdk/retry
1121
1142
  const retryImport = /from\s+["']~\/utils\/retry["']/g;
1122
1143
  if (retryImport.test(result)) {
1123
- result = result.replace(retryImport, 'from "@decocms/start/sdk/retry"');
1144
+ result = result.replace(retryImport, 'from "@decocms/blocks/sdk/retry"');
1124
1145
  changed = true;
1125
- log(ctx, ` Rewrote retry import → @decocms/start/sdk/retry: src/${relPath}`);
1146
+ log(ctx, ` Rewrote retry import → @decocms/blocks/sdk/retry: src/${relPath}`);
1126
1147
  }
1127
1148
 
1128
1149
  // Rewrite type-only imports from productListingPage (Props type)
@@ -1301,7 +1322,7 @@ function moveRootConstantsToSrc(ctx: MigrationContext) {
1301
1322
  * with broken imports.
1302
1323
  *
1303
1324
  * New stack: the canonical Minicart contract + VTEX transform live in
1304
- * `@decocms/apps/vtex/inline-loaders/minicart`. We replace the loader with a
1325
+ * `@decocms/apps-vtex/inline-loaders/minicart`. We replace the loader with a
1305
1326
  * thin VTEX-only re-export. Sites on Shopify/VNDA/Wake/Linx/Nuvemshop are
1306
1327
  * not currently in production on the new stack — when one is, swap this for
1307
1328
  * a runtime dispatcher again or add a platform-flagged rewrite.
@@ -1325,19 +1346,19 @@ function rewriteMinicartLoader(ctx: MigrationContext) {
1325
1346
  //
1326
1347
  // The legacy site shipped per-platform loaders behind a \`usePlatform()\`
1327
1348
  // switch (vnda, wake, linx, shopify, nuvemshop). The canonical minicart
1328
- // contract now lives in \`@decocms/apps\`. Until a non-VTEX customer comes
1349
+ // contract now lives in \`@decocms/apps-vtex\`. Until a non-VTEX customer comes
1329
1350
  // online on the new stack, we re-export the framework loader directly.
1330
1351
  // TODO: when adding another platform, replace this with a runtime
1331
1352
  // dispatcher and import the matching framework loader.
1332
- export { default } from "@decocms/apps/vtex/inline-loaders/minicart";
1333
- export type { MinicartProps } from "@decocms/apps/vtex/inline-loaders/minicart";
1353
+ export { default } from "@decocms/apps-vtex/inline-loaders/minicart";
1354
+ export type { MinicartProps } from "@decocms/apps-vtex/inline-loaders/minicart";
1334
1355
  `;
1335
1356
 
1336
1357
  if (ctx.dryRun) {
1337
1358
  log(ctx, `[DRY] Would rewrite: ${path.relative(ctx.sourceDir, filePath)} (VTEX-only re-export)`);
1338
1359
  } else {
1339
1360
  fs.writeFileSync(filePath, newContent);
1340
- log(ctx, `Rewrote: ${path.relative(ctx.sourceDir, filePath)} (VTEX-only re-export of @decocms/apps/vtex/inline-loaders/minicart)`);
1361
+ log(ctx, `Rewrote: ${path.relative(ctx.sourceDir, filePath)} (VTEX-only re-export of @decocms/apps-vtex/inline-loaders/minicart)`);
1341
1362
  }
1342
1363
  }
1343
1364
  }
@@ -1422,7 +1443,7 @@ export function cleanup(ctx: MigrationContext): void {
1422
1443
 
1423
1444
  // 5. Override contexts/device.tsx with SSR-safe useSyncExternalStore version.
1424
1445
  // The transform phase copies and transforms the source file (createContext-based),
1425
- // but @decocms/start shell-renders sections without a Device.Provider, so we
1446
+ // but @decocms/tanstack shell-renders sections without a Device.Provider, so we
1426
1447
  // must replace it with a standalone implementation.
1427
1448
  console.log(" Overriding contexts/device.tsx...");
1428
1449
  overrideDeviceContext(ctx);
@@ -1452,7 +1473,7 @@ export function cleanup(ctx: MigrationContext): void {
1452
1473
 
1453
1474
  // 11. Rewrite VTEX utility imports to use ~/lib/ wrappers
1454
1475
  // The old stack imports from apps/vtex/utils/* which get rewritten to
1455
- // @decocms/apps/vtex/utils/* — but the signatures are incompatible
1476
+ // @decocms/apps-vtex/utils/* — but the signatures are incompatible
1456
1477
  // and some types (VTEXCommerceStable) don't exist. Replace with
1457
1478
  // simplified ~/lib/ wrappers generated during scaffold.
1458
1479
  console.log(" Rewriting VTEX utility imports → ~/lib/ wrappers...");
@@ -1467,7 +1488,7 @@ export function cleanup(ctx: MigrationContext): void {
1467
1488
  // 11b. Rewrite legacy multi-platform minicart loader → VTEX-only re-export.
1468
1489
  // `sdk/cart/` is deleted by DIRS_TO_DELETE, leaving loaders/minicart.ts
1469
1490
  // with broken imports. Replace it with a thin re-export of the
1470
- // framework's @decocms/apps/vtex/inline-loaders/minicart loader.
1491
+ // framework's @decocms/apps-vtex/inline-loaders/minicart loader.
1471
1492
  console.log(" Rewriting loaders/minicart.ts → VTEX-only re-export...");
1472
1493
  rewriteMinicartLoader(ctx);
1473
1494
 
@@ -5,11 +5,11 @@ import { logPhase } from "./types";
5
5
 
6
6
  const FRAMEWORK_FINDINGS = [
7
7
  "Session/analytics SDK is boilerplate duplicated across all sites — should be a single framework function",
8
- "GTM event system (useGTMEvent, data-gtm-* listeners) is universal pattern — should be in @decocms/start",
8
+ "GTM event system (useGTMEvent, data-gtm-* listeners) is universal pattern — should be in @decocms/blocks",
9
9
  "Route files (__root.tsx, index.tsx, $.tsx, deco/*) are near-identical across sites — should be generated by framework",
10
10
  "server.ts, worker-entry.ts, router.tsx are pure boilerplate — should be a single createSite() call",
11
11
  "setup.ts section registration via import.meta.glob is 100% boilerplate — framework should handle this",
12
- "runtime.ts invoke proxy is identical across sites — already in @decocms/start but sites still have local copies",
12
+ "runtime.ts invoke proxy is identical across sites — already in @decocms/blocks/sdk/invoke but sites still have local copies",
13
13
  "apps/site.ts is mostly empty after migration — platform config should be in a config file, not code",
14
14
  ];
15
15
 
@@ -124,7 +124,7 @@ export function report(ctx: MigrationContext): void {
124
124
  lines.push("## Loader Inventory");
125
125
  lines.push("");
126
126
  lines.push(`- **${ctx.loaderInventory.length}** loaders inventoried`);
127
- lines.push(`- **${mapped}** mapped to \`@decocms/apps\` equivalents`);
127
+ lines.push(`- **${mapped}** mapped to \`@decocms/apps-*\` equivalents`);
128
128
  lines.push(`- **${custom}** custom (included in \`setup/commerce-loaders.ts\`)`);
129
129
  lines.push("");
130
130
  }
@@ -175,7 +175,7 @@ export function report(ctx: MigrationContext): void {
175
175
  lines.push("## Framework Findings");
176
176
  lines.push("");
177
177
  lines.push(
178
- "> These are patterns found during migration that should eventually be handled by `@decocms/start` instead of being duplicated in every site.",
178
+ "> These are patterns found during migration that should eventually be handled by `@decocms/blocks` instead of being duplicated in every site.",
179
179
  );
180
180
  lines.push("");
181
181
  for (const finding of ctx.frameworkFindings) {
@@ -215,11 +215,11 @@ export function report(ctx: MigrationContext): void {
215
215
  lines.push("#");
216
216
  lines.push("# Forwarding to a future ClickHouse-backed OTel collector is on the");
217
217
  lines.push("# roadmap (placeholder lives at");
218
- lines.push("# @decocms/start/sdk/otelAdapters/clickhouseCollector); when it");
218
+ lines.push("# @decocms/blocks/sdk/otelAdapters/clickhouseCollector); when it");
219
219
  lines.push("# ships, the codemod gains a `--destination-logs` /");
220
220
  lines.push("# `--destination-traces` flag to point at it.");
221
- lines.push("npx -p @decocms/start deco-cf-observability # dry-run");
222
- lines.push("npx -p @decocms/start deco-cf-observability --write # apply");
221
+ lines.push("npx -p @decocms/blocks-cli deco-cf-observability # dry-run");
222
+ lines.push("npx -p @decocms/blocks-cli deco-cf-observability --write # apply");
223
223
  lines.push("```");
224
224
  lines.push("");
225
225
 
@@ -135,7 +135,7 @@ export function scaffold(ctx: MigrationContext): void {
135
135
  // some file actually imports. See `writeImportedLibShims` in phase-cleanup.
136
136
 
137
137
  // Replace Context-based useDevice with SSR-safe useSyncExternalStore version.
138
- // @decocms/start shell-renders sections in a separate React root without
138
+ // @decocms/tanstack shell-renders sections in a separate React root without
139
139
  // Device.Provider, so the old createContext pattern throws during SSR.
140
140
  writeFile(ctx, "src/contexts/device.tsx", generateDeviceContext());
141
141
 
@@ -312,7 +312,7 @@ export default debounce;
312
312
 
313
313
  function generateSignalShim(): string {
314
314
  return `import { useState, useRef, useEffect, useMemo, useCallback } from "react";
315
- export { signal, type ReactiveSignal } from "@decocms/start/sdk/signal";
315
+ export { signal, type ReactiveSignal } from "@decocms/blocks/sdk/signal";
316
316
 
317
317
  /** Run a function immediately. Kept for legacy module-level side effects. */
318
318
  export function effect(fn: () => void | (() => void)): () => void {
@@ -535,8 +535,8 @@ function generateLocationMatcher(): string {
535
535
  * rules server-side.
536
536
  */
537
537
 
538
- import { registerMatcher } from "@decocms/start/cms";
539
- import type { MatcherContext } from "@decocms/start/cms";
538
+ import { registerMatcher } from "@decocms/blocks/cms";
539
+ import type { MatcherContext } from "@decocms/blocks/cms";
540
540
 
541
541
  const COUNTRY_NAME_TO_CODE: Record<string, string> = {
542
542
  Brasil: "BR",