@decocms/blocks-cli 7.64.3 → 7.65.1
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 +2 -2
- package/scripts/deco-migrate-cli.ts +5 -5
- package/scripts/generate-invoke.test.ts +2 -2
- package/scripts/generate-schema-loader-aliases.test.ts +186 -0
- package/scripts/generate-schema.test.ts +2 -2
- package/scripts/generate-schema.ts +35 -44
- package/scripts/generate-sections.test.ts +4 -4
- package/scripts/lib/blocks-dedupe.test.ts +1 -1
- package/scripts/lib/upgrade-6-to-7.ts +1 -1
- package/scripts/migrate/analyzers/htmx-analyze.test.ts +6 -6
- package/scripts/migrate/config.ts +2 -2
- package/scripts/migrate/phase-analyze.test.ts +3 -3
- package/scripts/migrate/phase-analyze.ts +4 -4
- package/scripts/migrate/phase-cleanup.ts +1 -1
- package/scripts/migrate/phase-transform.ts +2 -2
- package/scripts/migrate/post-cleanup/rules.ts +2 -2
- package/scripts/migrate/post-cleanup/shim-classify.test.ts +1 -1
- package/scripts/migrate/post-cleanup/shim-classify.ts +2 -2
- package/scripts/migrate/templates/ci-workflows.test.ts +3 -3
- package/scripts/migrate/templates/cursor-rules.test.ts +3 -3
- package/scripts/migrate/templates/lib-utils.ts +3 -3
- package/scripts/migrate/templates/perf-yml.ts +1 -1
- package/scripts/migrate/templates/section-loaders.ts +3 -3
- package/scripts/migrate/templates/setup.ts +1 -1
- package/scripts/migrate/transforms/ctx-compat.ts +1 -1
- package/scripts/migrate/transforms/htmx-on-events.test.ts +1 -1
- package/scripts/migrate/transforms/htmx-on-events.ts +2 -2
- package/scripts/migrate/types.ts +1 -1
- package/scripts/migrate-to-cf-observability.test.ts +5 -5
- package/scripts/migrate-to-cf-observability.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/blocks-cli",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.65.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco codegen (generate-blocks, generate-schema, generate-invoke) and Fresh-to-TanStack migration tooling",
|
|
6
6
|
"repository": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"lint:unused": "knip"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@decocms/blocks": "7.
|
|
36
|
+
"@decocms/blocks": "7.65.1",
|
|
37
37
|
"ts-morph": "^27.0.0",
|
|
38
38
|
"tsx": "^4.22.5"
|
|
39
39
|
},
|
|
@@ -123,15 +123,15 @@ function showHelp() {
|
|
|
123
123
|
npx tsx scripts/deco-migrate-cli.ts https://github.com/org/my-site
|
|
124
124
|
|
|
125
125
|
${dim("# Migrate local dir, compare against golden reference:")}
|
|
126
|
-
npx tsx scripts/deco-migrate-cli.ts ./
|
|
127
|
-
--ref ./
|
|
126
|
+
npx tsx scripts/deco-migrate-cli.ts ./acme \\
|
|
127
|
+
--ref ./acme-storefront
|
|
128
128
|
|
|
129
129
|
${dim("# Quick re-run (wipe previous output first):")}
|
|
130
|
-
npx tsx scripts/deco-migrate-cli.ts ./
|
|
131
|
-
-o ./
|
|
130
|
+
npx tsx scripts/deco-migrate-cli.ts ./acme \\
|
|
131
|
+
-o ./acme-migrated --clean
|
|
132
132
|
|
|
133
133
|
${dim("# Dry run to preview what would change:")}
|
|
134
|
-
npx tsx scripts/deco-migrate-cli.ts ./
|
|
134
|
+
npx tsx scripts/deco-migrate-cli.ts ./acme --dry-run -v
|
|
135
135
|
`);
|
|
136
136
|
}
|
|
137
137
|
|
|
@@ -197,8 +197,8 @@ describe("generate-invoke.ts — default --apps-dir resolution", () => {
|
|
|
197
197
|
// at node_modules/@decocms/apps-vtex/src/invoke.ts — NOT at the package
|
|
198
198
|
// root. The old default only probed the root, never resolved on a site
|
|
199
199
|
// with npm-installed packages, and forced sites to pass
|
|
200
|
-
// `--apps-dir node_modules/@decocms/apps-vtex/src` by hand (
|
|
201
|
-
// migration workaround). The default must probe <pkg>/invoke.ts first,
|
|
200
|
+
// `--apps-dir node_modules/@decocms/apps-vtex/src` by hand (a production
|
|
201
|
+
// site's migration workaround). The default must probe <pkg>/invoke.ts first,
|
|
202
202
|
// then <pkg>/src/invoke.ts.
|
|
203
203
|
|
|
204
204
|
function makeSite(layout: "root" | "src"): { siteDir: string; cleanup: () => void } {
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
interface Schema {
|
|
8
|
+
type?: string;
|
|
9
|
+
$ref?: string;
|
|
10
|
+
allOf?: Schema[];
|
|
11
|
+
anyOf?: Schema[];
|
|
12
|
+
properties?: Record<string, Schema>;
|
|
13
|
+
items?: Schema;
|
|
14
|
+
required?: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Exercise loader discovery AND prop matching through the real CLI. A context
|
|
18
|
+
// pre-seeded with "Settings" would miss the bug in output-type registration.
|
|
19
|
+
describe("generate-schema named loader aliases", () => {
|
|
20
|
+
let fixture: string;
|
|
21
|
+
let props: Schema;
|
|
22
|
+
|
|
23
|
+
beforeAll(() => {
|
|
24
|
+
fixture = mkdtempSync(path.join(tmpdir(), "schema-loader-aliases-"));
|
|
25
|
+
const write = (relative: string, contents: string) => {
|
|
26
|
+
const file = path.join(fixture, relative);
|
|
27
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
28
|
+
writeFileSync(file, contents);
|
|
29
|
+
};
|
|
30
|
+
write(
|
|
31
|
+
"tsconfig.json",
|
|
32
|
+
JSON.stringify({
|
|
33
|
+
compilerOptions: {
|
|
34
|
+
target: "ES2022",
|
|
35
|
+
module: "ESNext",
|
|
36
|
+
moduleResolution: "Bundler",
|
|
37
|
+
strict: true,
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
write(
|
|
42
|
+
"src/types.ts",
|
|
43
|
+
`export interface Config { title: string; modules: unknown; }
|
|
44
|
+
export type Settings = Omit<Config, "modules">;
|
|
45
|
+
export type Picked = Pick<Config, "title">;
|
|
46
|
+
export type Plain = { label: string };
|
|
47
|
+
export type Tree = { id: string; children?: Tree[] };
|
|
48
|
+
export type Generic<T> = { value: T };
|
|
49
|
+
export interface Product { productID: string; }
|
|
50
|
+
export interface Collection { label: string; settings: Settings; }
|
|
51
|
+
export type Unrelated = { title: string };`,
|
|
52
|
+
);
|
|
53
|
+
const loaders: Record<string, string> = {
|
|
54
|
+
settings: "Settings",
|
|
55
|
+
nullable: "Promise<Settings | null>",
|
|
56
|
+
picked: "Picked",
|
|
57
|
+
plain: "Plain",
|
|
58
|
+
list: "Promise<Settings[] | null>",
|
|
59
|
+
tree: "Tree",
|
|
60
|
+
products: "Product[]",
|
|
61
|
+
anonymous: "{ title: string }",
|
|
62
|
+
anonymousList: "{ label: string }[]",
|
|
63
|
+
strings: "string[]",
|
|
64
|
+
directOmit: 'Omit<Config, "title">',
|
|
65
|
+
generic: "Generic<string>",
|
|
66
|
+
};
|
|
67
|
+
for (const [name, output] of Object.entries(loaders)) {
|
|
68
|
+
write(
|
|
69
|
+
`src/loaders/${name}.ts`,
|
|
70
|
+
`import type { Config, Settings, Picked, Plain, Tree, Generic, Product } from "../types";
|
|
71
|
+
export default function load(): ${output} { throw new Error("fixture only"); }`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
write(
|
|
75
|
+
"src/sections/Example.tsx",
|
|
76
|
+
`import type { Config, Settings as SiteSettings, Picked, Plain, Tree,
|
|
77
|
+
Generic, Product, Collection, Unrelated } from "../types";
|
|
78
|
+
export interface Props {
|
|
79
|
+
settings: SiteSettings;
|
|
80
|
+
picked?: Picked | null;
|
|
81
|
+
plain: Plain;
|
|
82
|
+
list: SiteSettings[];
|
|
83
|
+
nullableList?: Array<SiteSettings> | null;
|
|
84
|
+
tree: Tree;
|
|
85
|
+
products: Product[];
|
|
86
|
+
collections: Collection[];
|
|
87
|
+
anonymous: { title: string };
|
|
88
|
+
anonymousList: { other: number }[];
|
|
89
|
+
strings: string[];
|
|
90
|
+
unrelated: Unrelated;
|
|
91
|
+
directOmit: Omit<Config, "modules">;
|
|
92
|
+
generic: Generic<number>;
|
|
93
|
+
}
|
|
94
|
+
export default function Example(_props: Props) { return null; }`,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const run = spawnSync(
|
|
98
|
+
process.execPath,
|
|
99
|
+
[
|
|
100
|
+
"--import",
|
|
101
|
+
import.meta.resolve("tsx"),
|
|
102
|
+
path.resolve(__dirname, "generate-schema.ts"),
|
|
103
|
+
"--skip-apps",
|
|
104
|
+
],
|
|
105
|
+
{ cwd: fixture, encoding: "utf8", timeout: 30_000 },
|
|
106
|
+
);
|
|
107
|
+
expect(run.status, run.error?.message ?? run.stderr).toBe(0);
|
|
108
|
+
const meta = JSON.parse(readFileSync(path.join(fixture, ".deco/meta.gen.json"), "utf8"));
|
|
109
|
+
const section = meta.schema.definitions[btoa("site/sections/Example.tsx")];
|
|
110
|
+
const ref = section.allOf.find((part: Schema) => part.$ref).$ref;
|
|
111
|
+
props = meta.schema.definitions[ref.replace("#/definitions/", "")];
|
|
112
|
+
}, 30_000);
|
|
113
|
+
|
|
114
|
+
afterAll(() => {
|
|
115
|
+
if (fixture) rmSync(fixture, { recursive: true, force: true });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
function field(name: string): Schema {
|
|
119
|
+
const schema = props.properties?.[name];
|
|
120
|
+
expect(schema, name).toBeDefined();
|
|
121
|
+
return schema!;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function inline(schema: Schema): Schema {
|
|
125
|
+
return schema.anyOf?.find((branch) => branch.type) ?? schema;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function expectLoader(schema: Schema, name: string) {
|
|
129
|
+
expect(schema.anyOf).toEqual(expect.arrayContaining([{ $ref: "#/definitions/Resolvable" }]));
|
|
130
|
+
expect(schema.anyOf).toContainEqual({
|
|
131
|
+
$ref: `#/definitions/${btoa(`site/loaders/${name}.ts`)}`,
|
|
132
|
+
});
|
|
133
|
+
expect(schema.properties).toBeUndefined();
|
|
134
|
+
expect(schema.anyOf).not.toContainEqual({
|
|
135
|
+
$ref: `#/definitions/${btoa("site/loaders/anonymousList.ts")}`,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
it("matches an imported Omit alias, including a nullable async loader", () => {
|
|
140
|
+
expectLoader(field("settings"), "settings");
|
|
141
|
+
expectLoader(field("settings"), "nullable");
|
|
142
|
+
expect(props.required).toContain("settings");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("matches Pick and plain object aliases without changing optionality", () => {
|
|
146
|
+
expectLoader(field("picked"), "picked");
|
|
147
|
+
expectLoader(field("plain"), "plain");
|
|
148
|
+
expect(props.required).not.toContain("picked");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("matches aliases through arrays, Promise and nullable wrappers", () => {
|
|
152
|
+
expectLoader(field("list"), "list");
|
|
153
|
+
expectLoader(field("nullableList"), "list");
|
|
154
|
+
expect(props.required).not.toContain("nullableList");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("matches a recursive alias without expanding its fields", () => {
|
|
158
|
+
expectLoader(field("tree"), "tree");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("keeps named-interface array loaders working", () => {
|
|
162
|
+
expectLoader(field("products"), "products");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("keeps unrelated object arrays editable and resolves their nested settings", () => {
|
|
166
|
+
const collections = inline(field("collections"));
|
|
167
|
+
expect(collections.type).toBe("array");
|
|
168
|
+
expect(collections.items?.properties?.label.type).toBe("string");
|
|
169
|
+
expectLoader(collections.items!.properties!.settings, "settings");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("does not collapse unrelated anonymous arrays into an __type[] bucket", () => {
|
|
173
|
+
expect(inline(field("anonymousList")).items?.properties?.other.type).toBe("number");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("does not register generic utility types or match different instantiations", () => {
|
|
177
|
+
expect(inline(field("directOmit")).properties?.title.type).toBe("string");
|
|
178
|
+
expect(inline(field("generic")).properties?.value.type).toBe("number");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("does not match primitives, anonymous objects or unrelated named aliases", () => {
|
|
182
|
+
expect(inline(field("strings")).items?.type).toBe("string");
|
|
183
|
+
expect(inline(field("anonymous")).properties?.title.type).toBe("string");
|
|
184
|
+
expect(inline(field("unrelated")).properties?.title.type).toBe("string");
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -156,7 +156,7 @@ describe("typeToJsonSchema with intersection types", () => {
|
|
|
156
156
|
useInMemoryFileSystem: true,
|
|
157
157
|
compilerOptions: { skipLibCheck: true },
|
|
158
158
|
});
|
|
159
|
-
// Mirrors
|
|
159
|
+
// Mirrors a production header `SiteNavigationElement` recursive workaround:
|
|
160
160
|
// nested children written as `Leaf & { children?: Array<…> }`. Before the
|
|
161
161
|
// intersection branch these collapsed to `children: { items: { type: "string" } }`.
|
|
162
162
|
const sf = project.createSourceFile(
|
|
@@ -217,7 +217,7 @@ describe("typeToJsonSchema Section-typed props", () => {
|
|
|
217
217
|
});
|
|
218
218
|
}, 30_000);
|
|
219
219
|
|
|
220
|
-
// Regression (
|
|
220
|
+
// Regression (a production footer): a component that declares its own local
|
|
221
221
|
// `type Section = { label; items }` used to collide with the magic name and
|
|
222
222
|
// collapse into a non-editable __SECTION_REF__ picker — the footer columns
|
|
223
223
|
// showed only a drag handle, no expand arrow, no form. A concretely-shaped
|
|
@@ -369,6 +369,37 @@ interface GenerationContext {
|
|
|
369
369
|
outputTypeToLoaderKeys: Map<string, string[]>;
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
+
function namedLoaderType(type: Type | undefined): string | null {
|
|
373
|
+
if (!type) return null;
|
|
374
|
+
const name = type.getSymbol()?.getName();
|
|
375
|
+
if (
|
|
376
|
+
name &&
|
|
377
|
+
name !== "__type" &&
|
|
378
|
+
name !== "__object" &&
|
|
379
|
+
name !== "Array" &&
|
|
380
|
+
name !== "ReadonlyArray"
|
|
381
|
+
) {
|
|
382
|
+
return name;
|
|
383
|
+
}
|
|
384
|
+
// Mapped/object aliases have a synthetic symbol but retain their own name.
|
|
385
|
+
// Bare Omit<T, K>/Generic<T> must not share a bucket across instantiations.
|
|
386
|
+
const alias = type.getAliasSymbol();
|
|
387
|
+
return alias && type.getAliasTypeArguments().length === 0 ? alias.getName() : null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Same bounded name lookup for loader outputs and section props; no shape matching. */
|
|
391
|
+
function loaderTypeName(type: Type): string | null {
|
|
392
|
+
if (type.isUnion()) {
|
|
393
|
+
const nonNull = type.getUnionTypes().filter((t) => !t.isNull() && !t.isUndefined());
|
|
394
|
+
if (nonNull.length === 1) type = nonNull[0];
|
|
395
|
+
}
|
|
396
|
+
if (type.isArray()) {
|
|
397
|
+
const name = namedLoaderType(type.getArrayElementType());
|
|
398
|
+
return name ? `${name}[]` : null;
|
|
399
|
+
}
|
|
400
|
+
return namedLoaderType(type);
|
|
401
|
+
}
|
|
402
|
+
|
|
372
403
|
/**
|
|
373
404
|
* Extract the return type name of a loader's default export.
|
|
374
405
|
* Unwraps Promise<T> and T | null wrappers.
|
|
@@ -383,25 +414,7 @@ function extractLoaderOutputTypeName(sourceFile: SourceFile): string | null {
|
|
|
383
414
|
const args = ret.getTypeArguments();
|
|
384
415
|
if (args.length) ret = args[0];
|
|
385
416
|
}
|
|
386
|
-
|
|
387
|
-
const nonNull = ret.getUnionTypes().filter((t) => !t.isNull() && !t.isUndefined());
|
|
388
|
-
if (nonNull.length === 1) ret = nonNull[0];
|
|
389
|
-
}
|
|
390
|
-
// Unwrap array element type — Product[] → "Product[]" (keyed as array).
|
|
391
|
-
// When the element type has no resolvable name (primitives like `string[]`,
|
|
392
|
-
// or opaque types like `VNode[]`), there is NO meaningful output-type key.
|
|
393
|
-
// Returning the generic `Array` symbol here would bucket the loader under
|
|
394
|
-
// "Array", which then collides with EVERY array-typed section prop
|
|
395
|
-
// (`Collection[]`, `Tab[]`, …) and wrongly turns them into loader pickers.
|
|
396
|
-
if (ret.isArray()) {
|
|
397
|
-
const elType = ret.getArrayElementType();
|
|
398
|
-
const elName = elType?.getSymbol()?.getName() ?? elType?.getAliasSymbol()?.getName() ?? null;
|
|
399
|
-
return elName ? `${elName}[]` : null;
|
|
400
|
-
}
|
|
401
|
-
// Reject anonymous object returns (`{ … }` → symbol name "__type"): they are
|
|
402
|
-
// not a nameable output type and would over-match anonymous-object props.
|
|
403
|
-
const name = ret.getSymbol()?.getName() ?? ret.getAliasSymbol()?.getName() ?? null;
|
|
404
|
-
return name && name !== "__type" ? name : null;
|
|
417
|
+
return loaderTypeName(ret);
|
|
405
418
|
}
|
|
406
419
|
|
|
407
420
|
export function typeToJsonSchema(type: Type, visited = new Set<string>(), ctx?: GenerationContext): any {
|
|
@@ -619,32 +632,10 @@ export function typeToJsonSchema(type: Type, visited = new Set<string>(), ctx?:
|
|
|
619
632
|
// Loader output type → block-ref: emit anyOf [Resolvable, ...matchingLoaders]
|
|
620
633
|
// baseHint strips "| null | undefined" so "ProductListingPage | null" → "ProductListingPage"
|
|
621
634
|
if (ctx?.outputTypeToLoaderKeys) {
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
// text (`baseHint`, e.g. "Collection[]") instead. The element-based
|
|
626
|
-
// `${elName}[]` lookup below handles arrays precisely.
|
|
627
|
-
const symName = typeSym?.getName();
|
|
628
|
-
const outputTypeName = symName && symName !== "Array" ? symName : baseHint;
|
|
629
|
-
let matchingLoaders =
|
|
630
|
-
ctx.outputTypeToLoaderKeys.get(outputTypeName) ??
|
|
635
|
+
const outputTypeName = loaderTypeName(propType);
|
|
636
|
+
const matchingLoaders =
|
|
637
|
+
(outputTypeName ? ctx.outputTypeToLoaderKeys.get(outputTypeName) : undefined) ??
|
|
631
638
|
(outputTypeName !== baseHint ? ctx.outputTypeToLoaderKeys.get(baseHint) : undefined);
|
|
632
|
-
// If no match yet and the prop type is an array, try element type name + "[]"
|
|
633
|
-
if (!matchingLoaders?.length) {
|
|
634
|
-
let arrayElementType = propType.isArray() ? propType.getArrayElementType() : null;
|
|
635
|
-
if (!arrayElementType && propType.isUnion()) {
|
|
636
|
-
const nonNull = propType.getUnionTypes().filter((t) => !t.isNull() && !t.isUndefined());
|
|
637
|
-
if (nonNull.length === 1 && nonNull[0].isArray()) {
|
|
638
|
-
arrayElementType = nonNull[0].getArrayElementType();
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
if (arrayElementType) {
|
|
642
|
-
const elName = arrayElementType.getSymbol()?.getName() ?? arrayElementType.getAliasSymbol()?.getName();
|
|
643
|
-
if (elName) {
|
|
644
|
-
matchingLoaders = ctx.outputTypeToLoaderKeys.get(`${elName}[]`);
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
639
|
if (matchingLoaders?.length) {
|
|
649
640
|
const blockRefSchema: any = {
|
|
650
641
|
anyOf: [
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Verifies the operationally important behavior: a co-located test/spec/
|
|
11
11
|
* stories/gen file sitting next to a real section must never be walked into
|
|
12
|
-
* sectionMeta (
|
|
12
|
+
* sectionMeta (a production-site incident this generator's `walkDir` reproduced:
|
|
13
13
|
* `sections.test.ts` became a bogus section in a site's generated output).
|
|
14
14
|
*/
|
|
15
15
|
import * as cp from "node:child_process";
|
|
@@ -273,8 +273,8 @@ describe("generate-sections neverDefer convention", () => {
|
|
|
273
273
|
// SectionMetaEntry interface the SAME file declares omitted the field —
|
|
274
274
|
// so every generated file containing a neverDefer section failed the
|
|
275
275
|
// site's typecheck (TS2353 excess property), and sites hand-patched the
|
|
276
|
-
// interface only to have the next regeneration wipe the patch (
|
|
277
|
-
// .deco/sections.gen.ts carried exactly that TODO). The emitted interface
|
|
276
|
+
// interface only to have the next regeneration wipe the patch (a production
|
|
277
|
+
// site's .deco/sections.gen.ts carried exactly that TODO). The emitted interface
|
|
278
278
|
// must match SectionMetaEntry in @decocms/blocks/cms.
|
|
279
279
|
let tmpDir: string;
|
|
280
280
|
let sectionsDir: string;
|
|
@@ -292,7 +292,7 @@ describe("generate-sections neverDefer convention", () => {
|
|
|
292
292
|
});
|
|
293
293
|
|
|
294
294
|
it("declares neverDefer on the emitted SectionMetaEntry interface and the generated file typechecks + imports", () => {
|
|
295
|
-
// Mirrors
|
|
295
|
+
// Mirrors a production site's src/sections/Product/SearchResult.tsx.
|
|
296
296
|
fs.writeFileSync(
|
|
297
297
|
path.join(sectionsDir, "SearchResult.tsx"),
|
|
298
298
|
"export const neverDefer = true;\nexport default function SearchResult() { return null; }\n",
|
|
@@ -83,7 +83,7 @@ describe("pickWinner", () => {
|
|
|
83
83
|
});
|
|
84
84
|
|
|
85
85
|
it("prefers higher decode-pass count when path-status matches", () => {
|
|
86
|
-
//
|
|
86
|
+
// A production reproduction case: a stale single-encoded leftover with a
|
|
87
87
|
// newer mtime and larger size loses to the bot's double-encoded fresh file.
|
|
88
88
|
const stale = cand({
|
|
89
89
|
file: "pages-Home%20-%20LB-618509.json",
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* only the package dependency that needs to move.
|
|
8
8
|
*
|
|
9
9
|
* Mapping derived by hand against a real production site
|
|
10
|
-
* (
|
|
10
|
+
* (a production VTEX storefront) — see the issue for the full worked table. Symbol
|
|
11
11
|
* renames and relocations mean a pure specifier-string rewrite is not
|
|
12
12
|
* enough for every case; those are called out below.
|
|
13
13
|
*/
|
|
@@ -116,7 +116,7 @@ describe("classify (pure)", () => {
|
|
|
116
116
|
});
|
|
117
117
|
|
|
118
118
|
it("treats hx-on with a fetch attr as click-swap (the fetch wins)", () => {
|
|
119
|
-
// Real example from
|
|
119
|
+
// Real example from a production htmx storefront (EmailAndPassword button): hx-get +
|
|
120
120
|
// hx-target + hx-trigger=click — engineer often piles
|
|
121
121
|
// hx-on alongside, but the dominant migration path is the
|
|
122
122
|
// click-swap recipe.
|
|
@@ -127,11 +127,11 @@ describe("classify (pure)", () => {
|
|
|
127
127
|
});
|
|
128
128
|
|
|
129
129
|
/* ------------------------------------------------------------------ */
|
|
130
|
-
/* analyzeFile() — JSX walker tests using real
|
|
130
|
+
/* analyzeFile() — JSX walker tests using real htmx-shaped fixtures */
|
|
131
131
|
/* ------------------------------------------------------------------ */
|
|
132
132
|
|
|
133
133
|
describe("analyzeFile (real shapes)", () => {
|
|
134
|
-
it("detects hx-on:click={useScript(...)} click handler (
|
|
134
|
+
it("detects hx-on:click={useScript(...)} click handler (AddToBagButton shape)", () => {
|
|
135
135
|
const file = `
|
|
136
136
|
import { useScript } from "@deco/deco/hooks";
|
|
137
137
|
export default function AddToBagButton() {
|
|
@@ -157,7 +157,7 @@ export default function AddToBagButton() {
|
|
|
157
157
|
expect(out[0].attrs).toEqual(["hx-on:click"]);
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
-
it("detects hx-post + hx-target + hx-trigger=keyup on an input as auto-fetch (
|
|
160
|
+
it("detects hx-post + hx-target + hx-trigger=keyup on an input as auto-fetch (SearchInput shape)", () => {
|
|
161
161
|
const file = `
|
|
162
162
|
<input
|
|
163
163
|
id={searchInputId}
|
|
@@ -181,7 +181,7 @@ export default function AddToBagButton() {
|
|
|
181
181
|
expect(out[0].attrs).toContain("hx-target");
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
-
it("detects hx-post + hx-target + hx-swap on a form as form-swap (
|
|
184
|
+
it("detects hx-post + hx-target + hx-swap on a form as form-swap (EmailAndPassword shape)", () => {
|
|
185
185
|
const file = `
|
|
186
186
|
<form
|
|
187
187
|
class="flex flex-col w-full"
|
|
@@ -202,7 +202,7 @@ export default function AddToBagButton() {
|
|
|
202
202
|
expect(out[0].attrs).toContain("hx-target");
|
|
203
203
|
});
|
|
204
204
|
|
|
205
|
-
it("detects hx-get on a button as click-swap (
|
|
205
|
+
it("detects hx-get on a button as click-swap (ForgotPassword shape)", () => {
|
|
206
206
|
const file = `
|
|
207
207
|
<button
|
|
208
208
|
type="button"
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Looks for `.deco-migrate.config.json` next to the source root. The file
|
|
5
5
|
* is optional — without it the script falls back to a baked-in default set
|
|
6
|
-
* of section-convention names that work for `
|
|
6
|
+
* of section-convention names that work for `acme` and most other
|
|
7
7
|
* Deco/VTEX sites that derived from the same template.
|
|
8
8
|
*
|
|
9
9
|
* The defaults are kept here (not in `transforms/section-conventions.ts`)
|
|
@@ -61,7 +61,7 @@ export interface SectionConventionSets {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
* Built-in defaults. Originally extracted from
|
|
64
|
+
* Built-in defaults. Originally extracted from a production migration —
|
|
65
65
|
* these names are common across Deco/VTEX storefronts that share the
|
|
66
66
|
* lineage. Sites that don't have these sections are unaffected (the
|
|
67
67
|
* matcher just never fires).
|
|
@@ -10,7 +10,7 @@ import { extractGoogleFonts, extractPlatform } from "./phase-analyze";
|
|
|
10
10
|
// 3. apps/site.ts loose string match against platform name + "platform"
|
|
11
11
|
// 4. .deco/blocks filenames hinting at platform
|
|
12
12
|
// Strategy 3 is the false-positive trap that ate Magento sites before #211:
|
|
13
|
-
//
|
|
13
|
+
// a production site's apps/site.ts imports `apps/vtex/mod.ts` for the color
|
|
14
14
|
// palette, so the content matches `"vtex"` even though the real platform is
|
|
15
15
|
// declared via `apps/magento.ts`. The fix adds "magento" to the platforms
|
|
16
16
|
// list so Strategy 2 fires first and short-circuits before Strategy 3 can
|
|
@@ -90,7 +90,7 @@ describe("extractPlatform", () => {
|
|
|
90
90
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
it("detects magento from apps/magento.ts (
|
|
93
|
+
it("detects magento from apps/magento.ts (a production site shape)", () => {
|
|
94
94
|
fs.writeFileSync(path.join(tmp, "apps", "magento.ts"), "export default {};\n");
|
|
95
95
|
// Also write a site.ts that imports vtex for color palettes — this is
|
|
96
96
|
// what tripped Strategy 3 before #211.
|
|
@@ -113,7 +113,7 @@ describe("extractPlatform", () => {
|
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
it("prefers magento over vtex when both an apps/magento.ts AND a vtex string match exist", () => {
|
|
116
|
-
// Reproduce the exact
|
|
116
|
+
// Reproduce the exact production trap: apps/magento.ts is the real signal,
|
|
117
117
|
// but apps/site.ts loosely contains "vtex" + "platform".
|
|
118
118
|
fs.writeFileSync(path.join(tmp, "apps", "magento.ts"), "export default {};\n");
|
|
119
119
|
fs.writeFileSync(
|
|
@@ -41,7 +41,7 @@ const PATTERN_DETECTORS: Array<[DetectedPattern, RegExp]> = [
|
|
|
41
41
|
["head-component", /<Head[\s>]/],
|
|
42
42
|
["define-app", /defineApp\(/],
|
|
43
43
|
["invoke-proxy", /proxy<Manifest/],
|
|
44
|
-
//
|
|
44
|
+
// legacy HTMX-style dynamic-section loader. Both the source file and
|
|
45
45
|
// every call site need manual conversion to React state / createServerFn.
|
|
46
46
|
["sections-component-loader", /sections\/Component\.tsx?$/m],
|
|
47
47
|
["use-component", /import\s*\{[^}]*\buseComponent\b[^}]*\}\s*from\s*["'][^"']*sections\/Component(?:\.tsx?)?["']/],
|
|
@@ -270,7 +270,7 @@ function decideAction(
|
|
|
270
270
|
};
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
//
|
|
273
|
+
// legacy HTMX-style dynamic-section loader → delete and flag.
|
|
274
274
|
// The file uses Deno-only APIs (`toFileUrl(Deno.cwd())`, `import.meta.resolve`)
|
|
275
275
|
// and the `useComponent(component, props)` HTMX render-and-swap pattern, none
|
|
276
276
|
// of which work on TanStack Start / Cloudflare Workers. The site author must
|
|
@@ -430,7 +430,7 @@ function extractVtexAccount(sourceDir: string): string | null {
|
|
|
430
430
|
const fp = path.join(sourceDir, candidate);
|
|
431
431
|
if (!fs.existsSync(fp)) continue;
|
|
432
432
|
const content = fs.readFileSync(fp, "utf-8");
|
|
433
|
-
// Match patterns like:
|
|
433
|
+
// Match patterns like: acme: "acmenewio"
|
|
434
434
|
const match = content.match(/vtexAn|VTEX_AN/i);
|
|
435
435
|
if (match) {
|
|
436
436
|
const dictMatch = content.match(/["'](\w+)["']\s*:\s*["'](\w+myvtex|\w+newio|\w+)["']/);
|
|
@@ -718,7 +718,7 @@ export function analyze(ctx: MigrationContext): void {
|
|
|
718
718
|
f.path === "sections/Component.ts",
|
|
719
719
|
);
|
|
720
720
|
if (componentLoaderFile || useComponentSites.length > 0) {
|
|
721
|
-
console.log("\n ⚠ HTMX dynamic-section loader detected (
|
|
721
|
+
console.log("\n ⚠ HTMX dynamic-section loader detected (legacy HTMX-style)");
|
|
722
722
|
if (componentLoaderFile) {
|
|
723
723
|
console.log(` • ${componentLoaderFile.path} (will be deleted)`);
|
|
724
724
|
}
|
|
@@ -627,7 +627,7 @@ function guardUndeclaredVariables(src: string, onFix: (msg: string) => void): st
|
|
|
627
627
|
/**
|
|
628
628
|
* Migrate account.json → src/constants/account.ts and rewrite imports.
|
|
629
629
|
*
|
|
630
|
-
* Old stack: root-level `account.json` containing e.g. `"
|
|
630
|
+
* Old stack: root-level `account.json` containing e.g. `"acme"`
|
|
631
631
|
* New stack: `src/constants/account.ts` exporting `accountName`
|
|
632
632
|
*
|
|
633
633
|
* Also rewrites every file that imports from `account.json` (via
|
|
@@ -31,7 +31,7 @@ function getSectionMeta(ctx: MigrationContext, relPath: string): SectionMeta | u
|
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Cached per-run section-conventions closure. Built once from the
|
|
34
|
-
* resolved config sets (`ctx.config.sectionConventions`), so
|
|
34
|
+
* resolved config sets (`ctx.config.sectionConventions`), so the built-in
|
|
35
35
|
* defaults still apply when no config file exists.
|
|
36
36
|
*/
|
|
37
37
|
let cachedSectionTransform:
|
|
@@ -227,7 +227,7 @@ export function transform(ctx: MigrationContext): void {
|
|
|
227
227
|
"that ships HTML fragments and swaps them client-side. It does not work on TanStack Start. " +
|
|
228
228
|
"Recipes: " +
|
|
229
229
|
"(1) Self-contained UI toggles → keep state in React (`useState` + event handlers); " +
|
|
230
|
-
"(2) Form submissions / mutations → `createServerFn` + `useMutation` (see
|
|
230
|
+
"(2) Form submissions / mutations → `createServerFn` + `useMutation` (see a production storefront for canonical examples); " +
|
|
231
231
|
"(3) Ad-hoc data fetches → call the loader/action via `~/server/invoke` and store results in `useState`. " +
|
|
232
232
|
"Remove the import after refactoring, then delete `src/sections/Component.tsx`.",
|
|
233
233
|
severity: "error",
|
|
@@ -1090,8 +1090,8 @@ export const FRAMEWORK_DUPLICATES: FrameworkDuplicate[] = [
|
|
|
1090
1090
|
sitePath: "src/sdk/useSuggestions.ts",
|
|
1091
1091
|
canonicalImport: "@decocms/blocks/sdk/useSuggestions",
|
|
1092
1092
|
// Fingerprint: hand-rolled hook with the module-level signal +
|
|
1093
|
-
// serial-queue + latestQuery cancel pattern.
|
|
1094
|
-
//
|
|
1093
|
+
// serial-queue + latestQuery cancel pattern. Multiple production sites
|
|
1094
|
+
// independently invented this exact shape. Sites that
|
|
1095
1095
|
// already adopted `createUseSuggestions(…)` factory calls won't
|
|
1096
1096
|
// match this signature.
|
|
1097
1097
|
contentSignature: [
|
|
@@ -183,7 +183,7 @@ describe("classifyShimExports — type/interface declarations", () => {
|
|
|
183
183
|
});
|
|
184
184
|
});
|
|
185
185
|
|
|
186
|
-
describe("classifyShimExports — real
|
|
186
|
+
describe("classifyShimExports — real production storefront fixtures", () => {
|
|
187
187
|
it("vtex-segment.ts (mixed: stub + functional)", () => {
|
|
188
188
|
const code = `
|
|
189
189
|
export function getSegmentFromBag(_req?: any): Record<string, unknown> | null {
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
* would need real semantic analysis. False negatives are tolerable; the
|
|
27
27
|
* rule still warns when *any* imported symbol from the shim is a clear
|
|
28
28
|
* stub, which is enough to surface the real-world regressions we've
|
|
29
|
-
* actually seen on production sites (
|
|
29
|
+
* actually seen on production sites (e.g. `getSegmentFromBag`,
|
|
30
30
|
* `getISCookiesFromBag`, `toProduct`).
|
|
31
31
|
*
|
|
32
32
|
* Implementation note — string parsing, not a real TypeScript AST. The
|
|
33
|
-
* shim files are tiny by design (the
|
|
33
|
+
* shim files are tiny by design (the real ones are 1-39 lines).
|
|
34
34
|
* A balanced-brace body extractor + small set of stub patterns covers
|
|
35
35
|
* every case observed on real sites. If this ever needs to handle
|
|
36
36
|
* decorators, generics on consts, or weirder JSX forms, the right move
|
|
@@ -45,9 +45,9 @@ describe("generateCiFiles", () => {
|
|
|
45
45
|
expect(f[".github/workflows/ci.yml"]).toContain('BUN_VERSION: "1.3.5"');
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
it("is de-projectized — no
|
|
48
|
+
it("is de-projectized — no site-specific migration-debt refs", () => {
|
|
49
49
|
const blob = Object.values(files).join("\n");
|
|
50
|
-
expect(blob).not.toMatch(/
|
|
50
|
+
expect(blob).not.toMatch(/sitea|siteb/i);
|
|
51
51
|
expect(blob).not.toMatch(/#7[89]|#81/); // issues #78/#79/#81
|
|
52
52
|
expect(blob).not.toMatch(/AGENTS\.md/);
|
|
53
53
|
});
|
|
@@ -156,7 +156,7 @@ describe("generateSyncBlocksBotYml", () => {
|
|
|
156
156
|
});
|
|
157
157
|
|
|
158
158
|
it("is de-projectized — no real site/customer names", () => {
|
|
159
|
-
expect(yml).not.toMatch(/
|
|
159
|
+
expect(yml).not.toMatch(/sitea|acme|siteb/i);
|
|
160
160
|
});
|
|
161
161
|
|
|
162
162
|
it("runs the site's own installed CLI, or a pinned one when given", () => {
|
|
@@ -58,8 +58,8 @@ describe("generateMigrationPolicyPointerRule", () => {
|
|
|
58
58
|
it("escapes nothing weird from siteName — siteName is used as a label only", () => {
|
|
59
59
|
// We don't sanitise; we trust the migration script to pass a real
|
|
60
60
|
// package name. But verify nothing surprising happens with hyphens
|
|
61
|
-
// (a common shape, e.g. "
|
|
62
|
-
const out = generateMigrationPolicyPointerRule("
|
|
63
|
-
expect(out).toContain("`
|
|
61
|
+
// (a common shape, e.g. "acme-storefront").
|
|
62
|
+
const out = generateMigrationPolicyPointerRule("acme-storefront");
|
|
63
|
+
expect(out).toContain("`acme-storefront`");
|
|
64
64
|
});
|
|
65
65
|
});
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - Most sites end up importing zero of these (apps-start exports
|
|
10
10
|
* direct equivalents for most VTEX utilities — see migrate#107).
|
|
11
11
|
* - Eager generation creates dead `src/lib/*.ts` files that every site
|
|
12
|
-
* then has to clean up by hand (
|
|
12
|
+
* then has to clean up by hand (~235 LOC on one production migration).
|
|
13
13
|
*
|
|
14
14
|
* Registry shape: `"src/lib/<name>.ts"` → file contents.
|
|
15
15
|
*/
|
|
@@ -41,8 +41,8 @@ export function selectImportedLibTemplates(
|
|
|
41
41
|
// Per the migration tooling policy (D3 — Throwing stubs):
|
|
42
42
|
// generated stubs MUST throw at runtime so the first call surfaces the
|
|
43
43
|
// gap loudly. Silent identity-cast `toProduct` was the bug behind
|
|
44
|
-
//
|
|
45
|
-
// for weeks before anyone noticed).
|
|
44
|
+
// a real production incident (PDP product data was being dropped on the
|
|
45
|
+
// floor for weeks before anyone noticed).
|
|
46
46
|
//
|
|
47
47
|
// Each thrown message points at the canonical replacement so the fix
|
|
48
48
|
// is mechanical. `deco-post-cleanup --fix` automates the swap.
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* @param workerName Cloudflare Worker name (= wrangler `name` field, e.g. "
|
|
22
|
+
* @param workerName Cloudflare Worker name (= wrangler `name` field, e.g. "my-site-tanstack").
|
|
23
23
|
* Used in the CF check-run name and the main preview base URL.
|
|
24
24
|
*/
|
|
25
25
|
export function generatePerfFiles(workerName: string): Record<string, string> {
|
|
@@ -139,8 +139,8 @@ export function generateSectionLoaders(ctx: MigrationContext): string {
|
|
|
139
139
|
// returned.
|
|
140
140
|
//
|
|
141
141
|
// The previous template chose mixins XOR own-loader and silently dropped
|
|
142
|
-
// the section's loader when both were present — see
|
|
143
|
-
//
|
|
142
|
+
// the section's loader when both were present — see a production
|
|
143
|
+
// storefront's SearchContainer SSR regression.
|
|
144
144
|
for (const meta of ctx.sectionMetas) {
|
|
145
145
|
if (meta.isHeader || meta.isAccountSection || meta.isStatusOnly) continue;
|
|
146
146
|
// Skip sections with no loader AND no device needs
|
|
@@ -418,7 +418,7 @@ export function generateSectionLoaders(ctx: MigrationContext): string {
|
|
|
418
418
|
// ---------- Privacy cookie check ----------
|
|
419
419
|
if (isVtex && hasPrivacyPolice) {
|
|
420
420
|
entries.push(``);
|
|
421
|
-
const vtexAccount = ctx.vtexAccount || "
|
|
421
|
+
const vtexAccount = ctx.vtexAccount || "acme";
|
|
422
422
|
entries.push(` "site/sections/Account/PrivacyPolice.tsx": (props: any, req) => {`);
|
|
423
423
|
entries.push(` const cookies = req.headers.get("cookie") ?? "";`);
|
|
424
424
|
entries.push(` const logged = cookies.includes("VtexIdclientAutCookie_${vtexAccount}");`);
|
|
@@ -11,7 +11,7 @@ function discoverFonts(ctx: MigrationContext): string[] {
|
|
|
11
11
|
const staticFonts = path.join(ctx.sourceDir, "static", "fonts");
|
|
12
12
|
if (fs.existsSync(staticFonts)) return scanFontDir(staticFonts);
|
|
13
13
|
|
|
14
|
-
// Check static-*/fonts/ (multi-brand sites
|
|
14
|
+
// Check static-*/fonts/ (multi-brand sites)
|
|
15
15
|
try {
|
|
16
16
|
const entries = fs.readdirSync(ctx.sourceDir, { withFileTypes: true });
|
|
17
17
|
for (const e of entries) {
|
|
@@ -11,7 +11,7 @@ import type { TransformResult } from "../types";
|
|
|
11
11
|
* *non-optional* deep read like `ctx.salesforce.cartExtension[0]` still throws
|
|
12
12
|
* — and `withSectionLoader`'s try/catch would swallow it, dropping the
|
|
13
13
|
* section's props (blank render). The hand-fixed reference migration
|
|
14
|
-
* (
|
|
14
|
+
* (a production storefront) solves this by optional-chaining every `ctx` read.
|
|
15
15
|
*
|
|
16
16
|
* This codemod reproduces that: every `ctx.` member-access chain becomes an
|
|
17
17
|
* optional chain (`ctx?.a?.b?.[0]`). Optional chaining short-circuits to
|
|
@@ -224,7 +224,7 @@ describe("transformHtmxOnEvents — idempotency + edge cases", () => {
|
|
|
224
224
|
});
|
|
225
225
|
});
|
|
226
226
|
|
|
227
|
-
describe("transformHtmxOnEvents —
|
|
227
|
+
describe("transformHtmxOnEvents — htmx-shaped fixtures", () => {
|
|
228
228
|
it("AddToBagButton: hx-on-click + Fresh useScript → onClick + TODO", () => {
|
|
229
229
|
const src = `import { useScript } from "site/sdk/useScript.ts";
|
|
230
230
|
|
|
@@ -61,8 +61,8 @@ const STANDARD_EVENT_MAP: Record<string, string> = {
|
|
|
61
61
|
* handler bodies reference Fresh-only globals (`useScript`,
|
|
62
62
|
* `globalThis.window.STOREFRONT`, `STOREFRONT.*`). The comment is the
|
|
63
63
|
* only file-level annotation the codemod emits — per-occurrence
|
|
64
|
-
* comments would balloon the diff for
|
|
65
|
-
*
|
|
64
|
+
* comments would balloon the diff for an 88-rename file like a
|
|
65
|
+
* production storefront's hot paths. It is detected by an idempotency check
|
|
66
66
|
* so re-running the codemod does not double-inject.
|
|
67
67
|
*/
|
|
68
68
|
const TODO_MARKER = "// MIGRATION TODO (codemod: htmx-on-event-rename):";
|
package/scripts/migrate/types.ts
CHANGED
|
@@ -140,7 +140,7 @@ export interface MigrationContext {
|
|
|
140
140
|
layout: import("./source-layout").SourceLayout;
|
|
141
141
|
siteName: string;
|
|
142
142
|
platform: Platform;
|
|
143
|
-
/** VTEX account name (e.g. "
|
|
143
|
+
/** VTEX account name (e.g. "acme") — extracted from source code */
|
|
144
144
|
vtexAccount: string | null;
|
|
145
145
|
gtmId: string | null;
|
|
146
146
|
|
|
@@ -46,13 +46,13 @@ describe("migrate-to-cf-observability codemod", () => {
|
|
|
46
46
|
fs.writeFileSync(
|
|
47
47
|
wranglerPath,
|
|
48
48
|
`{
|
|
49
|
-
"name": "
|
|
49
|
+
"name": "acme-tanstack",
|
|
50
50
|
"compatibility_date": "2026-02-14",
|
|
51
51
|
"main": "./src/worker-entry.ts",
|
|
52
52
|
"kv_namespaces": [{ "binding": "SITES_KV", "id": "abc" }],
|
|
53
53
|
"version_metadata": { "binding": "CF_VERSION_METADATA" },
|
|
54
54
|
"analytics_engine_datasets": [
|
|
55
|
-
{ "binding": "DECO_METRICS", "dataset": "
|
|
55
|
+
{ "binding": "DECO_METRICS", "dataset": "deco_metrics_acme" }
|
|
56
56
|
],
|
|
57
57
|
"observability": {
|
|
58
58
|
"logs": {
|
|
@@ -77,7 +77,7 @@ describe("migrate-to-cf-observability codemod", () => {
|
|
|
77
77
|
// No destinations by default.
|
|
78
78
|
expect(result).not.toContain('"destinations"');
|
|
79
79
|
// Original keys preserved.
|
|
80
|
-
expect(result).toContain('"name": "
|
|
80
|
+
expect(result).toContain('"name": "acme-tanstack"');
|
|
81
81
|
expect(result).toContain('"binding": "DECO_METRICS"');
|
|
82
82
|
// Result must be valid JSONC.
|
|
83
83
|
expect(() => JSON.parse(stripJsoncComments(result))).not.toThrow();
|
|
@@ -87,7 +87,7 @@ describe("migrate-to-cf-observability codemod", () => {
|
|
|
87
87
|
fs.writeFileSync(
|
|
88
88
|
wranglerPath,
|
|
89
89
|
`{
|
|
90
|
-
"name": "
|
|
90
|
+
"name": "acme-tanstack",
|
|
91
91
|
"main": "./src/worker-entry.ts",
|
|
92
92
|
"observability": {
|
|
93
93
|
"enabled": true,
|
|
@@ -146,7 +146,7 @@ describe("migrate-to-cf-observability codemod", () => {
|
|
|
146
146
|
fs.writeFileSync(
|
|
147
147
|
wranglerPath,
|
|
148
148
|
`{
|
|
149
|
-
"name": "
|
|
149
|
+
"name": "acme-tanstack",
|
|
150
150
|
"main": "./src/worker-entry.ts",
|
|
151
151
|
"observability": {
|
|
152
152
|
"logs": { "enabled": true }
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* Cloudflare captures nothing, regardless of the sub-block flags.
|
|
22
22
|
* `persist: true` keeps the data queryable in the CF dashboard
|
|
23
23
|
* (Workers Logs view + Traces view). Discovered the hard way during the
|
|
24
|
-
*
|
|
24
|
+
* production canary cutover.
|
|
25
25
|
*
|
|
26
26
|
* Behavior:
|
|
27
27
|
* - dry-run by default — prints a unified diff against the existing
|