@decocms/blocks-cli 7.0.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 (93) hide show
  1. package/package.json +44 -0
  2. package/scripts/analyze-traces.mjs +1117 -0
  3. package/scripts/audit-observability-config.test.ts +446 -0
  4. package/scripts/audit-observability-config.ts +511 -0
  5. package/scripts/deco-migrate-cli.ts +444 -0
  6. package/scripts/fast-deploy-kv.test.ts +131 -0
  7. package/scripts/generate-blocks.test.ts +94 -0
  8. package/scripts/generate-blocks.ts +274 -0
  9. package/scripts/generate-invoke.test.ts +195 -0
  10. package/scripts/generate-invoke.ts +469 -0
  11. package/scripts/generate-loaders.ts +217 -0
  12. package/scripts/generate-schema.ts +1287 -0
  13. package/scripts/generate-sections.ts +237 -0
  14. package/scripts/htmx-analyze.ts +226 -0
  15. package/scripts/lib/blocks-dedupe.test.ts +179 -0
  16. package/scripts/lib/blocks-dedupe.ts +142 -0
  17. package/scripts/lib/cf-kv-rest.ts +78 -0
  18. package/scripts/lib/jsonc.ts +122 -0
  19. package/scripts/lib/kv-snapshot.ts +51 -0
  20. package/scripts/lib/read-decofile.ts +70 -0
  21. package/scripts/lib/sync-helpers.ts +44 -0
  22. package/scripts/migrate/analyzers/htmx-analyze.test.ts +372 -0
  23. package/scripts/migrate/analyzers/htmx-analyze.ts +425 -0
  24. package/scripts/migrate/analyzers/island-classifier.ts +96 -0
  25. package/scripts/migrate/analyzers/loader-inventory.ts +63 -0
  26. package/scripts/migrate/analyzers/section-metadata.ts +147 -0
  27. package/scripts/migrate/analyzers/theme-extractor.ts +122 -0
  28. package/scripts/migrate/colors.ts +46 -0
  29. package/scripts/migrate/config.test.ts +202 -0
  30. package/scripts/migrate/config.ts +186 -0
  31. package/scripts/migrate/phase-analyze.test.ts +63 -0
  32. package/scripts/migrate/phase-analyze.ts +782 -0
  33. package/scripts/migrate/phase-cleanup-audit.test.ts +137 -0
  34. package/scripts/migrate/phase-cleanup-audit.ts +105 -0
  35. package/scripts/migrate/phase-cleanup.test.ts +141 -0
  36. package/scripts/migrate/phase-cleanup.ts +1588 -0
  37. package/scripts/migrate/phase-compile.test.ts +193 -0
  38. package/scripts/migrate/phase-compile.ts +177 -0
  39. package/scripts/migrate/phase-report.ts +243 -0
  40. package/scripts/migrate/phase-scaffold.ts +593 -0
  41. package/scripts/migrate/phase-transform.ts +310 -0
  42. package/scripts/migrate/phase-verify.test.ts +127 -0
  43. package/scripts/migrate/phase-verify.ts +572 -0
  44. package/scripts/migrate/post-cleanup/rules.ts +1708 -0
  45. package/scripts/migrate/post-cleanup/runner.test.ts +1771 -0
  46. package/scripts/migrate/post-cleanup/runner.ts +137 -0
  47. package/scripts/migrate/post-cleanup/shim-classify.test.ts +352 -0
  48. package/scripts/migrate/post-cleanup/shim-classify.ts +246 -0
  49. package/scripts/migrate/post-cleanup/types.ts +106 -0
  50. package/scripts/migrate/source-layout.test.ts +111 -0
  51. package/scripts/migrate/source-layout.ts +103 -0
  52. package/scripts/migrate/templates/app-css.ts +366 -0
  53. package/scripts/migrate/templates/cache-config.ts +26 -0
  54. package/scripts/migrate/templates/commerce-loaders.ts +230 -0
  55. package/scripts/migrate/templates/cursor-rules.test.ts +59 -0
  56. package/scripts/migrate/templates/cursor-rules.ts +70 -0
  57. package/scripts/migrate/templates/hooks.test.ts +141 -0
  58. package/scripts/migrate/templates/hooks.ts +152 -0
  59. package/scripts/migrate/templates/knip-config.ts +27 -0
  60. package/scripts/migrate/templates/lib-utils.test.ts +139 -0
  61. package/scripts/migrate/templates/lib-utils.ts +326 -0
  62. package/scripts/migrate/templates/lockfile-check-yml.test.ts +26 -0
  63. package/scripts/migrate/templates/lockfile-check-yml.ts +66 -0
  64. package/scripts/migrate/templates/package-json.ts +177 -0
  65. package/scripts/migrate/templates/routes.ts +237 -0
  66. package/scripts/migrate/templates/sdk-gen.ts +59 -0
  67. package/scripts/migrate/templates/section-loaders.ts +456 -0
  68. package/scripts/migrate/templates/server-entry.ts +561 -0
  69. package/scripts/migrate/templates/setup.ts +148 -0
  70. package/scripts/migrate/templates/tsconfig.ts +21 -0
  71. package/scripts/migrate/templates/types-gen.ts +174 -0
  72. package/scripts/migrate/templates/ui-components.ts +144 -0
  73. package/scripts/migrate/templates/vite-config.ts +101 -0
  74. package/scripts/migrate/transforms/dead-code.ts +455 -0
  75. package/scripts/migrate/transforms/deno-isms.ts +85 -0
  76. package/scripts/migrate/transforms/fresh-apis.ts +223 -0
  77. package/scripts/migrate/transforms/htmx-on-events.test.ts +305 -0
  78. package/scripts/migrate/transforms/htmx-on-events.ts +193 -0
  79. package/scripts/migrate/transforms/imports.ts +385 -0
  80. package/scripts/migrate/transforms/jsx.ts +317 -0
  81. package/scripts/migrate/transforms/section-conventions.ts +210 -0
  82. package/scripts/migrate/transforms/tailwind.ts +739 -0
  83. package/scripts/migrate/types.ts +244 -0
  84. package/scripts/migrate-blocks-to-kv.ts +104 -0
  85. package/scripts/migrate-post-cleanup.ts +191 -0
  86. package/scripts/migrate-to-cf-observability.test.ts +215 -0
  87. package/scripts/migrate-to-cf-observability.ts +699 -0
  88. package/scripts/migrate.ts +282 -0
  89. package/scripts/smoke-otlp-errorlog.ts +46 -0
  90. package/scripts/smoke-otlp-meter.ts +48 -0
  91. package/scripts/sync-blocks-to-kv.ts +153 -0
  92. package/scripts/tailwind-lint.ts +518 -0
  93. package/tsconfig.json +7 -0
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Cursor rule scaffolding for migrated sites.
3
+ *
4
+ * The canonical migration tooling policy (D1–D5, priorities, process)
5
+ * lives in `decocms/deco-start`. We don't duplicate it into every site
6
+ * — that would drift the moment the canonical changes. Instead the
7
+ * migration scaffolds a tiny pointer rule, marked `alwaysApply: true`,
8
+ * that loads on every agent session inside the migrated site and tells
9
+ * the agent where the real policy lives.
10
+ *
11
+ * Closes Wave 12-H from MIGRATION_TOOLING_PLAN.md.
12
+ */
13
+
14
+ /**
15
+ * Generate the contents of `.cursor/rules/migration-tooling-policy.mdc`
16
+ * for a freshly migrated site. Pure function: `siteName` is the only
17
+ * input that influences the body, and only as a friendly mention.
18
+ */
19
+ export function generateMigrationPolicyPointerRule(
20
+ siteName: string,
21
+ ): string {
22
+ return `---
23
+ description: Pointer to the canonical migration tooling policy (D1–D5, PR-only, etc.). Always loaded.
24
+ alwaysApply: true
25
+ ---
26
+
27
+ # Migration Tooling Policy — Pointer
28
+
29
+ > This site (\`${siteName}\`) was generated by the \`@decocms/start\`
30
+ > migration script. The canonical policy that governs how the migration
31
+ > tooling, framework (\`@decocms/start\`), and commerce layer
32
+ > (\`@decocms/apps\`) evolve lives **upstream**, not in this repo.
33
+
34
+ ## Where to read
35
+
36
+ - **Rule (always-applied) — full text:**
37
+ https://github.com/decocms/deco-start/blob/main/.cursor/rules/migration-tooling-policy.mdc
38
+ - **Plan (living tracker, decisions + waves):**
39
+ https://github.com/decocms/deco-start/blob/main/MIGRATION_TOOLING_PLAN.md
40
+ - **Migration skill (phase playbook):**
41
+ \`@decocms/start/.agents/skills/deco-to-tanstack-migration\`
42
+
43
+ ## What you need to know in this site
44
+
45
+ | ID | Decision | What it means here |
46
+ |----|----------|--------------------|
47
+ | **D1** | Force convergence — no fork runtime support | Site customisations live in \`src/apps/local/\` or open a PR to \`@decocms/apps\`. Don't wrap framework/commerce code in soft adapters. |
48
+ | **D2** | Rewrite HTMX on migration | If you find HTMX residue, rewrite to React. Don't bring back \`hx-*\` runtime. |
49
+ | **D3** | Generated stubs throw at runtime | If a \`~/lib/vtex-*\` import comes from a stub that returns \`null\` / \`{}\` / identity-cast, replace it. \`npx -p @decocms/start deco-post-cleanup --fix\` does the safe swaps automatically. |
50
+ | **D4** | Site-local apps by default, promote at 3+ sites | Don't try to upstream a pattern that has only shipped here. Build it twice in different sites first, then PR it to \`@decocms/apps\`. |
51
+ | **D5** | Failed migrations: \`rm -rf\` and re-run | No restart-mode magic. If the migration goes sideways, blow away the working tree and run again. |
52
+
53
+ ## How to find issues this rule wants you to fix
54
+
55
+ \`\`\`bash
56
+ npx -p @decocms/start deco-post-cleanup # audit only
57
+ npx -p @decocms/start deco-post-cleanup --fix # auto-fix the safe rules
58
+ npx -p @decocms/start deco-post-cleanup --strict # exit 1 on any finding (CI)
59
+ \`\`\`
60
+
61
+ ## Process
62
+
63
+ - **PR-only.** No direct pushes to \`main\`. Self-merge after green CI is fine.
64
+ - **Conventional commits** (\`feat\`, \`fix\`, \`chore\`, \`docs\`, \`refactor\`, \`test\`, \`perf\`).
65
+ - **CI must be green before merge.**
66
+ - When the canonical rule changes upstream, update this pointer if any
67
+ link or table heading goes stale. Keep it short — body content lives
68
+ upstream.
69
+ `;
70
+ }
@@ -0,0 +1,141 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { generateHooks } from "./hooks";
3
+ import type { MigrationContext } from "../types";
4
+
5
+ function makeCtx(platform: MigrationContext["platform"]): MigrationContext {
6
+ return {
7
+ sourceDir: "/tmp",
8
+ siteName: "test",
9
+ platform,
10
+ vtexAccount: null,
11
+ gtmId: null,
12
+ importMap: {},
13
+ discoveredNpmDeps: {},
14
+ themeColors: {},
15
+ fontFamily: null,
16
+ files: [],
17
+ sectionMetas: [],
18
+ islandClassifications: [],
19
+ islandWrapperTargets: new Map(),
20
+ loaderInventory: [],
21
+ scaffoldedFiles: [],
22
+ transformedFiles: [],
23
+ deletedFiles: [],
24
+ movedFiles: [],
25
+ manualReviewItems: [],
26
+ frameworkFindings: [],
27
+ dryRun: false,
28
+ verbose: false,
29
+ };
30
+ }
31
+
32
+ describe("generateHooks (vtex)", () => {
33
+ const files = generateHooks(makeCtx("vtex"));
34
+
35
+ it("emits all three hook files", () => {
36
+ expect(Object.keys(files).sort()).toEqual([
37
+ "src/hooks/useCart.ts",
38
+ "src/hooks/useUser.ts",
39
+ "src/hooks/useWishlist.ts",
40
+ ]);
41
+ });
42
+
43
+ it("useCart is the createUseCart factory shim, not 250-line legacy boilerplate", () => {
44
+ const code = files["src/hooks/useCart.ts"];
45
+ // Imports from the framework factory.
46
+ expect(code).toContain(
47
+ 'import { createUseCart } from "@decocms/apps/vtex/hooks/createUseCart"',
48
+ );
49
+ expect(code).toContain('import { invoke } from "~/server/invoke"');
50
+ // Re-exports types from @decocms/apps directly.
51
+ expect(code).toContain(
52
+ 'export type { OrderForm, OrderFormItem } from "@decocms/apps/vtex/types"',
53
+ );
54
+ // Calls the factory with invoke and destructures the public API.
55
+ expect(code).toContain(
56
+ "export const { useCart, resetCart, itemToAnalyticsItem } = createUseCart",
57
+ );
58
+ // And does NOT contain the old singleton machinery.
59
+ expect(code).not.toContain("const _listeners = new Set");
60
+ expect(code).not.toContain("forceRender");
61
+ expect(code).not.toContain("function getOrderFormIdFromCookie");
62
+ });
63
+
64
+ it("useCart shim is dramatically smaller than the legacy template", () => {
65
+ const lineCount = files["src/hooks/useCart.ts"].split("\n").length;
66
+ // Should be well under 20 lines (factory call + re-export + imports).
67
+ expect(lineCount).toBeLessThan(20);
68
+ });
69
+
70
+ it("useUser is the createUseUser factory shim (no signal-stub boilerplate)", () => {
71
+ const code = files["src/hooks/useUser.ts"];
72
+ expect(code).toContain(
73
+ 'import { createUseUser } from "@decocms/apps/vtex/hooks/createUseUser"',
74
+ );
75
+ expect(code).toContain('import { invoke } from "~/server/invoke"');
76
+ expect(code).toContain(
77
+ 'export type { Person } from "@decocms/apps/vtex/loaders/user"',
78
+ );
79
+ expect(code).toContain("export const { useUser, resetUser } = createUseUser");
80
+ // Must NOT scaffold the legacy signal stub.
81
+ expect(code).not.toContain('signal<User | null>(null)');
82
+ expect(code).not.toContain("export interface User {");
83
+ });
84
+
85
+ it("useWishlist is the createUseWishlist factory shim", () => {
86
+ const code = files["src/hooks/useWishlist.ts"];
87
+ expect(code).toContain(
88
+ 'import { createUseWishlist } from "@decocms/apps/vtex/hooks/createUseWishlist"',
89
+ );
90
+ expect(code).toContain('import { invoke } from "~/server/invoke"');
91
+ expect(code).toContain(
92
+ 'export type { WishlistItem } from "@decocms/apps/vtex/loaders/wishlist"',
93
+ );
94
+ expect(code).toContain(
95
+ "export const { useWishlist, resetWishlist } = createUseWishlist",
96
+ );
97
+ // Must NOT scaffold the legacy stub with TODO action bodies.
98
+ expect(code).not.toContain("TODO: Implement");
99
+ expect(code).not.toContain("getItem(_productId: string): boolean");
100
+ });
101
+
102
+ it("useUser + useWishlist VTEX shims are each well under 10 lines", () => {
103
+ expect(files["src/hooks/useUser.ts"].split("\n").length).toBeLessThan(10);
104
+ expect(files["src/hooks/useWishlist.ts"].split("\n").length).toBeLessThan(10);
105
+ });
106
+ });
107
+
108
+ describe("generateHooks (non-vtex)", () => {
109
+ it("custom platform falls back to the generic stub", () => {
110
+ const files = generateHooks(makeCtx("custom"));
111
+ const code = files["src/hooks/useCart.ts"];
112
+ expect(code).toContain("Cart Hook stub");
113
+ expect(code).toContain("TODO: Implement");
114
+ });
115
+
116
+ it("shopify currently shares the generic stub", () => {
117
+ const files = generateHooks(makeCtx("shopify"));
118
+ const code = files["src/hooks/useCart.ts"];
119
+ // Until a shopify factory exists, non-vtex platforms get the generic stub.
120
+ expect(code).toContain("Cart Hook stub");
121
+ });
122
+
123
+ it("non-vtex platforms keep the legacy signal-based useUser stub", () => {
124
+ const files = generateHooks(makeCtx("custom"));
125
+ const code = files["src/hooks/useUser.ts"];
126
+ // No factory CALL — docstring may mention it as a pointer for VTEX.
127
+ expect(code).not.toContain("createUseUser({");
128
+ expect(code).not.toContain("export const { useUser, resetUser }");
129
+ // Legacy stub shape (signal + User interface).
130
+ expect(code).toContain("signal<User | null>(null)");
131
+ expect(code).toContain("export interface User {");
132
+ });
133
+
134
+ it("non-vtex platforms keep the legacy useWishlist stub", () => {
135
+ const files = generateHooks(makeCtx("custom"));
136
+ const code = files["src/hooks/useWishlist.ts"];
137
+ expect(code).not.toContain("createUseWishlist({");
138
+ expect(code).not.toContain("export const { useWishlist, resetWishlist }");
139
+ expect(code).toContain("TODO: Implement");
140
+ });
141
+ });
@@ -0,0 +1,152 @@
1
+ import type { MigrationContext } from "../types";
2
+
3
+ export function generateHooks(ctx: MigrationContext): Record<string, string> {
4
+ const files: Record<string, string> = {};
5
+
6
+ if (ctx.platform === "vtex") {
7
+ files["src/hooks/useCart.ts"] = generateVtexUseCart();
8
+ files["src/hooks/useUser.ts"] = generateVtexUseUser();
9
+ files["src/hooks/useWishlist.ts"] = generateVtexUseWishlist();
10
+ } else {
11
+ files["src/hooks/useCart.ts"] = generateGenericUseCart();
12
+ files["src/hooks/useUser.ts"] = generateGenericUseUser();
13
+ files["src/hooks/useWishlist.ts"] = generateGenericUseWishlist();
14
+ }
15
+
16
+ return files;
17
+ }
18
+
19
+ function generateVtexUseCart(): string {
20
+ // The legacy invoke-based useCart hook is now a 5-line factory call —
21
+ // the heavy lifting (singleton state, listener pattern, async actions,
22
+ // analytics helpers) lives in @decocms/apps/vtex/hooks/createUseCart.
23
+ return `import { createUseCart } from "@decocms/apps/vtex/hooks/createUseCart";
24
+ import { invoke } from "~/server/invoke";
25
+
26
+ export type { OrderForm, OrderFormItem } from "@decocms/apps/vtex/types";
27
+
28
+ export const { useCart, resetCart, itemToAnalyticsItem } = createUseCart({
29
+ invoke,
30
+ });
31
+ `;
32
+ }
33
+
34
+ function generateGenericUseCart(): string {
35
+ return `/**
36
+ * Cart Hook stub — implement for your platform.
37
+ *
38
+ * Wire invoke calls from ~/server/invoke to your commerce platform's
39
+ * cart API (addItem, updateItem, getCart, etc.).
40
+ */
41
+ import { signal } from "~/sdk/signal";
42
+
43
+ const cart = signal<any>(null);
44
+ const loading = signal(false);
45
+
46
+ export function useCart() {
47
+ return {
48
+ cart,
49
+ loading,
50
+ async getCart() {
51
+ // TODO: Implement for your platform
52
+ return null;
53
+ },
54
+ async addItems(_items: any[]) {
55
+ // TODO: Implement
56
+ },
57
+ async updateItems(_items: any[]) {
58
+ // TODO: Implement
59
+ },
60
+ setCart(newCart: any) {
61
+ cart.value = newCart;
62
+ },
63
+ };
64
+ }
65
+
66
+ export default useCart;
67
+ `;
68
+ }
69
+
70
+ // VTEX path — these are five-line factory shims. The heavy lifting
71
+ // (singleton state, listener pattern, async actions, signal-shaped
72
+ // accessors, legacy arg-swap conventions) lives in
73
+ // @decocms/apps/vtex/hooks/createUseUser and createUseWishlist.
74
+ function generateVtexUseUser(): string {
75
+ return `import { createUseUser } from "@decocms/apps/vtex/hooks/createUseUser";
76
+ import { invoke } from "~/server/invoke";
77
+
78
+ export type { Person } from "@decocms/apps/vtex/loaders/user";
79
+
80
+ export const { useUser, resetUser } = createUseUser({ invoke });
81
+ `;
82
+ }
83
+
84
+ function generateVtexUseWishlist(): string {
85
+ return `import { createUseWishlist } from "@decocms/apps/vtex/hooks/createUseWishlist";
86
+ import { invoke } from "~/server/invoke";
87
+
88
+ export type { WishlistItem } from "@decocms/apps/vtex/loaders/wishlist";
89
+
90
+ export const { useWishlist, resetWishlist } = createUseWishlist({ invoke });
91
+ `;
92
+ }
93
+
94
+ // Non-VTEX fallback — keeps the legacy signal-based stub shape so any
95
+ // generic platform port that already consumes `~/hooks/useUser` keeps
96
+ // type-checking. Sites must wire their own platform-specific impl.
97
+ function generateGenericUseUser(): string {
98
+ return `/**
99
+ * User Hook — wire to invoke.site.loaders for your platform's user API.
100
+ *
101
+ * VTEX sites get a real factory from @decocms/apps/vtex/hooks/createUseUser;
102
+ * see migration template hooks.ts for the canonical five-line shim.
103
+ */
104
+ import { signal } from "~/sdk/signal";
105
+
106
+ export interface User {
107
+ "@id": string;
108
+ email: string;
109
+ givenName?: string;
110
+ familyName?: string;
111
+ }
112
+
113
+ const user = signal<User | null>(null);
114
+ const loading = signal(false);
115
+
116
+ export function useUser() {
117
+ return { user, loading };
118
+ }
119
+
120
+ export default useUser;
121
+ `;
122
+ }
123
+
124
+ function generateGenericUseWishlist(): string {
125
+ return `/**
126
+ * Wishlist Hook — wire to invoke.site.loaders/actions for your platform.
127
+ *
128
+ * VTEX sites get a real factory from @decocms/apps/vtex/hooks/createUseWishlist;
129
+ * see migration template hooks.ts for the canonical five-line shim.
130
+ */
131
+ import { signal } from "~/sdk/signal";
132
+
133
+ const loading = signal(false);
134
+
135
+ export function useWishlist() {
136
+ return {
137
+ loading,
138
+ async addItem(_productId: string, _productGroupId: string) {
139
+ // TODO: Implement for your platform
140
+ },
141
+ async removeItem(_productId: string) {
142
+ // TODO: Implement
143
+ },
144
+ getItem(_productId: string): boolean {
145
+ return false;
146
+ },
147
+ };
148
+ }
149
+
150
+ export default useWishlist;
151
+ `;
152
+ }
@@ -0,0 +1,27 @@
1
+ export function generateKnipConfig(): string {
2
+ return `import type { KnipConfig } from "knip";
3
+
4
+ const config: KnipConfig = {
5
+ entry: [
6
+ "src/routes/**/*.{ts,tsx}",
7
+ "src/setup.ts",
8
+ "src/runtime.ts",
9
+ "src/sections/**/*.{ts,tsx}",
10
+ "vite.config.ts",
11
+ ],
12
+ project: ["src/**/*.{ts,tsx}"],
13
+ ignore: [
14
+ "src/server/invoke.gen.ts",
15
+ "src/server/cms/blocks.gen.ts",
16
+ "src/routeTree.gen.ts",
17
+ ],
18
+ ignoreDependencies: [
19
+ "babel-plugin-react-compiler",
20
+ "@vitejs/plugin-react",
21
+ "wrangler",
22
+ ],
23
+ };
24
+
25
+ export default config;
26
+ `;
27
+ }
@@ -0,0 +1,139 @@
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
+ });
92
+
93
+ describe("D3 — generated stubs throw at runtime", () => {
94
+ // Each stub MUST throw an Error whose message identifies:
95
+ // - the stub path so the dev sees it in their stack trace
96
+ // - the canonical replacement (so the fix is mechanical)
97
+ //
98
+ // See migration-tooling-policy.mdc § Decision 3.
99
+ it("vtex-transform.toProduct throws and points at the canonical path", () => {
100
+ const src = LIB_TEMPLATES["src/lib/vtex-transform.ts"];
101
+ expect(src).toMatch(/throw new Error/);
102
+ expect(src).toMatch(/@decocms\/apps\/vtex\/utils\/transform/);
103
+ expect(src).toMatch(/\[deco-migrate\]/);
104
+ });
105
+
106
+ it("vtex-intelligent-search.getISCookiesFromBag throws", () => {
107
+ const src = LIB_TEMPLATES["src/lib/vtex-intelligent-search.ts"];
108
+ expect(src).toMatch(/getISCookiesFromBag[\s\S]*?throw new Error/);
109
+ expect(src).toMatch(/\[deco-migrate\]/);
110
+ // The other helpers in this file (isFilterParam, toPath,
111
+ // withDefaultFacets, withDefaultParams) are real impls — must not
112
+ // throw.
113
+ expect(src).toMatch(/export function isFilterParam[\s\S]*?return key\.startsWith/);
114
+ });
115
+
116
+ it("vtex-segment.getSegmentFromBag and withSegmentCookie both throw", () => {
117
+ const src = LIB_TEMPLATES["src/lib/vtex-segment.ts"];
118
+ expect(src).toMatch(/getSegmentFromBag[\s\S]*?throw new Error/);
119
+ expect(src).toMatch(/withSegmentCookie[\s\S]*?throw new Error/);
120
+ expect(src).toMatch(/@decocms\/apps\/vtex\/utils\/segment/);
121
+ });
122
+
123
+ it("non-stub helpers stay implemented (negative check — no throw)", () => {
124
+ // These are real impls, not stubs. They must not throw.
125
+ const real = [
126
+ "src/lib/http-utils.ts",
127
+ "src/lib/vtex-id.ts",
128
+ "src/lib/graphql-utils.ts",
129
+ "src/lib/filter-navigate.ts",
130
+ "src/lib/fetch-utils.ts",
131
+ ];
132
+ for (const key of real) {
133
+ const src = LIB_TEMPLATES[key];
134
+ expect(src, `${key} should not contain a generated stub throw`).not.toMatch(
135
+ /\[deco-migrate\][^"]*generated stub/,
136
+ );
137
+ }
138
+ });
139
+ });