@decocms/blocks-cli 8.0.0 → 8.1.0-next.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 +5 -3
- package/scripts/analyze-traces.mjs +1 -0
- package/scripts/cdn-rules.test.ts +105 -0
- package/scripts/cdn-rules.ts +192 -0
- package/scripts/deco-migrate-cli.ts +6 -7
- package/scripts/fast-deploy-kv.test.ts +172 -0
- package/scripts/generate-blocks.test.ts +11 -0
- package/scripts/generate-blocks.ts +0 -10
- package/scripts/generate-invoke.test.ts +2 -2
- package/scripts/generate-invoke.ts +9 -21
- package/scripts/generate-loaders.test.ts +2 -2
- package/scripts/generate-loaders.ts +0 -10
- package/scripts/generate-schema-loader-aliases.test.ts +186 -0
- package/scripts/generate-schema-workspaces.test.ts +180 -0
- package/scripts/generate-schema.test.ts +68 -29
- package/scripts/generate-schema.ts +101 -89
- package/scripts/generate-sections.test.ts +104 -35
- package/scripts/generate-sections.ts +57 -11
- package/scripts/generate-workspaces.test.ts +179 -0
- package/scripts/generate.test.ts +1 -2
- package/scripts/generate.ts +43 -45
- package/scripts/lib/blocks-dedupe.test.ts +1 -1
- package/scripts/lib/cf-kv-rest.ts +49 -1
- package/scripts/lib/installedPackages.ts +42 -0
- package/scripts/lib/invokeSource.ts +18 -0
- package/scripts/lib/kv-snapshot.ts +44 -0
- package/scripts/lib/read-decofile.ts +13 -2
- package/scripts/lib/upgrade-6-to-7.ts +1 -1
- package/scripts/lib/wrangler-config.test.ts +49 -0
- package/scripts/lib/wrangler-config.ts +31 -0
- package/scripts/migrate/analyzers/htmx-analyze.test.ts +6 -6
- package/scripts/migrate/analyzers/tailwind-config.ts +95 -1
- package/scripts/migrate/config.ts +2 -2
- package/scripts/migrate/delete-sets.ts +75 -0
- package/scripts/migrate/fast-deploy-scaffold.test.ts +130 -0
- package/scripts/migrate/icon-casing.test.ts +63 -0
- package/scripts/migrate/phase-analyze.test.ts +66 -4
- package/scripts/migrate/phase-analyze.ts +79 -60
- package/scripts/migrate/phase-cleanup.test.ts +3 -1
- package/scripts/migrate/phase-cleanup.ts +1 -1
- package/scripts/migrate/phase-compile.test.ts +3 -1
- package/scripts/migrate/phase-report.test.ts +2 -0
- package/scripts/migrate/phase-report.ts +3 -4
- package/scripts/migrate/phase-scaffold.ts +150 -45
- package/scripts/migrate/phase-transform.ts +107 -2
- package/scripts/migrate/phase-verify-legacy-specifiers.test.ts +13 -0
- package/scripts/migrate/phase-verify.test.ts +3 -1
- package/scripts/migrate/phase-verify.ts +87 -33
- package/scripts/migrate/post-cleanup/rules.ts +29 -11
- package/scripts/migrate/post-cleanup/shim-classify.test.ts +1 -1
- package/scripts/migrate/post-cleanup/shim-classify.ts +2 -2
- package/scripts/migrate/source-layout.test.ts +43 -0
- package/scripts/migrate/source-layout.ts +32 -5
- package/scripts/migrate/templates/app-css.test.ts +24 -0
- package/scripts/migrate/templates/app-css.ts +27 -0
- package/scripts/migrate/templates/ci-workflows.test.ts +179 -0
- package/scripts/migrate/templates/ci-yml.ts +182 -0
- package/scripts/migrate/templates/cursor-rules.test.ts +3 -3
- package/scripts/migrate/templates/hooks.test.ts +3 -1
- package/scripts/migrate/templates/lib-utils.ts +3 -3
- package/scripts/migrate/templates/main-push-guard-yml.ts +45 -0
- package/scripts/migrate/templates/package-json.ts +10 -0
- package/scripts/migrate/templates/parity-yml.ts +136 -0
- package/scripts/migrate/templates/perf-yml.ts +449 -0
- package/scripts/migrate/templates/playwright-yml.ts +127 -0
- package/scripts/migrate/templates/react-doctor-yml.ts +46 -0
- package/scripts/migrate/templates/routes.test.ts +31 -0
- package/scripts/migrate/templates/routes.ts +19 -9
- package/scripts/migrate/templates/section-loaders.ts +3 -3
- package/scripts/migrate/templates/server-entry-vtex-invoke.test.ts +19 -0
- package/scripts/migrate/templates/server-entry.ts +19 -14
- package/scripts/migrate/templates/setup.ts +11 -3
- package/scripts/migrate/templates/sync-blocks-bot-yml.ts +201 -0
- package/scripts/migrate/templates/ui-components.ts +3 -0
- package/scripts/migrate/transforms/ctx-compat.test.ts +37 -0
- package/scripts/migrate/transforms/ctx-compat.ts +93 -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/transforms/jsx-class-type.test.ts +28 -0
- package/scripts/migrate/transforms/jsx.ts +10 -0
- package/scripts/migrate/transforms/picture.test.ts +56 -0
- package/scripts/migrate/transforms/picture.ts +95 -0
- package/scripts/migrate/transforms/tailwind-renames.test.ts +29 -0
- package/scripts/migrate/transforms/tailwind-renames.ts +34 -0
- package/scripts/migrate/transforms/timer-types.test.ts +30 -0
- package/scripts/migrate/transforms/timer-types.ts +48 -0
- package/scripts/migrate/transforms/use-script-handlers.test.ts +37 -0
- package/scripts/migrate/transforms/use-script-handlers.ts +36 -0
- package/scripts/migrate/types.ts +13 -1
- package/scripts/migrate-blocks-to-kv.ts +16 -2
- package/scripts/migrate-to-cf-observability.test.ts +5 -5
- package/scripts/migrate-to-cf-observability.ts +1 -1
- package/scripts/migrate.ts +244 -6
- package/scripts/reconcile.test.ts +162 -0
- package/scripts/reconcile.ts +303 -0
- package/scripts/sync-blocks-bot.test.ts +276 -0
- package/scripts/sync-blocks-bot.ts +567 -0
- package/scripts/sync-blocks-to-kv.ts +20 -2
- package/scripts/upgrade-6-to-7.ts +0 -0
- package/scripts/lib/legacyArtifact.ts +0 -20
|
@@ -11,6 +11,14 @@ import { extractSectionMetadata } from "./analyzers/section-metadata";
|
|
|
11
11
|
import { classifyIslands } from "./analyzers/island-classifier";
|
|
12
12
|
import { inventoryLoaders } from "./analyzers/loader-inventory";
|
|
13
13
|
import { extractTailwindConfig } from "./analyzers/tailwind-config";
|
|
14
|
+
import {
|
|
15
|
+
COMPONENT_DELETE,
|
|
16
|
+
GENERATED_FILES,
|
|
17
|
+
LOADER_DELETE,
|
|
18
|
+
ROOT_DELETE,
|
|
19
|
+
SDK_DELETE,
|
|
20
|
+
STATIC_DELETE,
|
|
21
|
+
} from "./delete-sets";
|
|
14
22
|
|
|
15
23
|
const PATTERN_DETECTORS: Array<[DetectedPattern, RegExp]> = [
|
|
16
24
|
["preact-hooks", /from\s+["']preact\/hooks["']/],
|
|
@@ -33,7 +41,7 @@ const PATTERN_DETECTORS: Array<[DetectedPattern, RegExp]> = [
|
|
|
33
41
|
["head-component", /<Head[\s>]/],
|
|
34
42
|
["define-app", /defineApp\(/],
|
|
35
43
|
["invoke-proxy", /proxy<Manifest/],
|
|
36
|
-
//
|
|
44
|
+
// legacy HTMX-style dynamic-section loader. Both the source file and
|
|
37
45
|
// every call site need manual conversion to React state / createServerFn.
|
|
38
46
|
["sections-component-loader", /sections\/Component\.tsx?$/m],
|
|
39
47
|
["use-component", /import\s*\{[^}]*\buseComponent\b[^}]*\}\s*from\s*["'][^"']*sections\/Component(?:\.tsx?)?["']/],
|
|
@@ -78,61 +86,6 @@ const SKIP_FILES = new Set([
|
|
|
78
86
|
"bun.lockb",
|
|
79
87
|
]);
|
|
80
88
|
|
|
81
|
-
/** Files that are generated and should be deleted */
|
|
82
|
-
const GENERATED_FILES = new Set([
|
|
83
|
-
"fresh.gen.ts",
|
|
84
|
-
"manifest.gen.ts",
|
|
85
|
-
"fresh.config.ts",
|
|
86
|
-
]);
|
|
87
|
-
|
|
88
|
-
/** SDK files that have framework equivalents or are scaffolded fresh */
|
|
89
|
-
const SDK_DELETE = new Set([
|
|
90
|
-
"sdk/clx.ts",
|
|
91
|
-
"sdk/useId.ts",
|
|
92
|
-
// sdk/useOffer.ts — kept: sites often customize offer logic
|
|
93
|
-
// sdk/useVariantPossiblities.ts — kept: sites often customize variant logic
|
|
94
|
-
"sdk/usePlatform.tsx",
|
|
95
|
-
"sdk/signal.ts",
|
|
96
|
-
"sdk/format.ts",
|
|
97
|
-
]);
|
|
98
|
-
|
|
99
|
-
/** Component files that are scaffolded fresh (old versions must not overwrite) */
|
|
100
|
-
const COMPONENT_DELETE = new Set([
|
|
101
|
-
"components/ui/Image.tsx",
|
|
102
|
-
"components/ui/Picture.tsx",
|
|
103
|
-
"components/ui/Video.tsx",
|
|
104
|
-
]);
|
|
105
|
-
|
|
106
|
-
/** Loaders that depend on deleted admin tooling */
|
|
107
|
-
const LOADER_DELETE = new Set([
|
|
108
|
-
"loaders/availableIcons.ts",
|
|
109
|
-
"loaders/icons.ts",
|
|
110
|
-
]);
|
|
111
|
-
|
|
112
|
-
/** Root config/infra files to delete */
|
|
113
|
-
const ROOT_DELETE = new Set([
|
|
114
|
-
"main.ts",
|
|
115
|
-
"dev.ts",
|
|
116
|
-
"deno.json",
|
|
117
|
-
"deno.lock",
|
|
118
|
-
"tailwind.css",
|
|
119
|
-
"tailwind.config.ts",
|
|
120
|
-
"runtime.ts",
|
|
121
|
-
"constants.ts",
|
|
122
|
-
"fresh.gen.ts",
|
|
123
|
-
"manifest.gen.ts",
|
|
124
|
-
"fresh.config.ts",
|
|
125
|
-
"browserslist",
|
|
126
|
-
"bw_stats.json",
|
|
127
|
-
"islands.ts",
|
|
128
|
-
]);
|
|
129
|
-
|
|
130
|
-
/** Static files that are code/tooling, not assets — should be deleted */
|
|
131
|
-
const STATIC_DELETE = new Set([
|
|
132
|
-
"static/adminIcons.ts",
|
|
133
|
-
"static/generate-icons.ts",
|
|
134
|
-
"static/tailwind.css",
|
|
135
|
-
]);
|
|
136
89
|
|
|
137
90
|
/**
|
|
138
91
|
* Scan file content for inline npm: imports and return { name: version } pairs.
|
|
@@ -317,7 +270,7 @@ function decideAction(
|
|
|
317
270
|
};
|
|
318
271
|
}
|
|
319
272
|
|
|
320
|
-
//
|
|
273
|
+
// legacy HTMX-style dynamic-section loader → delete and flag.
|
|
321
274
|
// The file uses Deno-only APIs (`toFileUrl(Deno.cwd())`, `import.meta.resolve`)
|
|
322
275
|
// and the `useComponent(component, props)` HTMX render-and-swap pattern, none
|
|
323
276
|
// of which work on TanStack Start / Cloudflare Workers. The site author must
|
|
@@ -418,6 +371,50 @@ function scanDir(
|
|
|
418
371
|
}
|
|
419
372
|
}
|
|
420
373
|
|
|
374
|
+
export function extractGoogleFonts(
|
|
375
|
+
sourceDir: string,
|
|
376
|
+
): { preconnects: string[]; stylesheets: string[] } {
|
|
377
|
+
const preconnects: string[] = [];
|
|
378
|
+
const stylesheets: string[] = [];
|
|
379
|
+
const seen = new Set<string>();
|
|
380
|
+
|
|
381
|
+
const hrefRe = /href=["'](https:\/\/fonts\.(?:googleapis|gstatic)\.com[^"']*)["']/g;
|
|
382
|
+
|
|
383
|
+
// Read _app.tsx and _layout.tsx for <link> declarations
|
|
384
|
+
for (const candidate of ["routes/_app.tsx", "routes/_layout.tsx"]) {
|
|
385
|
+
const fp = path.join(sourceDir, candidate);
|
|
386
|
+
if (!fs.existsSync(fp)) continue;
|
|
387
|
+
const content = fs.readFileSync(fp, "utf-8");
|
|
388
|
+
for (const m of content.matchAll(hrefRe)) {
|
|
389
|
+
const href = m[1];
|
|
390
|
+
if (seen.has(href)) continue;
|
|
391
|
+
seen.add(href);
|
|
392
|
+
if (href.includes("/css")) {
|
|
393
|
+
stylesheets.push(href);
|
|
394
|
+
} else {
|
|
395
|
+
preconnects.push(href);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Also check tailwind.css for @import url(https://fonts.googleapis.com/...)
|
|
401
|
+
for (const cssCandidate of ["tailwind.css", "styles/app.css", "styles/global.css"]) {
|
|
402
|
+
const fp = path.join(sourceDir, cssCandidate);
|
|
403
|
+
if (!fs.existsSync(fp)) continue;
|
|
404
|
+
const content = fs.readFileSync(fp, "utf-8");
|
|
405
|
+
const importRe = /@import\s+url\(['"]?(https:\/\/fonts\.googleapis\.com[^'")\s]+)/g;
|
|
406
|
+
for (const m of content.matchAll(importRe)) {
|
|
407
|
+
const href = m[1];
|
|
408
|
+
if (!seen.has(href)) {
|
|
409
|
+
seen.add(href);
|
|
410
|
+
stylesheets.push(href);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return { preconnects, stylesheets };
|
|
416
|
+
}
|
|
417
|
+
|
|
421
418
|
function extractGtmId(sourceDir: string): string | null {
|
|
422
419
|
const appPath = path.join(sourceDir, "routes", "_app.tsx");
|
|
423
420
|
if (!fs.existsSync(appPath)) return null;
|
|
@@ -433,7 +430,7 @@ function extractVtexAccount(sourceDir: string): string | null {
|
|
|
433
430
|
const fp = path.join(sourceDir, candidate);
|
|
434
431
|
if (!fs.existsSync(fp)) continue;
|
|
435
432
|
const content = fs.readFileSync(fp, "utf-8");
|
|
436
|
-
// Match patterns like:
|
|
433
|
+
// Match patterns like: acme: "acmenewio"
|
|
437
434
|
const match = content.match(/vtexAn|VTEX_AN/i);
|
|
438
435
|
if (match) {
|
|
439
436
|
const dictMatch = content.match(/["'](\w+)["']\s*:\s*["'](\w+myvtex|\w+newio|\w+)["']/);
|
|
@@ -627,6 +624,20 @@ export function analyze(ctx: MigrationContext): void {
|
|
|
627
624
|
ctx.platform = extractPlatform(ctx.sourceDir);
|
|
628
625
|
ctx.vtexAccount = ctx.platform === "vtex" ? extractVtexAccount(ctx.sourceDir) : null;
|
|
629
626
|
ctx.gtmId = extractGtmId(ctx.sourceDir);
|
|
627
|
+
ctx.googleFonts = extractGoogleFonts(ctx.sourceDir);
|
|
628
|
+
// If no font URLs were extracted but the file mentions googleapis (likely a
|
|
629
|
+
// JSX expression like href={URL_CONST}), flag it for manual review.
|
|
630
|
+
if (ctx.googleFonts.preconnects.length === 0 && ctx.googleFonts.stylesheets.length === 0) {
|
|
631
|
+
const appPath = path.join(ctx.sourceDir, "routes", "_app.tsx");
|
|
632
|
+
if (fs.existsSync(appPath) && fs.readFileSync(appPath, "utf-8").includes("fonts.googleapis")) {
|
|
633
|
+
ctx.manualReviewItems.push({
|
|
634
|
+
file: "routes/_app.tsx",
|
|
635
|
+
reason:
|
|
636
|
+
"Google Fonts reference found but could not be extracted automatically (may use a JSX expression or variable). Add the font preconnect and stylesheet links manually to src/routes/__root.tsx.",
|
|
637
|
+
severity: "warning",
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
}
|
|
630
641
|
|
|
631
642
|
// Extract theme colors and font from CMS
|
|
632
643
|
const theme = extractThemeFromCms(ctx.sourceDir);
|
|
@@ -655,8 +666,16 @@ export function analyze(ctx: MigrationContext): void {
|
|
|
655
666
|
console.log(` Font: ${ctx.fontFamily}`);
|
|
656
667
|
}
|
|
657
668
|
|
|
658
|
-
// Scan all files
|
|
669
|
+
// Scan all files. The root scan skips `src/` (SKIP_DIRS) — for a MODERN
|
|
670
|
+
// layout the code lives there, so scan `src/` separately with baseDir=src so
|
|
671
|
+
// its files categorize as if they were at root (`src/sections/X` → `sections/X`
|
|
672
|
+
// → section → targetPath `src/sections/X`). The root scan still handles config
|
|
673
|
+
// + static/ at the repo root.
|
|
659
674
|
scanDir(ctx.sourceDir, ctx.sourceDir, ctx.files);
|
|
675
|
+
if (ctx.layout === "modern") {
|
|
676
|
+
const srcDir = path.join(ctx.sourceDir, "src");
|
|
677
|
+
if (fs.existsSync(srcDir)) scanDir(srcDir, srcDir, ctx.files);
|
|
678
|
+
}
|
|
660
679
|
|
|
661
680
|
// Summary
|
|
662
681
|
const byAction = { transform: 0, delete: 0, move: 0, scaffold: 0, "manual-review": 0 };
|
|
@@ -699,7 +718,7 @@ export function analyze(ctx: MigrationContext): void {
|
|
|
699
718
|
f.path === "sections/Component.ts",
|
|
700
719
|
);
|
|
701
720
|
if (componentLoaderFile || useComponentSites.length > 0) {
|
|
702
|
-
console.log("\n ⚠ HTMX dynamic-section loader detected (
|
|
721
|
+
console.log("\n ⚠ HTMX dynamic-section loader detected (legacy HTMX-style)");
|
|
703
722
|
if (componentLoaderFile) {
|
|
704
723
|
console.log(` • ${componentLoaderFile.path} (will be deleted)`);
|
|
705
724
|
}
|
|
@@ -20,7 +20,9 @@ function makeCtx(sourceDir: string, dryRun = false): MigrationContext {
|
|
|
20
20
|
importMap: {},
|
|
21
21
|
discoveredNpmDeps: {},
|
|
22
22
|
themeColors: {},
|
|
23
|
-
tailwindConfig: { colors: {}, fontFamily: {}, screens: {}, safelist: [], safelistPatterns: [], plugins: [], reviewItems: [] },
|
|
23
|
+
tailwindConfig: { colors: {}, fontFamily: {}, screens: {}, animations: {}, keyframes: {}, safelist: [], safelistPatterns: [], plugins: [], reviewItems: [] },
|
|
24
|
+
layout: "classic",
|
|
25
|
+
googleFonts: { preconnects: [], stylesheets: [] },
|
|
24
26
|
fontFamily: null,
|
|
25
27
|
files: [],
|
|
26
28
|
sectionMetas: [],
|
|
@@ -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
|
|
@@ -16,7 +16,9 @@ function makeCtx(sourceDir: string, dryRun = false): MigrationContext {
|
|
|
16
16
|
importMap: {},
|
|
17
17
|
discoveredNpmDeps: {},
|
|
18
18
|
themeColors: {},
|
|
19
|
-
tailwindConfig: { colors: {}, fontFamily: {}, screens: {}, safelist: [], safelistPatterns: [], plugins: [], reviewItems: [] },
|
|
19
|
+
tailwindConfig: { colors: {}, fontFamily: {}, screens: {}, animations: {}, keyframes: {}, safelist: [], safelistPatterns: [], plugins: [], reviewItems: [] },
|
|
20
|
+
layout: "classic",
|
|
21
|
+
googleFonts: { preconnects: [], stylesheets: [] },
|
|
20
22
|
fontFamily: null,
|
|
21
23
|
files: [],
|
|
22
24
|
sectionMetas: [],
|
|
@@ -22,6 +22,8 @@ describe("report — CSS Migration section", () => {
|
|
|
22
22
|
colors: { perola: "#F5F0E8", "brand-500": "#B10200" },
|
|
23
23
|
fontFamily: { "bebas-neue": "Bebas Neue, sans-serif" },
|
|
24
24
|
screens: {},
|
|
25
|
+
animations: {},
|
|
26
|
+
keyframes: {},
|
|
25
27
|
safelist: ["bg-red-500"],
|
|
26
28
|
safelistPatterns: [],
|
|
27
29
|
plugins: [],
|
|
@@ -199,7 +199,7 @@ export function report(ctx: MigrationContext): void {
|
|
|
199
199
|
lines.push("- [ ] `src/hooks/useCart.ts` — wire to actual server functions for your platform");
|
|
200
200
|
lines.push("- [ ] `src/worker-entry.ts` — verify CSP, proxy, and segment builder");
|
|
201
201
|
lines.push("- [ ] See the **CSS Migration** section above for DaisyUI v4→v5 / Tailwind v3→v4 findings specific to this site");
|
|
202
|
-
lines.push("- [ ] Run `npm run generate:blocks
|
|
202
|
+
lines.push("- [ ] Run `npm run generate` (unified: blocks, sections, loaders, schema) after migration");
|
|
203
203
|
lines.push("");
|
|
204
204
|
|
|
205
205
|
// Known Issues
|
|
@@ -250,9 +250,8 @@ export function report(ctx: MigrationContext): void {
|
|
|
250
250
|
lines.push("# 1. Install dependencies (bun is the canonical PM)");
|
|
251
251
|
lines.push("bun install");
|
|
252
252
|
lines.push("");
|
|
253
|
-
lines.push("# 2. Generate CMS blocks
|
|
254
|
-
lines.push("bun run generate
|
|
255
|
-
lines.push("bun run generate:schema");
|
|
253
|
+
lines.push("# 2. Generate CMS artifacts (blocks, sections, loaders, schema)");
|
|
254
|
+
lines.push("bun run generate");
|
|
256
255
|
lines.push("");
|
|
257
256
|
lines.push("# 3. Generate routes");
|
|
258
257
|
lines.push("bunx tsr generate");
|
|
@@ -1,29 +1,40 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
3
|
+
// `lib-utils` is imported lazily — see end of phase-cleanup. Eager
|
|
4
|
+
// generation of all 11 shims left every site with dead code that had
|
|
5
|
+
// to be cleaned up by hand.
|
|
6
|
+
import { extractTheme } from "./analyzers/theme-extractor";
|
|
7
|
+
import { generateAppCss } from "./templates/app-css";
|
|
8
|
+
import { generateCacheConfig } from "./templates/cache-config";
|
|
9
|
+
import { generateCiFiles } from "./templates/ci-yml";
|
|
10
|
+
import { generateCommerceInit } from "./templates/commerce-init";
|
|
11
|
+
import { generateCommerceLoaders } from "./templates/commerce-loaders";
|
|
12
|
+
import { generateSyncBlocksBotYml } from "./templates/sync-blocks-bot-yml";
|
|
13
|
+
import { generateMigrationPolicyPointerRule } from "./templates/cursor-rules";
|
|
14
|
+
import { generateHooks } from "./templates/hooks";
|
|
9
15
|
import { generateKnipConfig } from "./templates/knip-config";
|
|
16
|
+
import { generateLockfileCheckYml } from "./templates/lockfile-check-yml";
|
|
17
|
+
import { generateMainPushGuardYml } from "./templates/main-push-guard-yml";
|
|
18
|
+
import {
|
|
19
|
+
CANONICAL_BUN_VERSION,
|
|
20
|
+
CANONICAL_NODE_VERSION,
|
|
21
|
+
generatePackageJson,
|
|
22
|
+
} from "./templates/package-json";
|
|
23
|
+
import { generateParityYml } from "./templates/parity-yml";
|
|
24
|
+
import { generatePerfFiles } from "./templates/perf-yml";
|
|
25
|
+
import { generatePlaywrightFiles } from "./templates/playwright-yml";
|
|
26
|
+
import { generateReactDoctorYml } from "./templates/react-doctor-yml";
|
|
10
27
|
import { generateRoutes } from "./templates/routes";
|
|
11
|
-
import {
|
|
28
|
+
import { generateSdkFiles } from "./templates/sdk-gen";
|
|
29
|
+
import { generateSectionLoaders } from "./templates/section-loaders";
|
|
12
30
|
import { generateServerEntry } from "./templates/server-entry";
|
|
13
|
-
import {
|
|
31
|
+
import { generateSetup } from "./templates/setup";
|
|
32
|
+
import { generateTsconfig } from "./templates/tsconfig";
|
|
14
33
|
import { generateTypeFiles } from "./templates/types-gen";
|
|
15
34
|
import { generateUiComponents } from "./templates/ui-components";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import { generateSectionLoaders } from "./templates/section-loaders";
|
|
20
|
-
import { generateCacheConfig } from "./templates/cache-config";
|
|
21
|
-
import { generateSdkFiles } from "./templates/sdk-gen";
|
|
22
|
-
import { generateMigrationPolicyPointerRule } from "./templates/cursor-rules";
|
|
23
|
-
// `lib-utils` is imported lazily — see end of phase-cleanup. Eager
|
|
24
|
-
// generation of all 11 shims left every site with dead code that had
|
|
25
|
-
// to be cleaned up by hand.
|
|
26
|
-
import { extractTheme } from "./analyzers/theme-extractor";
|
|
35
|
+
import { generateViteConfig } from "./templates/vite-config";
|
|
36
|
+
import type { MigrationContext } from "./types";
|
|
37
|
+
import { log, logPhase } from "./types";
|
|
27
38
|
|
|
28
39
|
function writeFile(ctx: MigrationContext, relPath: string, content: string) {
|
|
29
40
|
const fullPath = path.join(ctx.sourceDir, relPath);
|
|
@@ -69,13 +80,21 @@ export function scaffold(ctx: MigrationContext): void {
|
|
|
69
80
|
// does NOT scaffold deploy/preview/sync-secrets workflows in the site
|
|
70
81
|
// repo; the operator wires the repo<->worker connection in the CF
|
|
71
82
|
// dashboard once after the first push.
|
|
72
|
-
writeFile(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
83
|
+
writeFile(
|
|
84
|
+
ctx,
|
|
85
|
+
".prettierrc",
|
|
86
|
+
JSON.stringify(
|
|
87
|
+
{
|
|
88
|
+
semi: true,
|
|
89
|
+
singleQuote: false,
|
|
90
|
+
trailingComma: "all" as const,
|
|
91
|
+
printWidth: 100,
|
|
92
|
+
tabWidth: 2,
|
|
93
|
+
},
|
|
94
|
+
null,
|
|
95
|
+
2,
|
|
96
|
+
) + "\n",
|
|
97
|
+
);
|
|
79
98
|
|
|
80
99
|
// PR-time lockfile guardrail. Lives in the site repo (per-site, not
|
|
81
100
|
// a centralised reusable workflow) because per D6.3 we are not
|
|
@@ -87,6 +106,40 @@ export function scaffold(ctx: MigrationContext): void {
|
|
|
87
106
|
generateLockfileCheckYml(CANONICAL_BUN_VERSION),
|
|
88
107
|
);
|
|
89
108
|
|
|
109
|
+
// Advisory per-PR performance workflow: detects which sections changed,
|
|
110
|
+
// maps them to CMS page paths, runs Lighthouse against the CF preview URL
|
|
111
|
+
// (PR vs main), and posts a comparison comment. Gate = CLS + TBT only.
|
|
112
|
+
writeMultiFile(ctx, generatePerfFiles(ctx.siteName));
|
|
113
|
+
|
|
114
|
+
// Per-PR quality pipeline (ci.yml) + its no-suppressions gate. BLOCKS on
|
|
115
|
+
// generate + build; the migration-cleanliness gates (no-suppressions,
|
|
116
|
+
// typecheck, format, knip) ship advisory so day-one CI is green. Node pinned
|
|
117
|
+
// in lockstep with the perf/playwright workflows.
|
|
118
|
+
writeMultiFile(ctx, generateCiFiles(CANONICAL_NODE_VERSION, CANONICAL_BUN_VERSION));
|
|
119
|
+
|
|
120
|
+
// Branch-protection surrogate: fails visibly if a commit reaches main
|
|
121
|
+
// without a PR (never blocks the push itself).
|
|
122
|
+
writeFile(ctx, ".github/workflows/main-push-guard.yml", generateMainPushGuardYml());
|
|
123
|
+
|
|
124
|
+
// Functional E2E harness (chromium + webkit) + self-contained config/smoke.
|
|
125
|
+
writeMultiFile(ctx, generatePlaywrightFiles(CANONICAL_BUN_VERSION));
|
|
126
|
+
|
|
127
|
+
// Advisory React lint (react-doctor): comments on PRs, never fails.
|
|
128
|
+
writeFile(ctx, ".github/workflows/react-doctor.yml", generateReactDoctorYml());
|
|
129
|
+
|
|
130
|
+
// Advisory parity validation: compares each PR preview against the original
|
|
131
|
+
// live storefront (@decocms/parity). Inert until PARITY_PROD_URL is set.
|
|
132
|
+
writeFile(ctx, ".github/workflows/parity.yml", generateParityYml(ctx.siteName));
|
|
133
|
+
|
|
134
|
+
// Daily content pull from the still-live storefront (`<origin>/.decofile` ->
|
|
135
|
+
// `.deco/blocks` -> PR). Replaces the legacy cross-repo-PAT push sync; inert
|
|
136
|
+
// until the operator sets the repo variable SYNC_BLOCKS_ORIGIN.
|
|
137
|
+
writeFile(
|
|
138
|
+
ctx,
|
|
139
|
+
".github/workflows/sync-blocks-bot.yml",
|
|
140
|
+
generateSyncBlocksBotYml(CANONICAL_BUN_VERSION),
|
|
141
|
+
);
|
|
142
|
+
|
|
90
143
|
// Server entry files (server.ts, worker-entry.ts, router.tsx, runtime.ts, context.ts)
|
|
91
144
|
writeMultiFile(ctx, generateServerEntry(ctx));
|
|
92
145
|
|
|
@@ -224,18 +277,48 @@ export { type Font as SiteThemeFont };
|
|
|
224
277
|
`;
|
|
225
278
|
}
|
|
226
279
|
|
|
227
|
-
function generateWranglerConfig(ctx: MigrationContext): string {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
280
|
+
export function generateWranglerConfig(ctx: MigrationContext): string {
|
|
281
|
+
// Emitted as real JSONC (not JSON.stringify) so the comments survive — every
|
|
282
|
+
// field below is load-bearing and the next person to edit this file needs to
|
|
283
|
+
// know why. The ids are placeholders: the control-plane owns them (it
|
|
284
|
+
// provisions the site's OWN namespaces and overrides these at build time),
|
|
285
|
+
// so a wrong id here can never reach production.
|
|
286
|
+
return `{
|
|
287
|
+
"name": "${ctx.siteName}",
|
|
288
|
+
"main": "src/worker-entry.ts",
|
|
289
|
+
"compatibility_date": "2025-05-01",
|
|
290
|
+
// no_handle_cross_request_promise_resolution: the framework's SWR/dedup
|
|
291
|
+
// caches hold in-flight promises across requests; without this flag the
|
|
292
|
+
// worker hangs.
|
|
293
|
+
"compatibility_flags": ["nodejs_compat", "no_handle_cross_request_promise_resolution"],
|
|
294
|
+
// Workers Cache — tiered cache IN FRONT of the worker. On a hit the worker
|
|
295
|
+
// never runs. Per-response cacheability is still driven by Cache-Control.
|
|
296
|
+
"cache": { "enabled": true },
|
|
297
|
+
// Surfaces the build sha as service.version on every span and log line.
|
|
298
|
+
"version_metadata": { "binding": "CF_VERSION_METADATA" },
|
|
299
|
+
// Tail worker. The ONLY channel that can see exceededMemory / exceededCpu:
|
|
300
|
+
// Cloudflare kills the isolate before any in-worker code could report them.
|
|
301
|
+
// Without this a memory incident is invisible — see docs/runbooks/.
|
|
302
|
+
"tail_consumers": [{ "service": "deco-otel-tail" }],
|
|
303
|
+
"kv_namespaces": [
|
|
304
|
+
// Fast Deploy content store. Holds decofile:<deployment-id> + the revision
|
|
305
|
+
// index, so a CMS publish goes live without a code deploy. The id is
|
|
306
|
+
// written by the control-plane at site creation and re-forced from
|
|
307
|
+
// CF_KV_NAMESPACE_ID on every build (per-site KV isolation).
|
|
308
|
+
{ "binding": "DECO_KV", "id": "" }
|
|
309
|
+
// No SITES_KV: withABTesting (server-entry) passes through when it is
|
|
310
|
+
// unbound, and nothing provisions it — an empty id would fail deploy.
|
|
311
|
+
// Add it with the shared namespace's real id when starting an A/B test.
|
|
312
|
+
],
|
|
313
|
+
"vars": {
|
|
314
|
+
"DECO_SITE_NAME": "${ctx.siteName}",
|
|
315
|
+
"DECO_ENV_NAME": "production",
|
|
316
|
+
// Fast Deploy opt-in. Requires BOTH this flag AND the DECO_KV binding
|
|
317
|
+
// above; inert if either is missing. Set to "0" to disable.
|
|
318
|
+
"DECO_FAST_DEPLOY": "1"
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
`;
|
|
239
322
|
}
|
|
240
323
|
|
|
241
324
|
function generateGitignore(): string {
|
|
@@ -340,8 +423,13 @@ export default debounce;
|
|
|
340
423
|
}
|
|
341
424
|
|
|
342
425
|
function generateSignalShim(): string {
|
|
343
|
-
return `import { useState, useRef, useEffect, useMemo, useCallback } from "react";
|
|
344
|
-
|
|
426
|
+
return `import { useState, useRef, useEffect, useMemo, useCallback, useSyncExternalStore } from "react";
|
|
427
|
+
// Import locally (so \`ReactiveSignal\` is in scope for useSignalValue below) AND
|
|
428
|
+
// re-export for consumers. A bare \`export { … } from\` re-export does NOT bind the
|
|
429
|
+
// name locally — using it as a type then fails with "Cannot find name".
|
|
430
|
+
import { signal, type ReactiveSignal } from "@decocms/blocks/sdk/signal";
|
|
431
|
+
export { signal };
|
|
432
|
+
export type { ReactiveSignal };
|
|
345
433
|
|
|
346
434
|
/** Run a function immediately. Kept for legacy module-level side effects. */
|
|
347
435
|
export function effect(fn: () => void | (() => void)): () => void {
|
|
@@ -386,6 +474,24 @@ export function useComputed<T>(compute: () => T): { readonly value: T } {
|
|
|
386
474
|
export function useSignalEffect(cb: () => void | (() => void)): void {
|
|
387
475
|
useEffect(cb);
|
|
388
476
|
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Subscribe to a ReactiveSignal and re-render when it changes.
|
|
480
|
+
* Required for signals from useUI() (displayCart, displayMenu, etc.) —
|
|
481
|
+
* reading .value directly in render is a dead read in React and freezes the UI.
|
|
482
|
+
*
|
|
483
|
+
* @example
|
|
484
|
+
* const { displayCart } = useUI();
|
|
485
|
+
* const open = useSignalValue(displayCart);
|
|
486
|
+
* return <Drawer open={open} />;
|
|
487
|
+
*/
|
|
488
|
+
export function useSignalValue<T>(sig: ReactiveSignal<T>): T {
|
|
489
|
+
return useSyncExternalStore(
|
|
490
|
+
(cb) => sig.subscribe(cb),
|
|
491
|
+
() => sig.value,
|
|
492
|
+
() => sig.value,
|
|
493
|
+
);
|
|
494
|
+
}
|
|
389
495
|
`;
|
|
390
496
|
}
|
|
391
497
|
|
|
@@ -531,10 +637,7 @@ ${entries.join("\n")}
|
|
|
531
637
|
}
|
|
532
638
|
|
|
533
639
|
function hasLocationMatcher(ctx: MigrationContext): boolean {
|
|
534
|
-
const dirs = [
|
|
535
|
-
path.join(ctx.sourceDir, "matchers"),
|
|
536
|
-
path.join(ctx.sourceDir, "src", "matchers"),
|
|
537
|
-
];
|
|
640
|
+
const dirs = [path.join(ctx.sourceDir, "matchers"), path.join(ctx.sourceDir, "src", "matchers")];
|
|
538
641
|
for (const dir of dirs) {
|
|
539
642
|
if (fs.existsSync(dir)) {
|
|
540
643
|
const files = fs.readdirSync(dir);
|
|
@@ -549,7 +652,9 @@ function hasLocationMatcher(ctx: MigrationContext): boolean {
|
|
|
549
652
|
try {
|
|
550
653
|
const content = fs.readFileSync(path.join(blocksDir, file), "utf-8");
|
|
551
654
|
if (content.includes("website/matchers/location")) return true;
|
|
552
|
-
} catch {
|
|
655
|
+
} catch {
|
|
656
|
+
/* skip */
|
|
657
|
+
}
|
|
553
658
|
}
|
|
554
659
|
}
|
|
555
660
|
return false;
|