@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
|
@@ -12,6 +12,8 @@ describe("generateAppCss — tailwind.config.ts porting", () => {
|
|
|
12
12
|
colors: { perola: "#F5F0E8", "brand-500": "#B10200" },
|
|
13
13
|
fontFamily: { "bebas-neue": "Bebas Neue, sans-serif" },
|
|
14
14
|
screens: { "3xl": "1920px" },
|
|
15
|
+
animations: {},
|
|
16
|
+
keyframes: {},
|
|
15
17
|
safelist: ["bg-red-500", "text-brand-500"],
|
|
16
18
|
safelistPatterns: [],
|
|
17
19
|
plugins: [],
|
|
@@ -27,6 +29,28 @@ describe("generateAppCss — tailwind.config.ts porting", () => {
|
|
|
27
29
|
expect(css).toContain('@source inline("bg-red-500 text-brand-500");');
|
|
28
30
|
});
|
|
29
31
|
|
|
32
|
+
it("emits --animate-* in @theme and @keyframes blocks from tailwind.config.ts", () => {
|
|
33
|
+
const ctx = createContext("/tmp/does-not-exist");
|
|
34
|
+
ctx.tailwindConfig = {
|
|
35
|
+
colors: {},
|
|
36
|
+
fontFamily: {},
|
|
37
|
+
screens: {},
|
|
38
|
+
animations: { "fade-in": "fadeIn 0.5s ease-in" },
|
|
39
|
+
keyframes: { fadeIn: { "0%": "opacity: 0", "100%": "opacity: 1" } },
|
|
40
|
+
safelist: [],
|
|
41
|
+
safelistPatterns: [],
|
|
42
|
+
plugins: [],
|
|
43
|
+
reviewItems: [],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const css = generateAppCss(ctx);
|
|
47
|
+
|
|
48
|
+
expect(css).toContain("--animate-fade-in: fadeIn 0.5s ease-in;");
|
|
49
|
+
expect(css).toContain("@keyframes fadeIn {");
|
|
50
|
+
expect(css).toContain("0% { opacity: 0; }");
|
|
51
|
+
expect(css).toContain("100% { opacity: 1; }");
|
|
52
|
+
});
|
|
53
|
+
|
|
30
54
|
it("does not duplicate a custom color that collides with a daisyUI semantic key", () => {
|
|
31
55
|
const ctx = createContext("/tmp/does-not-exist");
|
|
32
56
|
ctx.tailwindConfig.colors = { primary: "#000000" };
|
|
@@ -240,6 +240,15 @@ ${colorLines}
|
|
|
240
240
|
}
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
const twAnimations = ctx.tailwindConfig.animations;
|
|
244
|
+
const twAnimationKeys = Object.keys(twAnimations);
|
|
245
|
+
if (twAnimationKeys.length > 0) {
|
|
246
|
+
themeBlock += `\n\n /* Custom animations ported from tailwind.config.ts */`;
|
|
247
|
+
for (const key of twAnimationKeys) {
|
|
248
|
+
themeBlock += `\n --animate-${key}: ${twAnimations[key]};`;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
243
252
|
// Gray scale compat (Tailwind v3 had gray-50..gray-950 by default) — uses
|
|
244
253
|
// the site's own tailwind.config.ts gray-* override when present, falling
|
|
245
254
|
// back to the v3 default otherwise.
|
|
@@ -254,6 +263,16 @@ ${colorLines}
|
|
|
254
263
|
themeBlock += `\n}`;
|
|
255
264
|
sections.push(themeBlock);
|
|
256
265
|
|
|
266
|
+
// Emit @keyframes for each extracted keyframe from tailwind.config.ts.
|
|
267
|
+
// TW4 has no keyframes config — they must live in CSS directly.
|
|
268
|
+
const twKeyframes = ctx.tailwindConfig.keyframes;
|
|
269
|
+
for (const [name, stops] of Object.entries(twKeyframes)) {
|
|
270
|
+
const stopLines = Object.entries(stops)
|
|
271
|
+
.map(([pct, props]) => ` ${pct} { ${props}; }`)
|
|
272
|
+
.join("\n");
|
|
273
|
+
sections.push(`@keyframes ${name} {\n${stopLines}\n}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
257
276
|
// ── DaisyUI v5 compat ─────────────────────────────────────────────
|
|
258
277
|
sections.push(`/* DaisyUI v5: flatten depth/noise to match v4 look */
|
|
259
278
|
:root {
|
|
@@ -353,6 +372,14 @@ section[data-deferred="true"] {
|
|
|
353
372
|
overflow: hidden;
|
|
354
373
|
}
|
|
355
374
|
|
|
375
|
+
/* Tailwind v3 -> v4 compat: restore the default border-color.
|
|
376
|
+
TW4 Preflight changed border-color from gray-200 (#e5e7eb) to currentColor --
|
|
377
|
+
any element with "border" but no explicit "border-{color}" now renders black.
|
|
378
|
+
--color-gray-200 is always defined in the ported @theme (gray scale compat block). */
|
|
379
|
+
*, *::before, *::after {
|
|
380
|
+
border-color: var(--color-gray-200, #e5e7eb);
|
|
381
|
+
}
|
|
382
|
+
|
|
356
383
|
/* Tailwind v3 → v4 compat: restore the default cursor:pointer on interactive
|
|
357
384
|
elements. Tailwind v4 dropped this from Preflight, leaving Fresh-era sites
|
|
358
385
|
with hundreds of buttons looking unclickable. See:
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { generateCiFiles } from "./ci-yml";
|
|
3
|
+
import { generateMainPushGuardYml } from "./main-push-guard-yml";
|
|
4
|
+
import { generateParityYml } from "./parity-yml";
|
|
5
|
+
import { generatePlaywrightFiles } from "./playwright-yml";
|
|
6
|
+
import { generateReactDoctorYml } from "./react-doctor-yml";
|
|
7
|
+
import { generateSyncBlocksBotYml } from "./sync-blocks-bot-yml";
|
|
8
|
+
|
|
9
|
+
describe("generateCiFiles", () => {
|
|
10
|
+
const files = generateCiFiles("22.15.0", "1.3.5");
|
|
11
|
+
const ci = files[".github/workflows/ci.yml"];
|
|
12
|
+
|
|
13
|
+
it("emits the workflow plus the no-suppressions gate + allowlist", () => {
|
|
14
|
+
expect(Object.keys(files).sort()).toEqual([
|
|
15
|
+
".github/workflows/ci.yml",
|
|
16
|
+
"tools/gates/no-suppressions.sh",
|
|
17
|
+
"tools/gates/suppressions-allowlist.txt",
|
|
18
|
+
]);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("pins node + bun and installs frozen", () => {
|
|
22
|
+
expect(ci).toContain("name: CI");
|
|
23
|
+
expect(ci).toContain('NODE_VERSION: "22.15.0"');
|
|
24
|
+
expect(ci).toContain('BUN_VERSION: "1.3.5"');
|
|
25
|
+
expect(ci).toContain("bun install --frozen-lockfile");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("blocks on generate + build, keeps cleanliness gates advisory", () => {
|
|
29
|
+
// generate + build have NO continue-on-error
|
|
30
|
+
expect(ci).toMatch(
|
|
31
|
+
/Generate artifacts[\s\S]*?run: bun run generate && bun run generate:routes/,
|
|
32
|
+
);
|
|
33
|
+
expect(ci).toMatch(/Build \(vite\)\n\s+run: bun run build\n/);
|
|
34
|
+
// the four cleanliness gates are advisory
|
|
35
|
+
for (const advisory of ["Typecheck", "Format check", "Knip", "no new suppression"]) {
|
|
36
|
+
const stepIdx = ci.indexOf(advisory);
|
|
37
|
+
expect(stepIdx, `${advisory} step missing`).toBeGreaterThan(-1);
|
|
38
|
+
// a continue-on-error appears within the step block
|
|
39
|
+
expect(ci.slice(stepIdx, stepIdx + 200)).toContain("continue-on-error: true");
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("strips a bun@ prefix from the version", () => {
|
|
44
|
+
const f = generateCiFiles("22.15.0", "bun@1.3.5");
|
|
45
|
+
expect(f[".github/workflows/ci.yml"]).toContain('BUN_VERSION: "1.3.5"');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("is de-projectized — no site-specific migration-debt refs", () => {
|
|
49
|
+
const blob = Object.values(files).join("\n");
|
|
50
|
+
expect(blob).not.toMatch(/sitea|siteb/i);
|
|
51
|
+
expect(blob).not.toMatch(/#7[89]|#81/); // issues #78/#79/#81
|
|
52
|
+
expect(blob).not.toMatch(/AGENTS\.md/);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("ships an empty allowlist (only comments)", () => {
|
|
56
|
+
const allow = files["tools/gates/suppressions-allowlist.txt"];
|
|
57
|
+
const entries = allow.split("\n").filter((l) => l.trim() && !l.trim().startsWith("#"));
|
|
58
|
+
expect(entries).toEqual([]);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("generateMainPushGuardYml", () => {
|
|
63
|
+
const yml = generateMainPushGuardYml();
|
|
64
|
+
it("guards main pushes via the commits/{sha}/pulls API", () => {
|
|
65
|
+
expect(yml).toContain("name: main-push-guard");
|
|
66
|
+
expect(yml).toMatch(/on:\s*\n\s*push:\s*\n\s*branches: \[main\]/);
|
|
67
|
+
expect(yml).toContain("commits/${SHA}/pulls");
|
|
68
|
+
expect(yml).toContain("exit 1");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("generatePlaywrightFiles", () => {
|
|
73
|
+
const files = generatePlaywrightFiles("1.3.5");
|
|
74
|
+
|
|
75
|
+
it("emits the workflow, a self-contained config, and a smoke spec", () => {
|
|
76
|
+
expect(Object.keys(files).sort()).toEqual([
|
|
77
|
+
".github/workflows/playwright.yml",
|
|
78
|
+
"playwright.config.ts",
|
|
79
|
+
"tests/e2e/smoke.spec.ts",
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("installs chromium + webkit and pins bun", () => {
|
|
84
|
+
const wf = files[".github/workflows/playwright.yml"];
|
|
85
|
+
expect(wf).toContain('bun-version: "1.3.5"');
|
|
86
|
+
expect(wf).toContain("playwright install --with-deps chromium webkit");
|
|
87
|
+
expect(wf).toContain("bun run test:e2e");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("config is self-contained (no support-helper imports) and targets tests/e2e", () => {
|
|
91
|
+
const cfg = files["playwright.config.ts"];
|
|
92
|
+
expect(cfg).toContain('testDir: "./tests/e2e"');
|
|
93
|
+
expect(cfg).not.toContain("./tests/support");
|
|
94
|
+
expect(cfg).toContain("webkit");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("smoke needs no app boot or network (uses setContent)", () => {
|
|
98
|
+
const spec = files["tests/e2e/smoke.spec.ts"];
|
|
99
|
+
expect(spec).toContain("page.setContent");
|
|
100
|
+
expect(spec).not.toContain("await page.goto"); // no real navigation (comment may mention page.goto)
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("generateReactDoctorYml", () => {
|
|
105
|
+
const yml = generateReactDoctorYml();
|
|
106
|
+
it("runs react-doctor advisory (no uncommented blocking)", () => {
|
|
107
|
+
expect(yml).toContain("name: React Doctor");
|
|
108
|
+
expect(yml).toContain("millionco/react-doctor@v2");
|
|
109
|
+
expect(yml).toContain("fetch-depth: 0");
|
|
110
|
+
expect(yml).not.toMatch(/^\s*blocking: error/m);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("generateParityYml", () => {
|
|
115
|
+
const yml = generateParityYml("loja-tanstack");
|
|
116
|
+
it("runs parity journey advisory, gated on PARITY_PROD_URL", () => {
|
|
117
|
+
expect(yml).toContain("name: Parity");
|
|
118
|
+
expect(yml).toContain("continue-on-error: true"); // advisory, never blocks
|
|
119
|
+
expect(yml).toContain("vars.PARITY_PROD_URL"); // prod = original storefront
|
|
120
|
+
expect(yml).toContain("bunx @decocms/parity journey");
|
|
121
|
+
expect(yml).toContain("--junit parity-results.xml");
|
|
122
|
+
expect(yml).toContain("--github");
|
|
123
|
+
expect(yml).toContain("Workers Builds: loja-tanstack"); // worker name interpolated
|
|
124
|
+
expect(yml).toContain("secrets.ANTHROPIC_API_KEY"); // optional LLM key
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("generateSyncBlocksBotYml", () => {
|
|
129
|
+
const yml = generateSyncBlocksBotYml("bun@1.3.5");
|
|
130
|
+
|
|
131
|
+
it("pulls on a daily cron, gated on SYNC_BLOCKS_ORIGIN", () => {
|
|
132
|
+
expect(yml).toContain("name: sync-blocks-bot");
|
|
133
|
+
expect(yml).toContain('BUN_VERSION: "1.3.5"'); // bun@ prefix stripped
|
|
134
|
+
expect(yml).toContain('- cron: "0 6 * * *"');
|
|
135
|
+
expect(yml).toContain("vars.SYNC_BLOCKS_ORIGIN");
|
|
136
|
+
expect(yml).toContain("scripts/sync-blocks-bot.ts");
|
|
137
|
+
expect(yml).toContain("--fail-on-plaintext-secret");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("only ever needs GITHUB_TOKEN — no cross-repo credential", () => {
|
|
141
|
+
expect(yml).toContain("secrets.GITHUB_TOKEN");
|
|
142
|
+
expect(yml).not.toMatch(/secrets\.(?!GITHUB_TOKEN)[A-Z_]+/);
|
|
143
|
+
expect(yml).toContain("permissions:");
|
|
144
|
+
expect(yml).toContain("concurrency:");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("guards the diff to .deco/blocks and builds before opening the PR", () => {
|
|
148
|
+
const guardIdx = yml.indexOf("Guard");
|
|
149
|
+
const buildIdx = yml.indexOf("Validar (generate + build)");
|
|
150
|
+
const prIdx = yml.indexOf("gh pr create");
|
|
151
|
+
expect(guardIdx).toBeGreaterThan(-1);
|
|
152
|
+
expect(guardIdx).toBeLessThan(buildIdx);
|
|
153
|
+
expect(buildIdx).toBeLessThan(prIdx);
|
|
154
|
+
expect(yml).toContain("grep -v '^\\.deco/blocks/'");
|
|
155
|
+
expect(yml).toContain("git add .deco/blocks");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("is de-projectized — no real site/customer names", () => {
|
|
159
|
+
expect(yml).not.toMatch(/sitea|acme|siteb/i);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("runs the site's own installed CLI, or a pinned one when given", () => {
|
|
163
|
+
expect(yml).toContain("bunx tsx node_modules/@decocms/blocks-cli/scripts/sync-blocks-bot.ts");
|
|
164
|
+
const pinned = generateSyncBlocksBotYml("1.3.5", "7.51.0");
|
|
165
|
+
// `npx --package=<pkg> <bin>`, never `bunx <pkg> <bin>` — the latter treats
|
|
166
|
+
// the bin name as an argument and silently runs deco-migrate instead.
|
|
167
|
+
expect(pinned).toContain("npx --yes --package=@decocms/blocks-cli@7.51.0 deco-sync-blocks-bot");
|
|
168
|
+
expect(pinned).not.toMatch(/bunx\s+(-y\s+)?@decocms\/blocks-cli/);
|
|
169
|
+
expect(pinned).not.toContain("node_modules/@decocms/blocks-cli");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("cannot pass green having run the wrong tool", () => {
|
|
173
|
+
// pipefail so `| tee` doesn't swallow the plaintext-secret exit 1, plus a
|
|
174
|
+
// grep on the report that only this script emits.
|
|
175
|
+
expect(yml).toContain("set -o pipefail");
|
|
176
|
+
expect(yml).toContain("--json --github");
|
|
177
|
+
expect(yml).toContain(`grep -q '"remoteBlocks"'`);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffolds the per-PR quality pipeline and its companion gate script:
|
|
3
|
+
* .github/workflows/ci.yml — GitHub Actions workflow
|
|
4
|
+
* tools/gates/no-suppressions.sh — bans NEW type suppressions + preact
|
|
5
|
+
* tools/gates/suppressions-allowlist.txt — grandfathered offenders (seeded empty)
|
|
6
|
+
*
|
|
7
|
+
* Design rationale:
|
|
8
|
+
* - A freshly migrated site carries inherited debt (leftover
|
|
9
|
+
* @ts-expect-error, not-yet-prettier-clean files, unused exports). So the
|
|
10
|
+
* only BLOCKING gates are the ones a site MUST pass to run at all:
|
|
11
|
+
* `generate` (CMS + route artifacts) and `build` (vite). Everything that
|
|
12
|
+
* depends on migration cleanliness — no-suppressions, typecheck,
|
|
13
|
+
* format:check, knip — ships ADVISORY (`continue-on-error: true`) so day-one
|
|
14
|
+
* CI is green. Each carries a comment on how to promote it to blocking as
|
|
15
|
+
* the debt zeroes out.
|
|
16
|
+
* - The scripts the steps call (`generate`, `generate:routes`, `typecheck`,
|
|
17
|
+
* `format:check`, `knip`) are all defined in the scaffolded package.json.
|
|
18
|
+
*
|
|
19
|
+
* @param nodeVersion Node version for setup-node (lockstep with package.json engines).
|
|
20
|
+
* @param bunVersion Bun version for setup-bun (= CANONICAL_BUN_VERSION).
|
|
21
|
+
*/
|
|
22
|
+
export function generateCiFiles(nodeVersion: string, bunVersion: string): Record<string, string> {
|
|
23
|
+
const bun = bunVersion.replace(/^bun@/, "");
|
|
24
|
+
return {
|
|
25
|
+
".github/workflows/ci.yml": generateCiYml(nodeVersion, bun),
|
|
26
|
+
"tools/gates/no-suppressions.sh": NO_SUPPRESSIONS_SH,
|
|
27
|
+
"tools/gates/suppressions-allowlist.txt": SUPPRESSIONS_ALLOWLIST,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function generateCiYml(nodeVersion: string, bunVersion: string): string {
|
|
32
|
+
return `name: CI
|
|
33
|
+
|
|
34
|
+
# Per-PR quality pipeline. No auto-merge: it only informs the human review.
|
|
35
|
+
#
|
|
36
|
+
# BLOCKS (the site must run): generate (CMS + routes), build.
|
|
37
|
+
# ADVISORY (continue-on-error — inherited migration debt varies per site):
|
|
38
|
+
# no-suppressions, typecheck, format:check, knip. Promote each to blocking by
|
|
39
|
+
# removing its \`continue-on-error\` once that debt is zero.
|
|
40
|
+
|
|
41
|
+
on:
|
|
42
|
+
pull_request:
|
|
43
|
+
push:
|
|
44
|
+
branches: [main]
|
|
45
|
+
|
|
46
|
+
permissions:
|
|
47
|
+
contents: read
|
|
48
|
+
|
|
49
|
+
concurrency:
|
|
50
|
+
group: ci-\${{ github.workflow }}-\${{ github.ref }}
|
|
51
|
+
cancel-in-progress: true
|
|
52
|
+
|
|
53
|
+
env:
|
|
54
|
+
NODE_VERSION: "${nodeVersion}"
|
|
55
|
+
BUN_VERSION: "${bunVersion}"
|
|
56
|
+
|
|
57
|
+
jobs:
|
|
58
|
+
gates:
|
|
59
|
+
name: gates (no-suppressions, generate, typecheck, format, knip, build)
|
|
60
|
+
runs-on: ubuntu-latest
|
|
61
|
+
steps:
|
|
62
|
+
- uses: actions/checkout@v4
|
|
63
|
+
|
|
64
|
+
- uses: actions/setup-node@v4
|
|
65
|
+
with:
|
|
66
|
+
node-version: \${{ env.NODE_VERSION }}
|
|
67
|
+
|
|
68
|
+
- uses: oven-sh/setup-bun@v2
|
|
69
|
+
with:
|
|
70
|
+
bun-version: \${{ env.BUN_VERSION }}
|
|
71
|
+
|
|
72
|
+
- name: bun install --frozen-lockfile
|
|
73
|
+
run: bun install --frozen-lockfile
|
|
74
|
+
|
|
75
|
+
# ADVISORY — a fresh migration ships inherited suppressions. Seed the
|
|
76
|
+
# allowlist with the current offenders, then remove \`continue-on-error\`:
|
|
77
|
+
# git grep -nE '@ts-expect-error|@ts-ignore' -- src/ tools/ \\
|
|
78
|
+
# > tools/gates/suppressions-allowlist.txt
|
|
79
|
+
- name: Gate — no new suppression + zero preact (advisory)
|
|
80
|
+
run: bash tools/gates/no-suppressions.sh
|
|
81
|
+
continue-on-error: true
|
|
82
|
+
|
|
83
|
+
# BLOCKS — CMS artifacts + route tree (the build depends on these). knip
|
|
84
|
+
# also needs routeTree.gen.ts (src/router.tsx imports it).
|
|
85
|
+
- name: Generate artifacts (CMS + routes)
|
|
86
|
+
run: bun run generate && bun run generate:routes
|
|
87
|
+
|
|
88
|
+
# ADVISORY — flip to blocking once typecheck is clean.
|
|
89
|
+
- name: Typecheck (advisory)
|
|
90
|
+
run: bun run typecheck
|
|
91
|
+
continue-on-error: true
|
|
92
|
+
|
|
93
|
+
# ADVISORY — flip to blocking after a repo-wide \`bun run format\` pass.
|
|
94
|
+
- name: Format check (advisory)
|
|
95
|
+
run: bun run format:check
|
|
96
|
+
continue-on-error: true
|
|
97
|
+
|
|
98
|
+
# ADVISORY — dead code / orphan deps.
|
|
99
|
+
- name: Knip (advisory)
|
|
100
|
+
run: bun run knip
|
|
101
|
+
continue-on-error: true
|
|
102
|
+
|
|
103
|
+
# BLOCKS — the site must compile (vite build does NOT typecheck).
|
|
104
|
+
- name: Build (vite)
|
|
105
|
+
run: bun run build
|
|
106
|
+
`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const NO_SUPPRESSIONS_SH = `#!/usr/bin/env bash
|
|
110
|
+
set -euo pipefail
|
|
111
|
+
|
|
112
|
+
# Gate: NO new type suppression + zero preact import.
|
|
113
|
+
#
|
|
114
|
+
# A migration leaves behind some @ts-expect-error/@ts-ignore. This gate keeps
|
|
115
|
+
# that debt from GROWING: existing suppressions are grandfathered in the
|
|
116
|
+
# allowlist (tools/gates/suppressions-allowlist.txt); any suppression outside it
|
|
117
|
+
# fails the PR. As the type debt zeroes out, remove entries from the allowlist —
|
|
118
|
+
# it only shrinks, never grows.
|
|
119
|
+
#
|
|
120
|
+
# Allowlist format: "path/file.ts:123" (the exact path:line from \`git grep -n\`).
|
|
121
|
+
# Blank lines and "#" are ignored. If you touched a file and an existing
|
|
122
|
+
# suppression's line number moved, update its entry — do not add a new one.
|
|
123
|
+
#
|
|
124
|
+
# Usage: bash tools/gates/no-suppressions.sh
|
|
125
|
+
|
|
126
|
+
ROOT_DIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
127
|
+
cd "\$ROOT_DIR"
|
|
128
|
+
|
|
129
|
+
ALLOWLIST="tools/gates/suppressions-allowlist.txt"
|
|
130
|
+
SELF_PATH="tools/gates/no-suppressions.sh"
|
|
131
|
+
EXIT_CODE=0
|
|
132
|
+
|
|
133
|
+
SCAN_PATHS=(-- 'src/' 'tools/' ":(exclude)\${SELF_PATH}" ":(exclude)\${ALLOWLIST}")
|
|
134
|
+
SUPPRESSION_PATTERN='@ts-expect-error|@ts-ignore'
|
|
135
|
+
|
|
136
|
+
echo "== gate: no new type suppression (\${SUPPRESSION_PATTERN}) =="
|
|
137
|
+
RAW_MATCHES="\$(git grep -nE "\${SUPPRESSION_PATTERN}" "\${SCAN_PATHS[@]}" 2>/dev/null || true)"
|
|
138
|
+
|
|
139
|
+
if [ -n "\$RAW_MATCHES" ]; then
|
|
140
|
+
VIOLATIONS=""
|
|
141
|
+
while IFS= read -r match; do
|
|
142
|
+
[ -z "\$match" ] && continue
|
|
143
|
+
path_line="\$(echo "\$match" | cut -d: -f1,2)"
|
|
144
|
+
if [ -f "\$ALLOWLIST" ] && grep -qxF "\$path_line" "\$ALLOWLIST"; then
|
|
145
|
+
continue
|
|
146
|
+
fi
|
|
147
|
+
VIOLATIONS="\${VIOLATIONS}\${match}"\$'\\n'
|
|
148
|
+
done <<<"\$RAW_MATCHES"
|
|
149
|
+
|
|
150
|
+
if [ -n "\$VIOLATIONS" ]; then
|
|
151
|
+
echo "FAILED: type suppression outside the allowlist:"
|
|
152
|
+
echo "\$VIOLATIONS"
|
|
153
|
+
echo "Fix the type at the source instead of suppressing. If an existing line"
|
|
154
|
+
echo "only changed number, update its allowlist entry (do not create a new one)."
|
|
155
|
+
EXIT_CODE=1
|
|
156
|
+
fi
|
|
157
|
+
fi
|
|
158
|
+
|
|
159
|
+
echo "== gate: zero preact import =="
|
|
160
|
+
# Matches REAL import specifiers ("preact", "preact/hooks", "@preact/signals"…),
|
|
161
|
+
# not the bare word "preact". Requires straight quotes around the specifier.
|
|
162
|
+
PREACT_IMPORT_PATTERN='["'"'"'](@preact/[a-zA-Z0-9_-]+|preact(/[a-zA-Z0-9_-]+)*)["'"'"']'
|
|
163
|
+
PREACT_MATCHES="\$(git grep -nE "\${PREACT_IMPORT_PATTERN}" "\${SCAN_PATHS[@]}" 2>/dev/null || true)"
|
|
164
|
+
if [ -n "\$PREACT_MATCHES" ]; then
|
|
165
|
+
echo "FAILED: preact import found (this stack is React, not Preact):"
|
|
166
|
+
echo "\$PREACT_MATCHES"
|
|
167
|
+
EXIT_CODE=1
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
if [ "\$EXIT_CODE" -eq 0 ]; then
|
|
171
|
+
echo "OK: no new suppression, no preact import."
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
exit "\$EXIT_CODE"
|
|
175
|
+
`;
|
|
176
|
+
|
|
177
|
+
const SUPPRESSIONS_ALLOWLIST = `# Grandfathered type suppressions (path:line from \`git grep -n\`).
|
|
178
|
+
# Seed this with the migration's leftover offenders to flip the CI
|
|
179
|
+
# no-suppressions gate from advisory to blocking:
|
|
180
|
+
# git grep -nE '@ts-expect-error|@ts-ignore' -- src/ tools/ >> tools/gates/suppressions-allowlist.txt
|
|
181
|
+
# This list only shrinks — never add a new entry for new code.
|
|
182
|
+
`;
|
|
@@ -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
|
});
|
|
@@ -12,7 +12,9 @@ function makeCtx(platform: MigrationContext["platform"]): MigrationContext {
|
|
|
12
12
|
importMap: {},
|
|
13
13
|
discoveredNpmDeps: {},
|
|
14
14
|
themeColors: {},
|
|
15
|
-
tailwindConfig: { colors: {}, fontFamily: {}, screens: {}, safelist: [], safelistPatterns: [], plugins: [], reviewItems: [] },
|
|
15
|
+
tailwindConfig: { colors: {}, fontFamily: {}, screens: {}, animations: {}, keyframes: {}, safelist: [], safelistPatterns: [], plugins: [], reviewItems: [] },
|
|
16
|
+
layout: "classic",
|
|
17
|
+
googleFonts: { preconnects: [], stylesheets: [] },
|
|
16
18
|
fontFamily: null,
|
|
17
19
|
files: [],
|
|
18
20
|
sectionMetas: [],
|
|
@@ -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.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffolds `.github/workflows/main-push-guard.yml` — a branch-protection
|
|
3
|
+
* surrogate. It does NOT block the push (real branch protection would, before
|
|
4
|
+
* the fact); it fails visibly in the Actions history when a commit reaches main
|
|
5
|
+
* without going through a PR (the "never commit straight to main" convention).
|
|
6
|
+
*
|
|
7
|
+
* Heuristic: GET /repos/{owner}/{repo}/commits/{sha}/pulls returns the PRs
|
|
8
|
+
* associated with a commit (including the merge/squash/rebase GitHub records on
|
|
9
|
+
* main when a PR merges). Empty list = the commit did not come from a PR.
|
|
10
|
+
*
|
|
11
|
+
* Fully generic — no per-site parameters.
|
|
12
|
+
*/
|
|
13
|
+
export function generateMainPushGuardYml(): string {
|
|
14
|
+
return `name: main-push-guard
|
|
15
|
+
|
|
16
|
+
# Branch-protection surrogate: detects a commit that reaches main WITHOUT a PR.
|
|
17
|
+
# Does not block the push — only fails visibly in the Actions history when the
|
|
18
|
+
# "never commit straight to main" convention is broken.
|
|
19
|
+
|
|
20
|
+
on:
|
|
21
|
+
push:
|
|
22
|
+
branches: [main]
|
|
23
|
+
|
|
24
|
+
permissions:
|
|
25
|
+
contents: read
|
|
26
|
+
pull-requests: read
|
|
27
|
+
|
|
28
|
+
jobs:
|
|
29
|
+
guard:
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
steps:
|
|
32
|
+
- name: Check that the commit on main came from a merged PR
|
|
33
|
+
env:
|
|
34
|
+
GH_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
35
|
+
REPO: \${{ github.repository }}
|
|
36
|
+
SHA: \${{ github.sha }}
|
|
37
|
+
run: |
|
|
38
|
+
count="\$(gh api "repos/\${REPO}/commits/\${SHA}/pulls" --jq 'length')"
|
|
39
|
+
if [ "\$count" -eq 0 ]; then
|
|
40
|
+
echo "::error::commit \${SHA} reached main with no associated PR (direct push). Convention: one issue = one PR, never commit straight to main."
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
echo "OK: commit \${SHA} associated with \${count} PR(s)."
|
|
44
|
+
`;
|
|
45
|
+
}
|
|
@@ -12,6 +12,14 @@ import type { MigrationContext } from "../types";
|
|
|
12
12
|
*/
|
|
13
13
|
export const CANONICAL_BUN_VERSION = "1.3.5";
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Fleet-wide canonical Node version. Used by the scaffolded CI / Playwright
|
|
17
|
+
* workflows' setup-node step (kept in lockstep with the perf workflow's pinned
|
|
18
|
+
* node-version). 22.15 is the floor for @cloudflare/vite-plugin's
|
|
19
|
+
* module.registerHooks.
|
|
20
|
+
*/
|
|
21
|
+
export const CANONICAL_NODE_VERSION = "22.15.0";
|
|
22
|
+
|
|
15
23
|
/**
|
|
16
24
|
* Get the latest published version of an npm package.
|
|
17
25
|
* Falls back to the provided default if the lookup fails.
|
|
@@ -145,6 +153,7 @@ export function generatePackageJson(ctx: MigrationContext): string {
|
|
|
145
153
|
format: 'prettier --write "src/**/*.{ts,tsx}"',
|
|
146
154
|
"format:check": 'prettier --check "src/**/*.{ts,tsx}"',
|
|
147
155
|
knip: "knip",
|
|
156
|
+
"test:e2e": "playwright test",
|
|
148
157
|
clean:
|
|
149
158
|
"rm -rf node_modules .cache dist .wrangler/state node_modules/.vite && bun install",
|
|
150
159
|
"tailwind:lint":
|
|
@@ -174,6 +183,7 @@ export function generatePackageJson(ctx: MigrationContext): string {
|
|
|
174
183
|
devDependencies: {
|
|
175
184
|
"@cloudflare/vite-plugin": "^1.27.0",
|
|
176
185
|
"@decocms/blocks-cli": `^${frameworkVersion}`,
|
|
186
|
+
"@playwright/test": "^1.62.1",
|
|
177
187
|
// CLI build of src/styles/app.css — used by the migration's own
|
|
178
188
|
// css-compile-check.ts (phase-compile) and by `tailwind:lint`, so a
|
|
179
189
|
// dropped custom theme token fails migration/CI instead of shipping
|