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