@decocms/start 2.4.2 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.agents/skills/deco-to-tanstack-migration/SKILL.md +1 -0
  2. package/.agents/skills/deco-to-tanstack-migration/references/post-migration-cleanup.md +163 -0
  3. package/.agents/skills/deco-to-tanstack-migration/references/vite-config/README.md +27 -2
  4. package/.agents/skills/deco-to-tanstack-migration/templates/vite-config.md +143 -68
  5. package/.cursor/skills/deco-to-tanstack-migration/SKILL.md +1 -0
  6. package/.cursor/skills/deco-to-tanstack-migration/references/post-migration-cleanup.md +163 -0
  7. package/.cursor/skills/deco-to-tanstack-migration/references/vite-config/README.md +27 -2
  8. package/.cursor/skills/deco-to-tanstack-migration/templates/vite-config.md +143 -68
  9. package/package.json +1 -1
  10. package/scripts/deco-migrate-cli.ts +1 -1
  11. package/scripts/migrate/analyzers/island-classifier.ts +2 -2
  12. package/scripts/migrate/analyzers/loader-inventory.ts +2 -2
  13. package/scripts/migrate/analyzers/section-metadata.ts +2 -2
  14. package/scripts/migrate/analyzers/theme-extractor.ts +2 -2
  15. package/scripts/migrate/phase-analyze.ts +6 -6
  16. package/scripts/migrate/phase-cleanup.test.ts +141 -0
  17. package/scripts/migrate/phase-cleanup.ts +134 -35
  18. package/scripts/migrate/phase-report.ts +2 -2
  19. package/scripts/migrate/phase-scaffold.ts +26 -22
  20. package/scripts/migrate/phase-transform.ts +9 -9
  21. package/scripts/migrate/phase-verify.ts +2 -2
  22. package/scripts/migrate/templates/app-css.ts +2 -2
  23. package/scripts/migrate/templates/cache-config.ts +1 -1
  24. package/scripts/migrate/templates/commerce-loaders.ts +1 -1
  25. package/scripts/migrate/templates/hooks.ts +1 -1
  26. package/scripts/migrate/templates/lib-utils.test.ts +91 -0
  27. package/scripts/migrate/templates/lib-utils.ts +51 -19
  28. package/scripts/migrate/templates/package-json.ts +1 -1
  29. package/scripts/migrate/templates/routes.ts +1 -1
  30. package/scripts/migrate/templates/sdk-gen.ts +1 -1
  31. package/scripts/migrate/templates/section-loaders.ts +1 -1
  32. package/scripts/migrate/templates/server-entry.ts +1 -1
  33. package/scripts/migrate/templates/setup.ts +1 -1
  34. package/scripts/migrate/templates/types-gen.ts +1 -1
  35. package/scripts/migrate/templates/ui-components.ts +1 -1
  36. package/scripts/migrate/templates/vite-config.ts +1 -1
  37. package/scripts/migrate/templates/wrangler.ts +1 -1
  38. package/scripts/migrate/transforms/dead-code.ts +1 -1
  39. package/scripts/migrate/transforms/deno-isms.ts +1 -1
  40. package/scripts/migrate/transforms/fresh-apis.ts +1 -1
  41. package/scripts/migrate/transforms/imports.ts +1 -1
  42. package/scripts/migrate/transforms/jsx.ts +1 -1
  43. package/scripts/migrate/transforms/section-conventions.ts +1 -1
  44. package/scripts/migrate/transforms/tailwind.ts +1 -1
  45. package/scripts/migrate.ts +8 -8
  46. package/src/cms/sectionLoaders.test.ts +4 -1
  47. package/src/vite/plugin.js +47 -10
  48. package/vitest.config.ts +11 -1
@@ -1,7 +1,11 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import type { MigrationContext } from "./types.ts";
4
- import { log, logPhase } from "./types.ts";
3
+ import type { MigrationContext } from "./types";
4
+ import { log, logPhase } from "./types";
5
+ import {
6
+ LIB_TEMPLATES,
7
+ selectImportedLibTemplates,
8
+ } from "./templates/lib-utils";
5
9
 
6
10
  /** Directories to remove entirely after migration */
