@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.
- package/package.json +44 -0
- package/scripts/analyze-traces.mjs +1117 -0
- package/scripts/audit-observability-config.test.ts +446 -0
- package/scripts/audit-observability-config.ts +511 -0
- package/scripts/deco-migrate-cli.ts +444 -0
- package/scripts/fast-deploy-kv.test.ts +131 -0
- package/scripts/generate-blocks.test.ts +94 -0
- package/scripts/generate-blocks.ts +274 -0
- package/scripts/generate-invoke.test.ts +195 -0
- package/scripts/generate-invoke.ts +469 -0
- package/scripts/generate-loaders.ts +217 -0
- package/scripts/generate-schema.ts +1287 -0
- package/scripts/generate-sections.ts +237 -0
- package/scripts/htmx-analyze.ts +226 -0
- package/scripts/lib/blocks-dedupe.test.ts +179 -0
- package/scripts/lib/blocks-dedupe.ts +142 -0
- package/scripts/lib/cf-kv-rest.ts +78 -0
- package/scripts/lib/jsonc.ts +122 -0
- package/scripts/lib/kv-snapshot.ts +51 -0
- package/scripts/lib/read-decofile.ts +70 -0
- package/scripts/lib/sync-helpers.ts +44 -0
- package/scripts/migrate/analyzers/htmx-analyze.test.ts +372 -0
- package/scripts/migrate/analyzers/htmx-analyze.ts +425 -0
- package/scripts/migrate/analyzers/island-classifier.ts +96 -0
- package/scripts/migrate/analyzers/loader-inventory.ts +63 -0
- package/scripts/migrate/analyzers/section-metadata.ts +147 -0
- package/scripts/migrate/analyzers/theme-extractor.ts +122 -0
- package/scripts/migrate/colors.ts +46 -0
- package/scripts/migrate/config.test.ts +202 -0
- package/scripts/migrate/config.ts +186 -0
- package/scripts/migrate/phase-analyze.test.ts +63 -0
- package/scripts/migrate/phase-analyze.ts +782 -0
- package/scripts/migrate/phase-cleanup-audit.test.ts +137 -0
- package/scripts/migrate/phase-cleanup-audit.ts +105 -0
- package/scripts/migrate/phase-cleanup.test.ts +141 -0
- package/scripts/migrate/phase-cleanup.ts +1588 -0
- package/scripts/migrate/phase-compile.test.ts +193 -0
- package/scripts/migrate/phase-compile.ts +177 -0
- package/scripts/migrate/phase-report.ts +243 -0
- package/scripts/migrate/phase-scaffold.ts +593 -0
- package/scripts/migrate/phase-transform.ts +310 -0
- package/scripts/migrate/phase-verify.test.ts +127 -0
- package/scripts/migrate/phase-verify.ts +572 -0
- package/scripts/migrate/post-cleanup/rules.ts +1708 -0
- package/scripts/migrate/post-cleanup/runner.test.ts +1771 -0
- package/scripts/migrate/post-cleanup/runner.ts +137 -0
- package/scripts/migrate/post-cleanup/shim-classify.test.ts +352 -0
- package/scripts/migrate/post-cleanup/shim-classify.ts +246 -0
- package/scripts/migrate/post-cleanup/types.ts +106 -0
- package/scripts/migrate/source-layout.test.ts +111 -0
- package/scripts/migrate/source-layout.ts +103 -0
- package/scripts/migrate/templates/app-css.ts +366 -0
- package/scripts/migrate/templates/cache-config.ts +26 -0
- package/scripts/migrate/templates/commerce-loaders.ts +230 -0
- package/scripts/migrate/templates/cursor-rules.test.ts +59 -0
- package/scripts/migrate/templates/cursor-rules.ts +70 -0
- package/scripts/migrate/templates/hooks.test.ts +141 -0
- package/scripts/migrate/templates/hooks.ts +152 -0
- package/scripts/migrate/templates/knip-config.ts +27 -0
- package/scripts/migrate/templates/lib-utils.test.ts +139 -0
- package/scripts/migrate/templates/lib-utils.ts +326 -0
- package/scripts/migrate/templates/lockfile-check-yml.test.ts +26 -0
- package/scripts/migrate/templates/lockfile-check-yml.ts +66 -0
- package/scripts/migrate/templates/package-json.ts +177 -0
- package/scripts/migrate/templates/routes.ts +237 -0
- package/scripts/migrate/templates/sdk-gen.ts +59 -0
- package/scripts/migrate/templates/section-loaders.ts +456 -0
- package/scripts/migrate/templates/server-entry.ts +561 -0
- package/scripts/migrate/templates/setup.ts +148 -0
- package/scripts/migrate/templates/tsconfig.ts +21 -0
- package/scripts/migrate/templates/types-gen.ts +174 -0
- package/scripts/migrate/templates/ui-components.ts +144 -0
- package/scripts/migrate/templates/vite-config.ts +101 -0
- package/scripts/migrate/transforms/dead-code.ts +455 -0
- package/scripts/migrate/transforms/deno-isms.ts +85 -0
- package/scripts/migrate/transforms/fresh-apis.ts +223 -0
- package/scripts/migrate/transforms/htmx-on-events.test.ts +305 -0
- package/scripts/migrate/transforms/htmx-on-events.ts +193 -0
- package/scripts/migrate/transforms/imports.ts +385 -0
- package/scripts/migrate/transforms/jsx.ts +317 -0
- package/scripts/migrate/transforms/section-conventions.ts +210 -0
- package/scripts/migrate/transforms/tailwind.ts +739 -0
- package/scripts/migrate/types.ts +244 -0
- package/scripts/migrate-blocks-to-kv.ts +104 -0
- package/scripts/migrate-post-cleanup.ts +191 -0
- package/scripts/migrate-to-cf-observability.test.ts +215 -0
- package/scripts/migrate-to-cf-observability.ts +699 -0
- package/scripts/migrate.ts +282 -0
- package/scripts/smoke-otlp-errorlog.ts +46 -0
- package/scripts/smoke-otlp-meter.ts +48 -0
- package/scripts/sync-blocks-to-kv.ts +153 -0
- package/scripts/tailwind-lint.ts +518 -0
- 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
|
+
});
|