7
11
  const DIRS_TO_DELETE = [
@@ -1005,42 +1009,38 @@ function upgradeSectionRenderer(ctx: MigrationContext) {
1005
1009
  }
1006
1010
 
1007
1011
  /**
1008
- * Rewrite imports from @decocms/apps/vtex/utils/* and other non-existent modules
1009
- * to use the simplified ~/lib/ wrappers generated during scaffold.
1012
+ * Cleanup pass for VTEX-style transform / signature workarounds.
1010
1013
  *
1011
- * This handles:
1012
- * - @decocms/apps/vtex/utils/transform → ~/lib/vtex-transform
1013
- * - @decocms/apps/vtex/utils/intelligentSearch ~/lib/vtex-intelligent-search
1014
- * - @decocms/apps/vtex/utils/segment → ~/lib/vtex-segment
1015
- * - @decocms/apps/vtex/client (VTEXCommerceStable) ~/lib/vtex-client
1016
- * - @decocms/apps/vtex/loaders/intelligentSearch/* inline stubs
1017
- * - createHttpClient from various sources → ~/lib/http-utils
1018
- * - STALE constant → ~/lib/fetch-utils
1019
- * - Typed HTTP client patterns → simplified fetch
1014
+ * Historical context:
1015
+ * 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.
1021
+ *
1022
+ * Scope kept here:
1023
+ * - `@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)
1026
+ * - `LabelledFuzzy` type + `mapLabelledFuzzyToFuzzy` helper inlined when
1027
+ * `intelligentSearch/productListingPage` is imported
1028
+ * - `createHttpClient<Type>(...)` → `createHttpClient(...)` (Proxy handles types)
1029
+ * - `fetcher: fetchSafe,` parameter strip (Proxy uses native fetch)
1030
+ * - Inline stub hoisting: when a Fresh loader declares
1031
+ * `const getSegmentFromBag = ... = ({})` etc, hoist them into imports from
1032
+ * the corresponding `~/lib/*` shim so multiple loaders stay in sync
1033
+ *
1034
+ * What was removed:
1035
+ * The four `@decocms/apps/vtex/utils/* → ~/lib/vtex-*` rewrites that the
1036
+ * first-pass `transforms/imports.ts` (lines 50-52) already produces in the
1037
+ * correct, direct form. See discovery notes in MIGRATION_TOOLING_PLAN.md.
1020
1038
  */
1021
1039
  function rewriteVtexUtilImports(ctx: MigrationContext) {
1022
- const importRewrites: Array<{ pattern: RegExp; replacement: string; desc: string }> = [
1023
- {
1024
- pattern: /from\s+["']@decocms\/apps\/vtex\/utils\/transform["']/g,
1025
- replacement: 'from "~/lib/vtex-transform"',
1026
- desc: "vtex/utils/transform → ~/lib/vtex-transform",
1027
- },
1028
- {
1029
- pattern: /from\s+["']@decocms\/apps\/vtex\/utils\/intelligentSearch["']/g,
1030
- replacement: 'from "~/lib/vtex-intelligent-search"',
1031
- desc: "vtex/utils/intelligentSearch → ~/lib/vtex-intelligent-search",
1032
- },
1033
- {
1034
- pattern: /from\s+["']@decocms\/apps\/vtex\/utils\/segment["']/g,
1035
- replacement: 'from "~/lib/vtex-segment"',
1036
- desc: "vtex/utils/segment → ~/lib/vtex-segment",
1037
- },
1038
- {
1039
- pattern: /from\s+["']@decocms\/apps\/vtex\/client["']/g,
1040
- replacement: 'from "~/lib/vtex-client"',
1041
- desc: "vtex/client → ~/lib/vtex-client",
1042
- },
1043
- ];
1040
+ // Intentionally empty see docstring. First-pass `transforms/imports.ts`
1041
+ // already maps `apps/vtex/utils/*` and `apps/vtex/client` directly to the
1042
+ // `@decocms/apps` equivalents; rewriting them again here would dead-shim them.
1043
+ const importRewrites: Array<{ pattern: RegExp; replacement: string; desc: string }> = [];
1044
1044
 
1045
1045
  rewriteFilesRecursive(ctx, path.join(ctx.sourceDir, "src"), (content, relPath) => {
1046
1046
  if (!relPath.endsWith(".tsx") && !relPath.endsWith(".ts")) return null;
@@ -1483,7 +1483,106 @@ export function cleanup(ctx: MigrationContext): void {
1483
1483
  console.log(" Fixing Worker-incompatible APIs...");
1484
1484
  fixWorkerIncompatibleApis(ctx);
1485
1485
 
1486
+ // 14. LAZY: write only the src/lib/* shim files that the migrated
1487
+ // codebase actually imports. Run after every previous step so we
1488
+ // observe the final import graph (transforms + cleanup rewrites
1489
+ // + inline-stub hoisting all settled).
1490
+ console.log(" Writing src/lib/* shims (lazy)...");
1491
+ writeImportedLibShims(ctx);
1492
+
1486
1493
  console.log(
1487
1494
  ` Deleted ${ctx.deletedFiles.length} files/dirs, moved ${ctx.movedFiles.length} files`,
1488
1495
  );
1489
1496
  }
1497
+
1498
+ /**
1499
+ * Walk `src/**\/*.{ts,tsx}` looking for `from "~/lib/<name>"` imports.
1500
+ * For each unique `<name>` that has a registered template in
1501
+ * `LIB_TEMPLATES`, write `src/lib/<name>.ts` with the template content.
1502
+ *
1503
+ * Sites that import none of the shims end up with no `src/lib/` directory
1504
+ * at all, instead of 11 dead files.
1505
+ *
1506
+ * Exported (rather than file-local) for unit testability — see
1507
+ * `phase-cleanup.test.ts`.
1508
+ */
1509
+ export function writeImportedLibShims(ctx: MigrationContext): void {
1510
+ const srcRoot = path.join(ctx.sourceDir, "src");
1511
+ if (!fs.existsSync(srcRoot)) return;
1512
+
1513
+ const importedSpecifiers = new Set<string>();
1514
+ // Match `from "~/lib/<name>"` or `from '~/lib/<name>'`. We don't care
1515
+ // about default vs named imports here — only the path matters.
1516
+ const importRe = /from\s+["']~\/lib\/([^"']+?)(?:\.ts)?["']/g;
1517
+
1518
+ /** Recursively scan a directory for .ts/.tsx files and collect specifiers. */
1519
+ const walk = (dir: string) => {
1520
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
1521
+ const full = path.join(dir, entry.name);
1522
+ if (entry.isDirectory()) {
1523
+ // Skip the lib dir itself so we don't count internal cross-refs
1524
+ // when we later add them.
1525
+ if (full === path.join(srcRoot, "lib")) continue;
1526
+ walk(full);
1527
+ continue;
1528
+ }
1529
+ if (!entry.isFile()) continue;
1530
+ if (!/\.(ts|tsx)$/.test(entry.name)) continue;
1531
+ const content = fs.readFileSync(full, "utf-8");
1532
+ let m: RegExpExecArray | null;
1533
+ while ((m = importRe.exec(content)) !== null) {
1534
+ importedSpecifiers.add(m[1]);
1535
+ }
1536
+ }
1537
+ };
1538
+ walk(srcRoot);
1539
+
1540
+ if (importedSpecifiers.size === 0) {
1541
+ log(ctx, " No ~/lib/* imports detected — no shims written.");
1542
+ return;
1543
+ }
1544
+
1545
+ const known = new Set(
1546
+ Object.keys(LIB_TEMPLATES).map((k) =>
1547
+ k.replace(/^src\/lib\//, "").replace(/\.ts$/, ""),
1548
+ ),
1549
+ );
1550
+ const unknown: string[] = [];
1551
+ for (const spec of importedSpecifiers) {
1552
+ if (!known.has(spec)) unknown.push(spec);
1553
+ }
1554
+ if (unknown.length > 0) {
1555
+ log(
1556
+ ctx,
1557
+ ` Warning: ~/lib/{${unknown.join(", ")}} imported but no template registered. ` +
1558
+ `Write the file by hand or add a template to scripts/migrate/templates/lib-utils.ts.`,
1559
+ );
1560
+ }
1561
+
1562
+ const toWrite = selectImportedLibTemplates(importedSpecifiers);
1563
+ if (Object.keys(toWrite).length === 0) return;
1564
+
1565
+ if (ctx.dryRun) {
1566
+ for (const relPath of Object.keys(toWrite)) {
1567
+ log(ctx, ` [DRY] Would write: ${relPath}`);
1568
+ }
1569
+ return;
1570
+ }
1571
+
1572
+ const libDir = path.join(srcRoot, "lib");
1573
+ if (!fs.existsSync(libDir)) {
1574
+ fs.mkdirSync(libDir, { recursive: true });
1575
+ }
1576
+
1577
+ for (const [relPath, content] of Object.entries(toWrite)) {
1578
+ const fullPath = path.join(ctx.sourceDir, relPath);
1579
+ fs.writeFileSync(fullPath, content, "utf-8");
1580
+ log(ctx, ` Wrote: ${relPath}`);
1581
+ }
1582
+
1583
+ log(
1584
+ ctx,
1585
+ ` Generated ${Object.keys(toWrite).length} src/lib/* shim(s) ` +
1586
+ `(out of ${Object.keys(LIB_TEMPLATES).length} available templates).`,
1587
+ );
1588
+ }
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import type { MigrationContext } from "./types.ts";
4
- import { logPhase } from "./types.ts";
3
+ import type { MigrationContext } from "./types";
4
+ 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",
@@ -1,25 +1,27 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import type { MigrationContext } from "./types.ts";
4
- import { log, logPhase } from "./types.ts";
5
- import { generatePackageJson } from "./templates/package-json.ts";
6
- import { generateTsconfig } from "./templates/tsconfig.ts";
7
- import { generateViteConfig } from "./templates/vite-config.ts";
8
- import { generateWrangler } from "./templates/wrangler.ts";
9
- import { generateKnipConfig } from "./templates/knip-config.ts";
10
- import { generateRoutes } from "./templates/routes.ts";
11
- import { generateSetup } from "./templates/setup.ts";
12
- import { generateServerEntry } from "./templates/server-entry.ts";
13
- import { generateAppCss } from "./templates/app-css.ts";
14
- import { generateTypeFiles } from "./templates/types-gen.ts";
15
- import { generateUiComponents } from "./templates/ui-components.ts";
16
- import { generateHooks } from "./templates/hooks.ts";
17
- import { generateCommerceLoaders } from "./templates/commerce-loaders.ts";
18
- import { generateSectionLoaders } from "./templates/section-loaders.ts";
19
- import { generateCacheConfig } from "./templates/cache-config.ts";
20
- import { generateSdkFiles } from "./templates/sdk-gen.ts";
21
- import { generateLibUtils } from "./templates/lib-utils.ts";
22
- import { extractTheme } from "./analyzers/theme-extractor.ts";
3
+ import type { MigrationContext } from "./types";
4
+ import { log, logPhase } from "./types";
5
+ import { generatePackageJson } from "./templates/package-json";
6
+ import { generateTsconfig } from "./templates/tsconfig";
7
+ import { generateViteConfig } from "./templates/vite-config";
8
+ import { generateWrangler } from "./templates/wrangler";
9
+ import { generateKnipConfig } from "./templates/knip-config";
10
+ import { generateRoutes } from "./templates/routes";
11
+ import { generateSetup } from "./templates/setup";
12
+ import { generateServerEntry } from "./templates/server-entry";
13
+ import { generateAppCss } from "./templates/app-css";
14
+ import { generateTypeFiles } from "./templates/types-gen";
15
+ import { generateUiComponents } from "./templates/ui-components";
16
+ import { generateHooks } from "./templates/hooks";
17
+ import { generateCommerceLoaders } from "./templates/commerce-loaders";
18
+ import { generateSectionLoaders } from "./templates/section-loaders";
19
+ import { generateCacheConfig } from "./templates/cache-config";
20
+ import { generateSdkFiles } from "./templates/sdk-gen";
21
+ // `lib-utils` is imported lazily — see end of phase-cleanup. Eager
22
+ // generation of all 11 shims left every site with dead code that had
23
+ // to be cleaned up by hand.
24
+ import { extractTheme } from "./analyzers/theme-extractor";
23
25
 
24
26
  function writeFile(ctx: MigrationContext, relPath: string, content: string) {
25
27
  const fullPath = path.join(ctx.sourceDir, relPath);
@@ -108,8 +110,10 @@ export function scaffold(ctx: MigrationContext): void {
108
110
  writeFile(ctx, "src/sdk/logger.ts", generateLoggerStub());
109
111
  writeMultiFile(ctx, generateSdkFiles(ctx));
110
112
 
111
- // VTEX utility wrappers (signature-compatible stubs for custom loaders)
112
- writeMultiFile(ctx, generateLibUtils(ctx));
113
+ // VTEX utility wrappers (signature-compatible stubs) are no longer
114
+ // generated eagerly here. They're written lazily at end of phase-cleanup,
115
+ // after all import rewrites have run, so that we only emit shims that
116
+ // some file actually imports. See `writeImportedLibShims` in phase-cleanup.
113
117
 
114
118
  // Replace Context-based useDevice with SSR-safe useSyncExternalStore version.
115
119
  // @decocms/start shell-renders sections in a separate React root without
@@ -1,14 +1,14 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import type { MigrationContext, TransformResult, SectionMeta } from "./types.ts";
4
- import { log, logPhase } from "./types.ts";
5
- import { transformImports } from "./transforms/imports.ts";
6
- import { transformJsx } from "./transforms/jsx.ts";
7
- import { transformFreshApis } from "./transforms/fresh-apis.ts";
8
- import { transformDenoIsms } from "./transforms/deno-isms.ts";
9
- import { transformTailwind } from "./transforms/tailwind.ts";
10
- import { transformDeadCode } from "./transforms/dead-code.ts";
11
- import { transformSectionConventions } from "./transforms/section-conventions.ts";
3
+ import type { MigrationContext, TransformResult, SectionMeta } from "./types";
4
+ import { log, logPhase } from "./types";
5
+ import { transformImports } from "./transforms/imports";
6
+ import { transformJsx } from "./transforms/jsx";
7
+ import { transformFreshApis } from "./transforms/fresh-apis";
8
+ import { transformDenoIsms } from "./transforms/deno-isms";
9
+ import { transformTailwind } from "./transforms/tailwind";
10
+ import { transformDeadCode } from "./transforms/dead-code";
11
+ import { transformSectionConventions } from "./transforms/section-conventions";
12
12
 
13
13
  /** Map of section path → metadata, populated per-run */
14
14
  let sectionMetaMap: Map<string, SectionMeta> | null = null;
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import type { MigrationContext } from "./types.ts";
4
- import { logPhase } from "./types.ts";
3
+ import type { MigrationContext } from "./types";
4
+ import { logPhase } from "./types";
5
5
 
6
6
  interface Check {
7
7
  name: string;
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import type { MigrationContext } from "../types.ts";
4
- import type { ExtractedTheme } from "../analyzers/theme-extractor.ts";
3
+ import type { MigrationContext } from "../types";
4
+ import type { ExtractedTheme } from "../analyzers/theme-extractor";
5
5
 
6
6
  /**
7
7
  * Find the original site's custom CSS file.
@@ -1,4 +1,4 @@
1
- import type { MigrationContext } from "../types.ts";
1
+ import type { MigrationContext } from "../types";
2
2
 
3
3
  export function generateCacheConfig(ctx: MigrationContext): string {
4
4
  if (ctx.platform !== "vtex") {
@@ -1,4 +1,4 @@
1
- import type { MigrationContext } from "../types.ts";
1
+ import type { MigrationContext } from "../types";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
 
@@ -1,4 +1,4 @@
1
- import type { MigrationContext } from "../types.ts";
1
+ import type { MigrationContext } from "../types";
2
2
 
3
3
  export function generateHooks(ctx: MigrationContext): Record<string, string> {
4
4
  const files: Record<string, string> = {};
@@ -0,0 +1,91 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ LIB_TEMPLATES,
4
+ selectImportedLibTemplates,
5
+ } from "./lib-utils";
6
+
7
+ describe("LIB_TEMPLATES registry", () => {
8
+ it("has entries", () => {
9
+ expect(Object.keys(LIB_TEMPLATES).length).toBeGreaterThan(0);
10
+ });
11
+
12
+ it("uses src/lib/<name>.ts keys (relative paths the writer expects)", () => {
13
+ for (const key of Object.keys(LIB_TEMPLATES)) {
14
+ expect(key).toMatch(/^src\/lib\/[a-z][a-z0-9-]*\.ts$/);
15
+ }
16
+ });
17
+
18
+ it("has non-empty contents for every entry", () => {
19
+ for (const [key, value] of Object.entries(LIB_TEMPLATES)) {
20
+ expect(value, `${key} should have content`).toBeTruthy();
21
+ expect(value.length, `${key} length`).toBeGreaterThan(20);
22
+ }
23
+ });
24
+
25
+ it("has unique keys (no shadowing)", () => {
26
+ const keys = Object.keys(LIB_TEMPLATES);
27
+ const set = new Set(keys);
28
+ expect(set.size).toBe(keys.length);
29
+ });
30
+ });
31
+
32
+ describe("selectImportedLibTemplates()", () => {
33
+ it("returns empty record when no specifiers are imported", () => {
34
+ expect(selectImportedLibTemplates(new Set())).toEqual({});
35
+ });
36
+
37
+ it("returns only the templates whose specifier is in the set", () => {
38
+ const result = selectImportedLibTemplates(new Set(["vtex-segment"]));
39
+ expect(Object.keys(result)).toEqual(["src/lib/vtex-segment.ts"]);
40
+ expect(result["src/lib/vtex-segment.ts"]).toBe(LIB_TEMPLATES["src/lib/vtex-segment.ts"]);
41
+ });
42
+
43
+ it("returns multiple templates when multiple specifiers are imported", () => {
44
+ const result = selectImportedLibTemplates(
45
+ new Set(["vtex-segment", "vtex-transform", "filter-navigate"]),
46
+ );
47
+ const keys = Object.keys(result).sort();
48
+ expect(keys).toEqual([
49
+ "src/lib/filter-navigate.ts",
50
+ "src/lib/vtex-segment.ts",
51
+ "src/lib/vtex-transform.ts",
52
+ ]);
53
+ });
54
+
55
+ it("ignores unknown specifiers without throwing", () => {
56
+ const result = selectImportedLibTemplates(
57
+ new Set(["vtex-segment", "this-template-does-not-exist"]),
58
+ );
59
+ expect(Object.keys(result)).toEqual(["src/lib/vtex-segment.ts"]);
60
+ });
61
+
62
+ it("does not mutate LIB_TEMPLATES (returns a fresh object)", () => {
63
+ const before = JSON.stringify(LIB_TEMPLATES);
64
+ const result = selectImportedLibTemplates(new Set(["vtex-segment"]));
65
+ result["src/lib/vtex-segment.ts"] = "// HIJACKED";
66
+ expect(JSON.stringify(LIB_TEMPLATES)).toBe(before);
67
+ });
68
+
69
+ it("covers every well-known migration target the writer might emit", () => {
70
+ // Sanity check: the names that `transforms/imports.ts` rewrites to
71
+ // and that phase-cleanup hoists for inline-stub injection MUST all
72
+ // have templates registered, otherwise migrated sites get import
73
+ // errors with no warning.
74
+ const expectedSpecifiers = [
75
+ "vtex-transform",
76
+ "vtex-intelligent-search",
77
+ "vtex-segment",
78
+ "vtex-fetch",
79
+ "vtex-id",
80
+ "vtex-client",
81
+ "fetch-utils",
82
+ "http-utils",
83
+ "graphql-utils",
84
+ "filter-navigate",
85
+ ];
86
+ for (const spec of expectedSpecifiers) {
87
+ const key = `src/lib/${spec}.ts`;
88
+ expect(LIB_TEMPLATES, `expected template for ${key}`).toHaveProperty(key);
89
+ }
90
+ });
91
+ });
@@ -1,25 +1,41 @@
1
- import type { MigrationContext } from "../types.ts";
1
+ /**
2
+ * Templates for src/lib/ utility wrappers that bridge signature gaps
3
+ * between deco-cx/apps (old stack) and @decocms/apps-start (new stack).
4
+ *
5
+ * These are written *lazily*: only shims that are actually imported by
6
+ * the migrated codebase get generated. See `selectImportedLibTemplates`.
7
+ *
8
+ * Lazy generation matters because:
9
+ * - Most sites end up importing zero of these (apps-start exports
10
+ * direct equivalents for most VTEX utilities — see migrate#107).
11
+ * - Eager generation creates dead `src/lib/*.ts` files that every site
12
+ * then has to clean up by hand (see baggagio-tanstack#7, ~235 LOC).
13
+ *
14
+ * Registry shape: `"src/lib/<name>.ts"` → file contents.
15
+ */
16
+ export const LIB_TEMPLATES: Record<string, string> = {
17
+ // Filled in below after the const declarations to keep the registry
18
+ // and template literals colocated. See trailing assignment.
19
+ };
2
20
 
3
21
  /**
4
- * Generates src/lib/ utility wrappers that provide signature-compatible
5
- * stubs for VTEX utilities. The old stack (deco-cx/apps) exports functions
6
- * with different signatures than @decocms/apps-start, and some types
7
- * (VTEXCommerceStable, LabelledFuzzy) don't exist at all. These wrappers
8
- * bridge the gap so custom loaders continue to compile and run.
22
+ * Given the set of `~/lib/X` imports actually present in the migrated
23
+ * codebase, return the subset of templates to write.
24
+ *
25
+ * `importedSpecifiers` are the `X` parts (without `~/lib/` prefix and
26
+ * without `.ts` extension), e.g. `"vtex-transform"`, `"http-utils"`.
9
27
  */
10
- export function generateLibUtils(_ctx: MigrationContext): Record<string, string> {
11
- return {
12
- "src/lib/vtex-transform.ts": LIB_VTEX_TRANSFORM,
13
- "src/lib/vtex-intelligent-search.ts": LIB_VTEX_INTELLIGENT_SEARCH,
14
- "src/lib/vtex-segment.ts": LIB_VTEX_SEGMENT,
15
- "src/lib/http-utils.ts": LIB_HTTP_UTILS,
16
- "src/lib/vtex-client.ts": LIB_VTEX_CLIENT,
17
- "src/lib/fetch-utils.ts": LIB_FETCH_UTILS,
18
- "src/lib/vtex-fetch.ts": LIB_VTEX_FETCH,
19
- "src/lib/vtex-id.ts": LIB_VTEX_ID,
20
- "src/lib/graphql-utils.ts": LIB_GRAPHQL_UTILS,
21
- "src/lib/filter-navigate.ts": LIB_FILTER_NAVIGATE,
22
- };
28
+ export function selectImportedLibTemplates(
29
+ importedSpecifiers: Set<string>,
30
+ ): Record<string, string> {
31
+ const out: Record<string, string> = {};
32
+ for (const spec of importedSpecifiers) {
33
+ const key = `src/lib/${spec}.ts`;
34
+ if (key in LIB_TEMPLATES) {
35
+ out[key] = LIB_TEMPLATES[key];
36
+ }
37
+ }
38
+ return out;
23
39
  }
24
40
 
25
41
  const LIB_VTEX_TRANSFORM = `import type { Product } from "@decocms/apps/commerce/types";
@@ -253,3 +269,19 @@ export function toFilterSearchString(filterUrl: string): string {
253
269
  return clean ? \`?\${clean}\` : "";
254
270
  }
255
271
  `;
272
+
273
+ // Populate the registry now that all template literals are declared.
274
+ // Keys must match the relative path the migration script writes; values
275
+ // are the file contents.
276
+ Object.assign(LIB_TEMPLATES, {
277
+ "src/lib/vtex-transform.ts": LIB_VTEX_TRANSFORM,
278
+ "src/lib/vtex-intelligent-search.ts": LIB_VTEX_INTELLIGENT_SEARCH,
279
+ "src/lib/vtex-segment.ts": LIB_VTEX_SEGMENT,
280
+ "src/lib/http-utils.ts": LIB_HTTP_UTILS,
281
+ "src/lib/vtex-client.ts": LIB_VTEX_CLIENT,
282
+ "src/lib/fetch-utils.ts": LIB_FETCH_UTILS,
283
+ "src/lib/vtex-fetch.ts": LIB_VTEX_FETCH,
284
+ "src/lib/vtex-id.ts": LIB_VTEX_ID,
285
+ "src/lib/graphql-utils.ts": LIB_GRAPHQL_UTILS,
286
+ "src/lib/filter-navigate.ts": LIB_FILTER_NAVIGATE,
287
+ });
@@ -1,5 +1,5 @@
1
1
  import { execSync } from "node:child_process";
2
- import type { MigrationContext } from "../types.ts";
2
+ import type { MigrationContext } from "../types";
3
3
 
4
4
  /**
5
5
  * Get the latest published version of an npm package.
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import type { MigrationContext } from "../types.ts";
3
+ import type { MigrationContext } from "../types";
4
4
 
5
5
  function discoverFonts(ctx: MigrationContext): string[] {
6
6
  const fontsDir = path.join(ctx.sourceDir, "public", "fonts");
@@ -1,4 +1,4 @@
1
- import type { MigrationContext } from "../types.ts";
1
+ import type { MigrationContext } from "../types";
2
2
 
3
3
  export function generateSdkFiles(ctx: MigrationContext): Record<string, string> {
4
4
  const files: Record<string, string> = {};
@@ -1,4 +1,4 @@
1
- import type { MigrationContext, SectionMeta } from "../types.ts";
1
+ import type { MigrationContext, SectionMeta } from "../types";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
4
 
@@ -1,4 +1,4 @@
1
- import type { MigrationContext } from "../types.ts";
1
+ import type { MigrationContext } from "../types";
2
2
 
3
3
  function capitalize(s: string): string {
4
4
  return s.charAt(0).toUpperCase() + s.slice(1);
@@ -1,6 +1,6 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import type { MigrationContext } from "../types.ts";
3
+ import type { MigrationContext } from "../types";
4
4
 
5
5
  function discoverFonts(ctx: MigrationContext): string[] {
6
6
  // Check public/fonts (post-move)
@@ -1,4 +1,4 @@
1
- import type { MigrationContext } from "../types.ts";
1
+ import type { MigrationContext } from "../types";
2
2
 
3
3
  export function generateTypeFiles(ctx: MigrationContext): Record<string, string> {
4
4
  const files: Record<string, string> = {};
@@ -1,4 +1,4 @@
1
- import type { MigrationContext } from "../types.ts";
1
+ import type { MigrationContext } from "../types";
2
2
 
3
3
  export function generateUiComponents(_ctx: MigrationContext): Record<string, string> {
4
4
  const files: Record<string, string> = {};
@@ -1,4 +1,4 @@
1
- import type { MigrationContext } from "../types.ts";
1
+ import type { MigrationContext } from "../types";
2
2
 
3
3
  export function generateViteConfig(ctx: MigrationContext): string {
4
4
  const isVtex = ctx.platform === "vtex";
@@ -1,4 +1,4 @@
1
- import type { MigrationContext } from "../types.ts";
1
+ import type { MigrationContext } from "../types";
2
2
 
3
3
  export function generateWrangler(ctx: MigrationContext): string {
4
4
  const workerName = ctx.siteName
@@ -1,4 +1,4 @@
1
- import type { TransformResult } from "../types.ts";
1
+ import type { TransformResult } from "../types";
2
2
 
3
3
  /**
4
4
  * Removes dead code patterns from the old Deco stack that don't work
@@ -1,4 +1,4 @@
1
- import type { TransformResult } from "../types.ts";
1
+ import type { TransformResult } from "../types";
2
2
 
3
3
  /**
4
4
  * Removes Deno-specific patterns:
@@ -1,4 +1,4 @@
1
- import type { TransformResult } from "../types.ts";
1
+ import type { TransformResult } from "../types";
2
2
 
3
3
  /**
4
4
  * Fix JSX after scriptAsDataURI → useScript replacement.
@@ -1,4 +1,4 @@
1
- import type { TransformResult } from "../types.ts";
1
+ import type { TransformResult } from "../types";
2
2
 
3
3
  /**
4
4
  * Import rewriting rules: from (Deno/Fresh/Preact) → to (Node/TanStack/React)
@@ -1,4 +1,4 @@
1
- import type { TransformResult } from "../types.ts";
1
+ import type { TransformResult } from "../types";
2
2
 
3
3
  /**
4
4
  * Convert JSX attribute syntax to object literal entries.
@@ -1,4 +1,4 @@
1
- import type { TransformResult, SectionMeta } from "../types.ts";
1
+ import type { TransformResult, SectionMeta } from "../types";
2
2
 
3
3
  /**
4
4
  * Adds section convention exports (sync, eager, layout, cache)
@@ -1,4 +1,4 @@
1
- import type { TransformResult } from "../types.ts";
1
+ import type { TransformResult } from "../types";
2
2
 
3
3
  /**
4
4
  * Tailwind v3 → v4 class migration transform.