@decocms/blocks-cli 7.36.0 → 7.37.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/scripts/deco-migrate-cli.ts +1 -2
- package/scripts/generate-blocks.test.ts +0 -65
- package/scripts/generate-blocks.ts +1 -20
- package/scripts/generate-loaders.test.ts +0 -55
- package/scripts/generate-loaders.ts +0 -13
- package/scripts/generate-schema.test.ts +5 -35
- package/scripts/generate-schema.ts +3 -21
- package/scripts/generate-sections.test.ts +1 -40
- package/scripts/generate-sections.ts +0 -13
- package/scripts/migrate/phase-report.ts +3 -4
- package/scripts/migrate/phase-scaffold.ts +6 -0
- package/scripts/migrate/phase-verify.ts +0 -1
- package/scripts/migrate/templates/perf-yml.ts +425 -0
- package/scripts/lib/legacyArtifact.ts +0 -64
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@decocms/blocks-cli",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.37.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deco codegen (generate-blocks, generate-schema, generate-invoke) and Fresh-to-TanStack migration tooling",
|
|
6
6
|
"repository": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"lint:unused": "knip"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@decocms/blocks": "7.
|
|
34
|
+
"@decocms/blocks": "7.37.1",
|
|
35
35
|
"ts-morph": "^27.0.0",
|
|
36
36
|
"tsx": "^4.22.5"
|
|
37
37
|
},
|
|
@@ -429,8 +429,7 @@ async function main() {
|
|
|
429
429
|
console.log(`\n ${bold("Next steps:")}`);
|
|
430
430
|
console.log(` cd ${outputDir}`);
|
|
431
431
|
console.log(` npm install`);
|
|
432
|
-
console.log(` npm run generate
|
|
433
|
-
console.log(` npm run generate:schema`);
|
|
432
|
+
console.log(` npm run generate`);
|
|
434
433
|
console.log(` npx tsr generate`);
|
|
435
434
|
console.log(` npm run dev`);
|
|
436
435
|
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import * as cp from "node:child_process";
|
|
2
|
-
import * as fs from "node:fs";
|
|
3
2
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
-
import * as os from "node:os";
|
|
5
3
|
import { tmpdir } from "node:os";
|
|
6
4
|
import path from "node:path";
|
|
7
5
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
@@ -105,66 +103,3 @@ describe("readBlockDelta", () => {
|
|
|
105
103
|
expect(delta).toEqual({});
|
|
106
104
|
});
|
|
107
105
|
});
|
|
108
|
-
|
|
109
|
-
describe("generate-blocks legacy artifact sync", () => {
|
|
110
|
-
let tmpDir: string;
|
|
111
|
-
|
|
112
|
-
beforeEach(() => {
|
|
113
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "generate-blocks-legacy-"));
|
|
114
|
-
// Minimal .deco/blocks/ dir with one block so output is non-empty.
|
|
115
|
-
fs.mkdirSync(path.join(tmpDir, ".deco", "blocks"), { recursive: true });
|
|
116
|
-
fs.writeFileSync(
|
|
117
|
-
path.join(tmpDir, ".deco", "blocks", "pages-Home.json"),
|
|
118
|
-
JSON.stringify({ path: "/" }),
|
|
119
|
-
);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
afterEach(() => {
|
|
123
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("syncs .ts and .json to the old path when legacy file is detected", () => {
|
|
127
|
-
const oldDir = path.join(tmpDir, "src", "server", "cms");
|
|
128
|
-
fs.mkdirSync(oldDir, { recursive: true });
|
|
129
|
-
fs.writeFileSync(path.join(oldDir, "blocks.gen.ts"), "// stale\n");
|
|
130
|
-
|
|
131
|
-
const { code, stderr } = runGenerator([], { cwd: tmpDir });
|
|
132
|
-
expect(code).toBe(0);
|
|
133
|
-
|
|
134
|
-
expect(stderr).toContain("src/server/cms/blocks.gen.ts");
|
|
135
|
-
expect(stderr).toContain(".deco/blocks.gen.ts");
|
|
136
|
-
expect(stderr).toContain("Update importers to use the new path");
|
|
137
|
-
|
|
138
|
-
// New path must exist.
|
|
139
|
-
expect(fs.existsSync(path.join(tmpDir, ".deco", "blocks.gen.ts"))).toBe(true);
|
|
140
|
-
expect(fs.existsSync(path.join(tmpDir, ".deco", "blocks.gen.json"))).toBe(true);
|
|
141
|
-
|
|
142
|
-
// Old .ts must be synced (stub content, not the stale placeholder).
|
|
143
|
-
const oldTs = fs.readFileSync(path.join(oldDir, "blocks.gen.ts"), "utf-8");
|
|
144
|
-
expect(oldTs).not.toBe("// stale\n");
|
|
145
|
-
|
|
146
|
-
// Old .json sibling must be synced — the Vite plugin reads it by suffix.
|
|
147
|
-
const oldJson = path.join(oldDir, "blocks.gen.json");
|
|
148
|
-
expect(fs.existsSync(oldJson)).toBe(true);
|
|
149
|
-
const oldContent = JSON.parse(fs.readFileSync(oldJson, "utf-8"));
|
|
150
|
-
const newContent = JSON.parse(
|
|
151
|
-
fs.readFileSync(path.join(tmpDir, ".deco", "blocks.gen.json"), "utf-8"),
|
|
152
|
-
);
|
|
153
|
-
expect(oldContent).toEqual(newContent);
|
|
154
|
-
}, 30_000);
|
|
155
|
-
|
|
156
|
-
it("does not warn and does not sync when an explicit --out-file is passed", () => {
|
|
157
|
-
const oldDir = path.join(tmpDir, "src", "server", "cms");
|
|
158
|
-
fs.mkdirSync(oldDir, { recursive: true });
|
|
159
|
-
fs.writeFileSync(path.join(oldDir, "blocks.gen.ts"), "// stale\n");
|
|
160
|
-
|
|
161
|
-
const explicitOut = path.join(tmpDir, "custom", "blocks.gen.ts");
|
|
162
|
-
const { code, stderr } = runGenerator(["--out-file", explicitOut], { cwd: tmpDir });
|
|
163
|
-
expect(code).toBe(0);
|
|
164
|
-
|
|
165
|
-
expect(stderr).not.toContain("Generator default output moved");
|
|
166
|
-
expect(fs.existsSync(explicitOut)).toBe(true);
|
|
167
|
-
// Old file must be untouched.
|
|
168
|
-
expect(fs.readFileSync(path.join(oldDir, "blocks.gen.ts"), "utf-8")).toBe("// stale\n");
|
|
169
|
-
}, 30_000);
|
|
170
|
-
});
|
|
@@ -27,10 +27,6 @@
|
|
|
27
27
|
* --out-file by swapping its extension, so passing --out-file
|
|
28
28
|
* moves both artifacts together)
|
|
29
29
|
*
|
|
30
|
-
* If no `--out-file` is passed and the OLD default (src/server/cms/blocks.gen.ts)
|
|
31
|
-
* still exists on disk, a one-line legacy warning is printed to stderr and the
|
|
32
|
-
* NEW default is written anyway — see lib/legacyArtifact.ts.
|
|
33
|
-
*
|
|
34
30
|
* Programmatic:
|
|
35
31
|
* import { generateBlocks } from "@decocms/blocks-cli/generate-blocks";
|
|
36
32
|
* await generateBlocks({ blocksDir, outFile });
|
|
@@ -49,7 +45,6 @@ import {
|
|
|
49
45
|
singleDecodeBlockName,
|
|
50
46
|
} from "./lib/blocks-dedupe";
|
|
51
47
|
import { buildCsvRedirectBlocks } from "./lib/csv-redirects";
|
|
52
|
-
import { syncLegacyArtifact, warnLegacyArtifact } from "./lib/legacyArtifact";
|
|
53
48
|
|
|
54
49
|
const TS_STUB = [
|
|
55
50
|
"// Auto-generated — thin wrapper around blocks.gen.json.",
|
|
@@ -321,24 +316,10 @@ if (isMainModule()) {
|
|
|
321
316
|
};
|
|
322
317
|
|
|
323
318
|
const blocksDir = path.resolve(process.cwd(), arg("blocks-dir", ".deco/blocks"));
|
|
324
|
-
const OUT_FILE_EXPLICIT = args.includes("--out-file");
|
|
325
319
|
const NEW_DEFAULT_OUT_FILE = ".deco/blocks.gen.ts";
|
|
326
|
-
const OLD_DEFAULT_OUT_FILE = "src/server/cms/blocks.gen.ts";
|
|
327
320
|
const outFile = path.resolve(process.cwd(), arg("out-file", NEW_DEFAULT_OUT_FILE));
|
|
328
|
-
const oldFilePath = path.resolve(process.cwd(), OLD_DEFAULT_OUT_FILE);
|
|
329
|
-
const hasLegacy = !OUT_FILE_EXPLICIT && fs.existsSync(oldFilePath);
|
|
330
|
-
if (hasLegacy) {
|
|
331
|
-
warnLegacyArtifact(OLD_DEFAULT_OUT_FILE, NEW_DEFAULT_OUT_FILE);
|
|
332
|
-
}
|
|
333
321
|
|
|
334
|
-
generateBlocks({ blocksDir, outFile }).
|
|
335
|
-
if (hasLegacy) {
|
|
336
|
-
syncLegacyArtifact(oldFilePath, result.outFile);
|
|
337
|
-
// The .ts is a Vite stub — the plugin reads the .json sibling by suffix.
|
|
338
|
-
// Sync the JSON too so the Vite interceptor at the old path finds it.
|
|
339
|
-
syncLegacyArtifact(oldFilePath.replace(/\.ts$/, ".json"), result.jsonFile);
|
|
340
|
-
}
|
|
341
|
-
}).catch((err) => {
|
|
322
|
+
generateBlocks({ blocksDir, outFile }).catch((err) => {
|
|
342
323
|
console.error(err);
|
|
343
324
|
process.exit(1);
|
|
344
325
|
});
|
|
@@ -62,58 +62,3 @@ describe("generate-loaders — loader vs action emit", () => {
|
|
|
62
62
|
expect(out).not.toContain('createLoaderEntry("site/actions/addToCart"');
|
|
63
63
|
});
|
|
64
64
|
});
|
|
65
|
-
|
|
66
|
-
describe("generate-loaders legacy artifact sync", () => {
|
|
67
|
-
let tmpDir: string;
|
|
68
|
-
|
|
69
|
-
beforeEach(() => {
|
|
70
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "generate-loaders-legacy-"));
|
|
71
|
-
const write = (rel: string, content: string) => {
|
|
72
|
-
const abs = path.join(tmpDir, rel);
|
|
73
|
-
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
74
|
-
fs.writeFileSync(abs, content);
|
|
75
|
-
};
|
|
76
|
-
write("src/loaders/related.ts", "export default async () => [];\n");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
afterEach(() => {
|
|
80
|
-
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("syncs to the old path when legacy file is detected", () => {
|
|
84
|
-
const oldDir = path.join(tmpDir, "src", "server", "cms");
|
|
85
|
-
fs.mkdirSync(oldDir, { recursive: true });
|
|
86
|
-
fs.writeFileSync(path.join(oldDir, "loaders.gen.ts"), "// stale\n");
|
|
87
|
-
|
|
88
|
-
const { code, stderr } = run(tmpDir);
|
|
89
|
-
expect(code).toBe(0);
|
|
90
|
-
|
|
91
|
-
expect(stderr).toContain("src/server/cms/loaders.gen.ts");
|
|
92
|
-
expect(stderr).toContain(".deco/loaders.gen.ts");
|
|
93
|
-
expect(stderr).toContain("Update importers to use the new path");
|
|
94
|
-
|
|
95
|
-
// New path must exist.
|
|
96
|
-
expect(fs.existsSync(path.join(tmpDir, ".deco", "loaders.gen.ts"))).toBe(true);
|
|
97
|
-
|
|
98
|
-
// Old file must be synced as a re-export shim (not the stale placeholder,
|
|
99
|
-
// and not a verbatim copy — a byte copy would carry `.deco/`-relative
|
|
100
|
-
// import paths that break at the deeper legacy location).
|
|
101
|
-
const oldContent = fs.readFileSync(path.join(oldDir, "loaders.gen.ts"), "utf-8");
|
|
102
|
-
expect(oldContent).not.toBe("// stale\n");
|
|
103
|
-
expect(oldContent).toContain('export * from "../../../.deco/loaders.gen"');
|
|
104
|
-
}, 30_000);
|
|
105
|
-
|
|
106
|
-
it("does not warn and does not sync when an explicit --out-file is passed", () => {
|
|
107
|
-
const oldDir = path.join(tmpDir, "src", "server", "cms");
|
|
108
|
-
fs.mkdirSync(oldDir, { recursive: true });
|
|
109
|
-
fs.writeFileSync(path.join(oldDir, "loaders.gen.ts"), "// stale\n");
|
|
110
|
-
|
|
111
|
-
const explicitOut = path.join(tmpDir, "custom", "loaders.gen.ts");
|
|
112
|
-
const { code, stderr } = run(tmpDir, ["--out-file", explicitOut]);
|
|
113
|
-
expect(code).toBe(0);
|
|
114
|
-
|
|
115
|
-
expect(stderr).not.toContain("Generator default output moved");
|
|
116
|
-
expect(fs.existsSync(explicitOut)).toBe(true);
|
|
117
|
-
expect(fs.readFileSync(path.join(oldDir, "loaders.gen.ts"), "utf-8")).toBe("// stale\n");
|
|
118
|
-
}, 30_000);
|
|
119
|
-
});
|
|
@@ -43,14 +43,9 @@
|
|
|
43
43
|
* --exclude comma-separated list of loader keys to skip (they have custom wiring)
|
|
44
44
|
* --prune-by-decofile only emit entries whose key appears as `__resolveType` in any JSON
|
|
45
45
|
* --decofile-dir @deprecated alias for --prune-by-decofile
|
|
46
|
-
*
|
|
47
|
-
* If no `--out-file` is passed and the OLD default (src/server/cms/loaders.gen.ts)
|
|
48
|
-
* still exists on disk, a one-line legacy warning is printed to stderr and the
|
|
49
|
-
* NEW default is written anyway — see lib/legacyArtifact.ts.
|
|
50
46
|
*/
|
|
51
47
|
import fs from "node:fs";
|
|
52
48
|
import path from "node:path";
|
|
53
|
-
import { syncLegacyArtifact, warnLegacyArtifact } from "./lib/legacyArtifact";
|
|
54
49
|
|
|
55
50
|
const args = process.argv.slice(2);
|
|
56
51
|
function arg(name: string, fallback: string): string {
|
|
@@ -60,15 +55,8 @@ function arg(name: string, fallback: string): string {
|
|
|
60
55
|
|
|
61
56
|
const loadersDir = path.resolve(process.cwd(), arg("loaders-dir", "src/loaders"));
|
|
62
57
|
const actionsDir = path.resolve(process.cwd(), arg("actions-dir", "src/actions"));
|
|
63
|
-
const OUT_FILE_EXPLICIT = args.includes("--out-file");
|
|
64
58
|
const NEW_DEFAULT_OUT_FILE = ".deco/loaders.gen.ts";
|
|
65
|
-
const OLD_DEFAULT_OUT_FILE = "src/server/cms/loaders.gen.ts";
|
|
66
59
|
const outFile = path.resolve(process.cwd(), arg("out-file", NEW_DEFAULT_OUT_FILE));
|
|
67
|
-
const oldLoadersPath = path.resolve(process.cwd(), OLD_DEFAULT_OUT_FILE);
|
|
68
|
-
const hasLegacy = !OUT_FILE_EXPLICIT && fs.existsSync(oldLoadersPath);
|
|
69
|
-
if (hasLegacy) {
|
|
70
|
-
warnLegacyArtifact(OLD_DEFAULT_OUT_FILE, NEW_DEFAULT_OUT_FILE);
|
|
71
|
-
}
|
|
72
60
|
const excludeRaw = arg("exclude", "");
|
|
73
61
|
const excludeSet = new Set(excludeRaw.split(",").map((s) => s.trim()).filter(Boolean));
|
|
74
62
|
|
|
@@ -256,7 +244,6 @@ lines.push("");
|
|
|
256
244
|
|
|
257
245
|
fs.mkdirSync(path.dirname(outFile), { recursive: true });
|
|
258
246
|
fs.writeFileSync(outFile, lines.join("\n"));
|
|
259
|
-
if (hasLegacy) syncLegacyArtifact(oldLoadersPath, outFile);
|
|
260
247
|
|
|
261
248
|
const filterNote = cmsReferences
|
|
262
249
|
? ` (filtered against ${cmsReferences.size} CMS __resolveType references; pruned ${prunedCount} dead entries)`
|
|
@@ -252,10 +252,10 @@ describe("typeToJsonSchema loader block-ref matching", () => {
|
|
|
252
252
|
});
|
|
253
253
|
|
|
254
254
|
// ---------------------------------------------------------------------------
|
|
255
|
-
// Default output path (.deco/)
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
255
|
+
// Default output path (.deco/) — subprocess, mirrors the pattern in
|
|
256
|
+
// generate-sections.test.ts. generate-schema.ts IS guarded by isMainModule(),
|
|
257
|
+
// but it's still driven as a subprocess here so the CLI's argv-parsed OUT_REL
|
|
258
|
+
// top-level code runs against a real cwd.
|
|
259
259
|
// ---------------------------------------------------------------------------
|
|
260
260
|
|
|
261
261
|
const SCRIPT = path.resolve(__dirname, "generate-schema.ts");
|
|
@@ -313,39 +313,9 @@ describe("generate-schema default output path (.deco/)", () => {
|
|
|
313
313
|
expect(meta.manifest.blocks.sections).toHaveProperty("site/sections/Hero.tsx");
|
|
314
314
|
}, 30_000);
|
|
315
315
|
|
|
316
|
-
it("
|
|
317
|
-
const oldDefaultDir = path.join(tmpDir, "src", "server", "admin");
|
|
318
|
-
fs.mkdirSync(oldDefaultDir, { recursive: true });
|
|
319
|
-
fs.writeFileSync(path.join(oldDefaultDir, "meta.gen.json"), "{}");
|
|
320
|
-
|
|
316
|
+
it("does not warn about a legacy default path", () => {
|
|
321
317
|
const { code, stderr } = runGenerator(["--skip-apps"], { cwd: tmpDir });
|
|
322
318
|
expect(code).toBe(0);
|
|
323
|
-
|
|
324
|
-
expect(stderr).toContain("src/server/admin/meta.gen.json");
|
|
325
|
-
expect(stderr).toContain(".deco/meta.gen.json");
|
|
326
|
-
expect(stderr).toContain("Update importers to use the new path");
|
|
327
|
-
|
|
328
|
-
const newDefault = path.join(tmpDir, ".deco", "meta.gen.json");
|
|
329
|
-
expect(fs.existsSync(newDefault)).toBe(true);
|
|
330
|
-
|
|
331
|
-
// Old file must be kept in sync so mid-migration imports still work.
|
|
332
|
-
const oldDefault = path.join(tmpDir, "src", "server", "admin", "meta.gen.json");
|
|
333
|
-
expect(fs.existsSync(oldDefault)).toBe(true);
|
|
334
|
-
const oldContent = JSON.parse(fs.readFileSync(oldDefault, "utf-8"));
|
|
335
|
-
const newContent = JSON.parse(fs.readFileSync(newDefault, "utf-8"));
|
|
336
|
-
expect(oldContent).toEqual(newContent);
|
|
337
|
-
}, 30_000);
|
|
338
|
-
|
|
339
|
-
it("does not warn when an explicit --out is passed, even if the OLD default file exists", () => {
|
|
340
|
-
const oldDefaultDir = path.join(tmpDir, "src", "server", "admin");
|
|
341
|
-
fs.mkdirSync(oldDefaultDir, { recursive: true });
|
|
342
|
-
fs.writeFileSync(path.join(oldDefaultDir, "meta.gen.json"), "{}");
|
|
343
|
-
|
|
344
|
-
const explicitOut = path.join(tmpDir, "custom", "meta.gen.json");
|
|
345
|
-
const { code, stderr } = runGenerator(["--skip-apps", "--out", explicitOut], { cwd: tmpDir });
|
|
346
|
-
expect(code).toBe(0);
|
|
347
|
-
|
|
348
319
|
expect(stderr).not.toContain("Generator default output moved");
|
|
349
|
-
expect(fs.existsSync(explicitOut)).toBe(true);
|
|
350
320
|
}, 30_000);
|
|
351
321
|
});
|
|
@@ -33,10 +33,6 @@ import { fileURLToPath } from "node:url";
|
|
|
33
33
|
* from disk with no runtime (FS-based Studio / Eitri stack).
|
|
34
34
|
* --framework With --compose, the value written to the `framework` field
|
|
35
35
|
* (default: "tanstack-start").
|
|
36
|
-
*
|
|
37
|
-
* If no `--out` is passed and the OLD default (src/server/admin/meta.gen.json)
|
|
38
|
-
* still exists on disk, a one-line legacy warning is printed to stderr and the
|
|
39
|
-
* NEW default is written anyway — see lib/legacyArtifact.ts.
|
|
40
36
|
*/
|
|
41
37
|
import {
|
|
42
38
|
type Symbol as MorphSymbol,
|
|
@@ -47,7 +43,6 @@ import {
|
|
|
47
43
|
type Type,
|
|
48
44
|
} from "ts-morph";
|
|
49
45
|
import { isExcludedCodegenFile } from "./lib/codegenExclusions";
|
|
50
|
-
import { syncLegacyArtifact, warnLegacyArtifact } from "./lib/legacyArtifact";
|
|
51
46
|
|
|
52
47
|
// ---------------------------------------------------------------------------
|
|
53
48
|
// CLI arg parsing
|
|
@@ -56,12 +51,9 @@ import { syncLegacyArtifact, warnLegacyArtifact } from "./lib/legacyArtifact";
|
|
|
56
51
|
// here) so that importing this module for its pure exports
|
|
57
52
|
// (definitionIdForPath, applyWidgetFormat, typeToJsonSchema — see
|
|
58
53
|
// generate-schema.test.ts) never reads argv or touches the filesystem.
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
// the test actually wanted to exercise. generateMeta() (below) and the
|
|
63
|
-
// final write are themselves only reached inside `if (isMainModule())`, so
|
|
64
|
-
// these vars only need real values in that same case.
|
|
54
|
+
// generateMeta() (below) and the final write are themselves only reached
|
|
55
|
+
// inside `if (isMainModule())`, so these vars only need real values in that
|
|
56
|
+
// same case.
|
|
65
57
|
// ---------------------------------------------------------------------------
|
|
66
58
|
const argv = process.argv.slice(2);
|
|
67
59
|
function arg(name: string, fallback: string): string {
|
|
@@ -70,7 +62,6 @@ function arg(name: string, fallback: string): string {
|
|
|
70
62
|
}
|
|
71
63
|
|
|
72
64
|
const NEW_DEFAULT_OUT_REL = ".deco/meta.gen.json";
|
|
73
|
-
const OLD_DEFAULT_OUT_REL = "src/server/admin/meta.gen.json";
|
|
74
65
|
|
|
75
66
|
let SITE_NAMESPACE = "site";
|
|
76
67
|
let SITE_NAME = "storefront";
|
|
@@ -90,8 +81,6 @@ let COMPOSE = false;
|
|
|
90
81
|
// Value written to the composed meta's `framework` field (only used with
|
|
91
82
|
// --compose). Defaults to composeMeta's historical "tanstack-start".
|
|
92
83
|
let FRAMEWORK = "tanstack-start";
|
|
93
|
-
let _hasLegacySchema = false;
|
|
94
|
-
let _oldSchemaPath = "";
|
|
95
84
|
|
|
96
85
|
if (isMainModule()) {
|
|
97
86
|
SITE_NAMESPACE = arg("namespace", SITE_NAMESPACE);
|
|
@@ -101,16 +90,10 @@ if (isMainModule()) {
|
|
|
101
90
|
LOADERS_REL = arg("loaders", LOADERS_REL);
|
|
102
91
|
APPS_REL = arg("apps", APPS_REL);
|
|
103
92
|
SKIP_APPS = argv.includes("--skip-apps");
|
|
104
|
-
const outFileExplicit = argv.includes("--out");
|
|
105
93
|
OUT_REL = arg("out", NEW_DEFAULT_OUT_REL);
|
|
106
94
|
PLATFORM = arg("platform", PLATFORM);
|
|
107
95
|
COMPOSE = argv.includes("--compose");
|
|
108
96
|
FRAMEWORK = arg("framework", FRAMEWORK);
|
|
109
|
-
_oldSchemaPath = path.resolve(process.cwd(), OLD_DEFAULT_OUT_REL);
|
|
110
|
-
_hasLegacySchema = !outFileExplicit && fs.existsSync(_oldSchemaPath);
|
|
111
|
-
if (_hasLegacySchema) {
|
|
112
|
-
warnLegacyArtifact(OLD_DEFAULT_OUT_REL, NEW_DEFAULT_OUT_REL);
|
|
113
|
-
}
|
|
114
97
|
}
|
|
115
98
|
|
|
116
99
|
// ---------------------------------------------------------------------------
|
|
@@ -1541,7 +1524,6 @@ if (isMainModule()) {
|
|
|
1541
1524
|
const outPath = path.resolve(process.cwd(), OUT_REL);
|
|
1542
1525
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
|
1543
1526
|
fs.writeFileSync(outPath, JSON.stringify(meta, null, 2));
|
|
1544
|
-
if (_hasLegacySchema) syncLegacyArtifact(_oldSchemaPath, outPath);
|
|
1545
1527
|
|
|
1546
1528
|
const defCount = Object.keys(meta.schema.definitions).length;
|
|
1547
1529
|
const secCount = Object.keys(meta.manifest.blocks.sections || {}).length;
|
|
@@ -115,48 +115,9 @@ describe("generate-sections default output path (.deco/)", () => {
|
|
|
115
115
|
const newDefault = path.join(tmpDir, ".deco", "sections.gen.ts");
|
|
116
116
|
expect(fs.existsSync(newDefault)).toBe(true);
|
|
117
117
|
expect(fs.readFileSync(newDefault, "utf-8")).toContain("site/sections/Hero.tsx");
|
|
118
|
-
// No legacy
|
|
118
|
+
// No legacy default path is written anymore, so no warning is expected.
|
|
119
119
|
expect(stderr).not.toContain("Generator default output moved");
|
|
120
120
|
}, 30_000);
|
|
121
|
-
|
|
122
|
-
it("warns once to stderr naming both paths when the OLD default file exists and no --out-file is passed, but still writes the NEW default", () => {
|
|
123
|
-
const oldDefaultDir = path.join(tmpDir, "src", "server", "cms");
|
|
124
|
-
fs.mkdirSync(oldDefaultDir, { recursive: true });
|
|
125
|
-
fs.writeFileSync(path.join(oldDefaultDir, "sections.gen.ts"), "// stale\n");
|
|
126
|
-
|
|
127
|
-
const { code, stderr } = runGenerator([], { cwd: tmpDir });
|
|
128
|
-
expect(code).toBe(0);
|
|
129
|
-
|
|
130
|
-
expect(stderr).toContain("src/server/cms/sections.gen.ts");
|
|
131
|
-
expect(stderr).toContain(".deco/sections.gen.ts");
|
|
132
|
-
expect(stderr).toContain("Update importers to use the new path");
|
|
133
|
-
|
|
134
|
-
const newDefault = path.join(tmpDir, ".deco", "sections.gen.ts");
|
|
135
|
-
expect(fs.existsSync(newDefault)).toBe(true);
|
|
136
|
-
expect(fs.readFileSync(newDefault, "utf-8")).toContain("site/sections/Hero.tsx");
|
|
137
|
-
|
|
138
|
-
// Old file is kept in sync as a re-export shim (NOT a verbatim copy — a
|
|
139
|
-
// byte copy would carry `.deco/`-relative import paths that break at the
|
|
140
|
-
// deeper legacy location). It re-exports the new file so mid-migration
|
|
141
|
-
// imports still resolve at any depth.
|
|
142
|
-
const oldDefault = path.join(tmpDir, "src", "server", "cms", "sections.gen.ts");
|
|
143
|
-
const oldContent = fs.readFileSync(oldDefault, "utf-8");
|
|
144
|
-
expect(oldContent).toContain('export * from "../../../.deco/sections.gen"');
|
|
145
|
-
expect(oldContent).not.toContain("site/sections/Hero.tsx");
|
|
146
|
-
}, 30_000);
|
|
147
|
-
|
|
148
|
-
it("does not warn when an explicit --out-file is passed, even if the OLD default file exists", () => {
|
|
149
|
-
const oldDefaultDir = path.join(tmpDir, "src", "server", "cms");
|
|
150
|
-
fs.mkdirSync(oldDefaultDir, { recursive: true });
|
|
151
|
-
fs.writeFileSync(path.join(oldDefaultDir, "sections.gen.ts"), "// stale\n");
|
|
152
|
-
|
|
153
|
-
const explicitOut = path.join(tmpDir, "custom", "sections.gen.ts");
|
|
154
|
-
const { code, stderr } = runGenerator(["--out-file", explicitOut], { cwd: tmpDir });
|
|
155
|
-
expect(code).toBe(0);
|
|
156
|
-
|
|
157
|
-
expect(stderr).not.toContain("Generator default output moved");
|
|
158
|
-
expect(fs.existsSync(explicitOut)).toBe(true);
|
|
159
|
-
}, 30_000);
|
|
160
121
|
});
|
|
161
122
|
|
|
162
123
|
describe("generate-sections --registry", () => {
|
|
@@ -28,15 +28,10 @@
|
|
|
28
28
|
* Built from every scanned section file, not just the ones
|
|
29
29
|
* carrying convention exports. Off by default so existing
|
|
30
30
|
* Vite sites regenerating sections.gen.ts in CI see zero diff.
|
|
31
|
-
*
|
|
32
|
-
* If no `--out-file` is passed and the OLD default (src/server/cms/sections.gen.ts)
|
|
33
|
-
* still exists on disk, a one-line legacy warning is printed to stderr and the
|
|
34
|
-
* NEW default is written anyway — see lib/legacyArtifact.ts.
|
|
35
31
|
*/
|
|
36
32
|
import fs from "node:fs";
|
|
37
33
|
import path from "node:path";
|
|
38
34
|
import { isExcludedCodegenFile } from "./lib/codegenExclusions";
|
|
39
|
-
import { syncLegacyArtifact, warnLegacyArtifact } from "./lib/legacyArtifact";
|
|
40
35
|
|
|
41
36
|
const args = process.argv.slice(2);
|
|
42
37
|
function arg(name: string, fallback: string): string {
|
|
@@ -45,15 +40,8 @@ function arg(name: string, fallback: string): string {
|
|
|
45
40
|
}
|
|
46
41
|
|
|
47
42
|
const sectionsDir = path.resolve(process.cwd(), arg("sections-dir", "src/sections"));
|
|
48
|
-
const OUT_FILE_EXPLICIT = args.includes("--out-file");
|
|
49
43
|
const NEW_DEFAULT_OUT_FILE = ".deco/sections.gen.ts";
|
|
50
|
-
const OLD_DEFAULT_OUT_FILE = "src/server/cms/sections.gen.ts";
|
|
51
44
|
const outFile = path.resolve(process.cwd(), arg("out-file", NEW_DEFAULT_OUT_FILE));
|
|
52
|
-
const oldSectionsPath = path.resolve(process.cwd(), OLD_DEFAULT_OUT_FILE);
|
|
53
|
-
const hasLegacy = !OUT_FILE_EXPLICIT && fs.existsSync(oldSectionsPath);
|
|
54
|
-
if (hasLegacy) {
|
|
55
|
-
warnLegacyArtifact(OLD_DEFAULT_OUT_FILE, NEW_DEFAULT_OUT_FILE);
|
|
56
|
-
}
|
|
57
45
|
const EMIT_REGISTRY = args.includes("--registry");
|
|
58
46
|
|
|
59
47
|
interface SectionMeta {
|
|
@@ -295,7 +283,6 @@ fs.writeFileSync(
|
|
|
295
283
|
outFile,
|
|
296
284
|
lines.join("\n").replace(/\n{3,}/g, "\n\n").replace(/\n*$/, "\n"),
|
|
297
285
|
);
|
|
298
|
-
if (hasLegacy) syncLegacyArtifact(oldSectionsPath, outFile);
|
|
299
286
|
|
|
300
287
|
console.log(
|
|
301
288
|
`Generated section metadata for ${entries.length} sections → ${path.relative(process.cwd(), outFile)}`,
|
|
@@ -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");
|
|
@@ -20,6 +20,7 @@ import { generateSectionLoaders } from "./templates/section-loaders";
|
|
|
20
20
|
import { generateCacheConfig } from "./templates/cache-config";
|
|
21
21
|
import { generateSdkFiles } from "./templates/sdk-gen";
|
|
22
22
|
import { generateMigrationPolicyPointerRule } from "./templates/cursor-rules";
|
|
23
|
+
import { generatePerfFiles } from "./templates/perf-yml";
|
|
23
24
|
// `lib-utils` is imported lazily — see end of phase-cleanup. Eager
|
|
24
25
|
// generation of all 11 shims left every site with dead code that had
|
|
25
26
|
// to be cleaned up by hand.
|
|
@@ -87,6 +88,11 @@ export function scaffold(ctx: MigrationContext): void {
|
|
|
87
88
|
generateLockfileCheckYml(CANONICAL_BUN_VERSION),
|
|
88
89
|
);
|
|
89
90
|
|
|
91
|
+
// Advisory per-PR performance workflow: detects which sections changed,
|
|
92
|
+
// maps them to CMS page paths, runs Lighthouse against the CF preview URL
|
|
93
|
+
// (PR vs main), and posts a comparison comment. Gate = CLS + TBT only.
|
|
94
|
+
writeMultiFile(ctx, generatePerfFiles(ctx.siteName));
|
|
95
|
+
|
|
90
96
|
// Server entry files (server.ts, worker-entry.ts, router.tsx, runtime.ts, context.ts)
|
|
91
97
|
writeMultiFile(ctx, generateServerEntry(ctx));
|
|
92
98
|
|
|
@@ -20,7 +20,6 @@ const REQUIRED_FILES = [
|
|
|
20
20
|
// Deploy / preview / sync-secrets pipelines are owned by Cloudflare
|
|
21
21
|
// Workers Builds (D6.3) -- configured in the CF dashboard, not via
|
|
22
22
|
// GitHub workflow files in the site repo.
|
|
23
|
-
".github/workflows/regen-blocks.yml",
|
|
24
23
|
".github/workflows/lockfile-check.yml",
|
|
25
24
|
"knip.config.ts",
|
|
26
25
|
".prettierrc",
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffolds the advisory per-PR performance workflow and its companion tools:
|
|
3
|
+
* .github/workflows/perf.yml — GitHub Actions workflow
|
|
4
|
+
* tools/perf/changed-paths.sh — maps edited sections → CMS page paths
|
|
5
|
+
* tools/perf/compare.mjs — compares two LHCI result dirs, emits markdown
|
|
6
|
+
* lighthouserc.json — LHCI collect settings (3 runs, mobile UA)
|
|
7
|
+
*
|
|
8
|
+
* Design rationale:
|
|
9
|
+
* - CF Workers Builds generates a per-PR preview URL (workers.dev). Both the
|
|
10
|
+
* PR and main previews are workers.dev — no edge cache — so the delta is
|
|
11
|
+
* purely code, not cache state.
|
|
12
|
+
* - Gate signal = CLS + TBT (layout stability / JS work; cache-insensitive).
|
|
13
|
+
* LCP and score are informational only (noisy on cold SSR).
|
|
14
|
+
* - Advisory: `continue-on-error: true`. Remove it once the site stabilises.
|
|
15
|
+
* - LHCI 0.15 does not write manifest.json; compare.mjs reads lhr-*.json
|
|
16
|
+
* directly and selects the median run by performance score.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param workerName Cloudflare Worker name (= wrangler `name` field, e.g. "oficina-tanstack").
|
|
21
|
+
* Used in the CF check-run name and the main preview base URL.
|
|
22
|
+
*/
|
|
23
|
+
export function generatePerfFiles(workerName: string): Record<string, string> {
|
|
24
|
+
return {
|
|
25
|
+
".github/workflows/perf.yml": generatePerfYml(workerName),
|
|
26
|
+
"tools/perf/changed-paths.sh": CHANGED_PATHS_SH,
|
|
27
|
+
"tools/perf/compare.mjs": COMPARE_MJS,
|
|
28
|
+
"lighthouserc.json": LIGHTHOUSERC_JSON,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function generatePerfYml(workerName: string): string {
|
|
33
|
+
return `name: Perf
|
|
34
|
+
|
|
35
|
+
# Teste de performance por PR (advisory — comenta, não trava; vire gate removendo
|
|
36
|
+
# o \`continue-on-error\` do job quando estabilizar, igual react-doctor/typecheck).
|
|
37
|
+
#
|
|
38
|
+
# Trigger: pull_request (único trigger que roda do branch do PR, não do main).
|
|
39
|
+
# Aguarda o check do CF Workers Builds completar via polling, então extrai a
|
|
40
|
+
# preview URL do output — sem token CF nem build local.
|
|
41
|
+
#
|
|
42
|
+
# Baseline: URL fixa da preview do main (https://${workerName}.deco-cx.workers.dev).
|
|
43
|
+
# Gate: CLS + TBT (estrutura/JS, insensíveis a cache). LCP/score: informativos.
|
|
44
|
+
|
|
45
|
+
on:
|
|
46
|
+
pull_request:
|
|
47
|
+
|
|
48
|
+
permissions:
|
|
49
|
+
contents: read
|
|
50
|
+
pull-requests: write
|
|
51
|
+
checks: read
|
|
52
|
+
|
|
53
|
+
concurrency:
|
|
54
|
+
group: perf-\${{ github.workflow }}-\${{ github.ref }}
|
|
55
|
+
cancel-in-progress: true
|
|
56
|
+
|
|
57
|
+
env:
|
|
58
|
+
MOBILE_UA: "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36"
|
|
59
|
+
BASE_URL: "https://${workerName}.deco-cx.workers.dev"
|
|
60
|
+
PERF_FALLBACK_PATHS: |
|
|
61
|
+
/
|
|
62
|
+
|
|
63
|
+
jobs:
|
|
64
|
+
perf:
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
continue-on-error: true
|
|
67
|
+
steps:
|
|
68
|
+
# Aguarda o CF Workers Builds completar e extrai a preview URL do output.
|
|
69
|
+
# Timeout: 10 min (builds normalmente terminam em ~3 min).
|
|
70
|
+
- id: cf
|
|
71
|
+
name: Aguardar CF build e extrair preview URL
|
|
72
|
+
env:
|
|
73
|
+
GH_TOKEN: \${{ github.token }}
|
|
74
|
+
SHA: \${{ github.event.pull_request.head.sha }}
|
|
75
|
+
run: |
|
|
76
|
+
echo "Aguardando CF Workers Builds para \$SHA..."
|
|
77
|
+
for i in \$(seq 1 60); do
|
|
78
|
+
summary=\$(gh api \\
|
|
79
|
+
"repos/\$GITHUB_REPOSITORY/commits/\$SHA/check-runs" \\
|
|
80
|
+
--jq '.check_runs[] | select(.name == "Workers Builds: ${workerName}") | .output.summary' \\
|
|
81
|
+
2>/dev/null || true)
|
|
82
|
+
|
|
83
|
+
if [ -n "\$summary" ]; then
|
|
84
|
+
conclusion=\$(gh api \\
|
|
85
|
+
"repos/\$GITHUB_REPOSITORY/commits/\$SHA/check-runs" \\
|
|
86
|
+
--jq '.check_runs[] | select(.name == "Workers Builds: ${workerName}") | .conclusion' \\
|
|
87
|
+
2>/dev/null || true)
|
|
88
|
+
|
|
89
|
+
if [ "\$conclusion" = "success" ]; then
|
|
90
|
+
url=\$(printf '%s' "\$summary" | grep -oP '(?<=Preview URL: )https://\\S+' | head -1)
|
|
91
|
+
if [ -n "\$url" ]; then
|
|
92
|
+
echo "url=\$url" >> "\$GITHUB_OUTPUT"
|
|
93
|
+
echo "run=true" >> "\$GITHUB_OUTPUT"
|
|
94
|
+
echo "CF build OK — preview: \$url"
|
|
95
|
+
exit 0
|
|
96
|
+
fi
|
|
97
|
+
elif [ -n "\$conclusion" ] && [ "\$conclusion" != "null" ]; then
|
|
98
|
+
echo "CF build concluiu com: \$conclusion — pulando perf"
|
|
99
|
+
echo "run=false" >> "\$GITHUB_OUTPUT"
|
|
100
|
+
exit 0
|
|
101
|
+
fi
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
echo "Tentativa \$i/60 — aguardando 10s..."
|
|
105
|
+
sleep 10
|
|
106
|
+
done
|
|
107
|
+
|
|
108
|
+
echo "Timeout aguardando CF build — pulando perf"
|
|
109
|
+
echo "run=false" >> "\$GITHUB_OUTPUT"
|
|
110
|
+
|
|
111
|
+
- uses: actions/checkout@v4
|
|
112
|
+
if: steps.cf.outputs.run == 'true'
|
|
113
|
+
with:
|
|
114
|
+
fetch-depth: 0
|
|
115
|
+
|
|
116
|
+
- uses: actions/setup-node@v4
|
|
117
|
+
if: steps.cf.outputs.run == 'true'
|
|
118
|
+
with:
|
|
119
|
+
node-version: "22.15.0"
|
|
120
|
+
|
|
121
|
+
- id: paths
|
|
122
|
+
name: Mapear seções editadas → paths
|
|
123
|
+
if: steps.cf.outputs.run == 'true'
|
|
124
|
+
env:
|
|
125
|
+
GH_TOKEN: \${{ github.token }}
|
|
126
|
+
run: |
|
|
127
|
+
# GitHub PR Files API = source of truth (evita problemas de squash merge)
|
|
128
|
+
changed=\$(gh api \\
|
|
129
|
+
"repos/\$GITHUB_REPOSITORY/pulls/\${{ github.event.pull_request.number }}/files" \\
|
|
130
|
+
--paginate --jq '.[].filename' \\
|
|
131
|
+
| grep -E '^src/(sections|previews)/.*\\.tsx\$' || true)
|
|
132
|
+
echo "--- seções alteradas neste PR ---"
|
|
133
|
+
printf '%s\\n' "\$changed"
|
|
134
|
+
chmod +x tools/perf/changed-paths.sh
|
|
135
|
+
CHANGED_FILES="\$changed" tools/perf/changed-paths.sh > perf-paths.txt || true
|
|
136
|
+
echo "--- paths a medir ---"; cat perf-paths.txt
|
|
137
|
+
if [ -s perf-paths.txt ]; then echo "run=true" >> "\$GITHUB_OUTPUT"
|
|
138
|
+
else echo "run=false" >> "\$GITHUB_OUTPUT"; fi
|
|
139
|
+
|
|
140
|
+
- name: Warm previews
|
|
141
|
+
if: steps.cf.outputs.run == 'true' && steps.paths.outputs.run == 'true'
|
|
142
|
+
env:
|
|
143
|
+
PR_URL: \${{ steps.cf.outputs.url }}
|
|
144
|
+
run: |
|
|
145
|
+
# --max-time 30: garante que curl não trava num SSR lento
|
|
146
|
+
while IFS= read -r p; do
|
|
147
|
+
[ -n "\$p" ] || continue
|
|
148
|
+
for origin in "\$PR_URL" "\$BASE_URL"; do
|
|
149
|
+
for _ in 1 2; do
|
|
150
|
+
curl -s -o /dev/null --max-time 30 -A "\$MOBILE_UA" \\
|
|
151
|
+
-w "warm %{http_code} \${origin}\${p} ttfb=%{time_starttransfer}s\\n" \\
|
|
152
|
+
"\${origin}\${p}" || true
|
|
153
|
+
done
|
|
154
|
+
done
|
|
155
|
+
done < perf-paths.txt
|
|
156
|
+
|
|
157
|
+
- name: Lighthouse (PR + base)
|
|
158
|
+
if: steps.cf.outputs.run == 'true' && steps.paths.outputs.run == 'true'
|
|
159
|
+
env:
|
|
160
|
+
PR_URL: \${{ steps.cf.outputs.url }}
|
|
161
|
+
run: |
|
|
162
|
+
collect() {
|
|
163
|
+
local origin="\$1" out="\$2" args=()
|
|
164
|
+
while IFS= read -r p; do [ -n "\$p" ] && args+=(--url="\${origin}\${p}"); done < perf-paths.txt
|
|
165
|
+
rm -rf .lighthouseci "\$out"
|
|
166
|
+
npx @lhci/cli collect --config=lighthouserc.json "\${args[@]}"
|
|
167
|
+
mv .lighthouseci "\$out"
|
|
168
|
+
echo "--- \$out contents ---"; ls -la "\$out" || true
|
|
169
|
+
}
|
|
170
|
+
collect "\$PR_URL" .lhci-pr
|
|
171
|
+
collect "\$BASE_URL" .lhci-base
|
|
172
|
+
|
|
173
|
+
- name: Montar comentário
|
|
174
|
+
if: steps.cf.outputs.run == 'true' && steps.paths.outputs.run == 'true'
|
|
175
|
+
run: |
|
|
176
|
+
node tools/perf/compare.mjs .lhci-base .lhci-pr | tee perf-comment.md >> "\$GITHUB_STEP_SUMMARY"
|
|
177
|
+
|
|
178
|
+
- name: Comentar no PR
|
|
179
|
+
if: steps.cf.outputs.run == 'true' && steps.paths.outputs.run == 'true'
|
|
180
|
+
uses: marocchino/sticky-pull-request-comment@v2
|
|
181
|
+
with:
|
|
182
|
+
header: perf
|
|
183
|
+
path: perf-comment.md
|
|
184
|
+
`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const CHANGED_PATHS_SH = `#!/usr/bin/env bash
|
|
188
|
+
set -euo pipefail
|
|
189
|
+
|
|
190
|
+
# Map the sections changed in this PR to the CMS page path(s) that render them,
|
|
191
|
+
# so the perf workflow only measures pages the PR can actually affect.
|
|
192
|
+
#
|
|
193
|
+
# How: each edited src/sections/**/*.tsx (or its previews/ twin) becomes a
|
|
194
|
+
# resolveType ("site/sections/<subpath>"); we grep the checked-in decofile
|
|
195
|
+
# (.deco/blocks/*.json) for that string and emit the \`path\` of every PAGE block
|
|
196
|
+
# that references it. Reusable/instance blocks (path == null) and param/wildcard
|
|
197
|
+
# paths (/*, /:slug/p, /conta/*) are skipped — they can't be measured as-is.
|
|
198
|
+
#
|
|
199
|
+
# Outcomes:
|
|
200
|
+
# - No section/preview files changed → print PERF_FALLBACK_PATHS (home sanity).
|
|
201
|
+
# - Sections changed, page(s) mapped → print the mapped path(s).
|
|
202
|
+
# - Sections changed, none mapped → print PERF_FALLBACK_PATHS + log why.
|
|
203
|
+
#
|
|
204
|
+
# Output: one URL path per line on stdout. Diagnostics go to stderr.
|
|
205
|
+
#
|
|
206
|
+
# Env:
|
|
207
|
+
# CHANGED_FILES newline-separated list of changed section/preview files
|
|
208
|
+
# (from GitHub PR Files API — preferred, avoids squash-merge issues)
|
|
209
|
+
# BASE_REF git ref/sha to diff against (fallback if CHANGED_FILES unset)
|
|
210
|
+
# PERF_FALLBACK_PATHS newline-separated fallback paths (default: "/")
|
|
211
|
+
# PERF_MAX_PATHS max paths to emit (default: 3). Sections used on many
|
|
212
|
+
# pages (Header/Footer) would otherwise produce 100+ paths;
|
|
213
|
+
# any page is representative for a shared section.
|
|
214
|
+
|
|
215
|
+
ROOT_DIR="\$(cd "\$(dirname "\${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
216
|
+
cd "\$ROOT_DIR"
|
|
217
|
+
|
|
218
|
+
BASE_REF="\${BASE_REF:-origin/main}"
|
|
219
|
+
BLOCKS_DIR=".deco/blocks"
|
|
220
|
+
|
|
221
|
+
log() { printf '%s\\n' "\$*" >&2; }
|
|
222
|
+
|
|
223
|
+
emit_fallback() {
|
|
224
|
+
log "perf: sections changed but no page mapped — using PERF_FALLBACK_PATHS"
|
|
225
|
+
printf '%s\\n' "\${PERF_FALLBACK_PATHS:-/}" | sed 's/[[:space:]]*\$//' | sed '/^\$/d'
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
# Prefer CHANGED_FILES (set by the workflow from the GitHub PR Files API) to
|
|
229
|
+
# avoid squash-merge ancestry issues where git log A..B shows all historical commits.
|
|
230
|
+
# Fall back to git log only when running locally without CHANGED_FILES.
|
|
231
|
+
changed=()
|
|
232
|
+
if [ -n "\${CHANGED_FILES:-}" ]; then
|
|
233
|
+
while IFS= read -r line; do
|
|
234
|
+
[ -n "\$line" ] && changed+=("\$line")
|
|
235
|
+
done < <(printf '%s\\n' "\$CHANGED_FILES" | grep -E '\\.tsx\$' | sort -u || true)
|
|
236
|
+
else
|
|
237
|
+
while IFS= read -r line; do
|
|
238
|
+
[ -n "\$line" ] && changed+=("\$line")
|
|
239
|
+
done < <(git log --name-only --format="" "\$BASE_REF"..HEAD -- src/sections src/previews 2>/dev/null | grep -E '\\.tsx\$' | sort -u || true)
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
# No sections changed → measure home as a baseline sanity check.
|
|
243
|
+
if [ "\${#changed[@]}" -eq 0 ]; then
|
|
244
|
+
log "perf: nenhuma seção alterada — usando PERF_FALLBACK_PATHS como sanity"
|
|
245
|
+
emit_fallback
|
|
246
|
+
exit 0
|
|
247
|
+
fi
|
|
248
|
+
|
|
249
|
+
# Each changed file → the resolveType the decofile references it by.
|
|
250
|
+
resolvetypes=()
|
|
251
|
+
for f in "\${changed[@]}"; do
|
|
252
|
+
case "\$f" in
|
|
253
|
+
src/sections/*) resolvetypes+=("site/\${f#src/}") ;;
|
|
254
|
+
src/previews/*) resolvetypes+=("site/sections/\${f#src/previews/}") ;;
|
|
255
|
+
esac
|
|
256
|
+
done
|
|
257
|
+
[ "\${#resolvetypes[@]}" -eq 0 ] && exit 0
|
|
258
|
+
|
|
259
|
+
# Grep the decofile for each resolveType; collect page paths (non-null).
|
|
260
|
+
paths=()
|
|
261
|
+
for rt in "\${resolvetypes[@]}"; do
|
|
262
|
+
while IFS= read -r block; do
|
|
263
|
+
[ -n "\$block" ] || continue
|
|
264
|
+
while IFS= read -r p; do
|
|
265
|
+
[ -n "\$p" ] && paths+=("\$p")
|
|
266
|
+
done < <(jq -r 'select(.path != null and (.path | type == "string")) | .path' "\$block" 2>/dev/null || true)
|
|
267
|
+
done < <(grep -Fl "\$rt" "\$BLOCKS_DIR"/*.json 2>/dev/null || true)
|
|
268
|
+
done
|
|
269
|
+
|
|
270
|
+
# Dedupe + drop param/wildcard paths (need a concrete slug; can't measure as-is).
|
|
271
|
+
clean=()
|
|
272
|
+
if [ "\${#paths[@]}" -gt 0 ]; then
|
|
273
|
+
while IFS= read -r line; do
|
|
274
|
+
[ -n "\$line" ] && clean+=("\$line")
|
|
275
|
+
done < <(printf '%s\\n' "\${paths[@]}" | grep -vE '[:*]' | sort -u)
|
|
276
|
+
fi
|
|
277
|
+
|
|
278
|
+
if [ "\${#clean[@]}" -eq 0 ]; then
|
|
279
|
+
emit_fallback
|
|
280
|
+
exit 0
|
|
281
|
+
fi
|
|
282
|
+
|
|
283
|
+
MAX="\${PERF_MAX_PATHS:-3}"
|
|
284
|
+
total="\${#clean[@]}"
|
|
285
|
+
if [ "\$total" -gt "\$MAX" ]; then
|
|
286
|
+
log "perf: \${total} pages mapped — capped at \${MAX} (ponytail: PERF_MAX_PATHS). Dropped: \${clean[*]:\$MAX}"
|
|
287
|
+
clean=("\${clean[@]:0:\$MAX}")
|
|
288
|
+
fi
|
|
289
|
+
|
|
290
|
+
log "perf: measuring \${#clean[@]} page(s) mapped from \${#changed[@]} changed file(s)"
|
|
291
|
+
printf '%s\\n' "\${clean[@]}"
|
|
292
|
+
`;
|
|
293
|
+
|
|
294
|
+
const COMPARE_MJS = `#!/usr/bin/env node
|
|
295
|
+
// Compare two Lighthouse CI result dirs (base = main preview, pr = PR preview)
|
|
296
|
+
// and emit a markdown table. Both previews run on *.workers.dev with no edge
|
|
297
|
+
// cache, same runner/region, same mobile UA — so the delta is the PR's code
|
|
298
|
+
// effect, not cache warm/cold.
|
|
299
|
+
//
|
|
300
|
+
// Gate signal = CLS + TBT (page structure / JS work — cache-insensitive).
|
|
301
|
+
// LCP / score are shown but informative only (noisy on cold workers.dev SSR).
|
|
302
|
+
//
|
|
303
|
+
// LHCI 0.15 does not write manifest.json; we read lhr-*.json directly.
|
|
304
|
+
//
|
|
305
|
+
// Usage: node compare.mjs <baseDir> <prDir> → writes markdown to stdout
|
|
306
|
+
// node compare.mjs --selftest → runs assertions, exits 0/1
|
|
307
|
+
|
|
308
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
309
|
+
import { join } from "node:path";
|
|
310
|
+
|
|
311
|
+
const TBT_ABS = 50; // ms — deltas below this are noise
|
|
312
|
+
const TBT_REL = 0.15; // 15% relative change
|
|
313
|
+
const CLS_ABS = 0.02; // absolute CLS delta that matters
|
|
314
|
+
const CLS_FLOOR = 0.1; // only flag CLS regression when PR is above "good"
|
|
315
|
+
|
|
316
|
+
function verdict(base, pr) {
|
|
317
|
+
const tbtUp = pr.tbt - base.tbt;
|
|
318
|
+
const tbtBad = tbtUp > TBT_ABS && tbtUp > base.tbt * TBT_REL;
|
|
319
|
+
const tbtGood = -tbtUp > TBT_ABS && -tbtUp > base.tbt * TBT_REL;
|
|
320
|
+
const clsUp = pr.cls - base.cls;
|
|
321
|
+
const clsBad = clsUp > CLS_ABS && pr.cls > CLS_FLOOR;
|
|
322
|
+
const clsGood = -clsUp > CLS_ABS && base.cls > CLS_FLOOR;
|
|
323
|
+
if (tbtBad || clsBad) return { icon: "🔴", gate: true };
|
|
324
|
+
if (tbtGood || clsGood) return { icon: "🟢", gate: false };
|
|
325
|
+
return { icon: "⚪", gate: false };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function loadDir(dir) {
|
|
329
|
+
const files = readdirSync(dir).filter((f) => f.startsWith("lhr-") && f.endsWith(".json"));
|
|
330
|
+
const byUrl = {};
|
|
331
|
+
for (const f of files) {
|
|
332
|
+
const lhr = JSON.parse(readFileSync(join(dir, f), "utf8"));
|
|
333
|
+
const url = lhr.finalUrl || lhr.requestedUrl;
|
|
334
|
+
if (!byUrl[url]) byUrl[url] = [];
|
|
335
|
+
byUrl[url].push(lhr);
|
|
336
|
+
}
|
|
337
|
+
const byPath = {};
|
|
338
|
+
for (const [url, runs] of Object.entries(byUrl)) {
|
|
339
|
+
runs.sort((a, b) => a.categories.performance.score - b.categories.performance.score);
|
|
340
|
+
const lhr = runs[Math.floor(runs.length / 2)];
|
|
341
|
+
const a = lhr.audits;
|
|
342
|
+
byPath[new URL(url).pathname] = {
|
|
343
|
+
score: Math.round((lhr.categories.performance.score ?? 0) * 100),
|
|
344
|
+
lcp: a["largest-contentful-paint"].numericValue,
|
|
345
|
+
cls: a["cumulative-layout-shift"].numericValue,
|
|
346
|
+
tbt: a["total-blocking-time"].numericValue,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
return byPath;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const ms = (v) => \`\${Math.round(v)}ms\`;
|
|
353
|
+
const cls = (v) => v.toFixed(3);
|
|
354
|
+
function delta(base, pr, fmt) {
|
|
355
|
+
const d = pr - base;
|
|
356
|
+
const arrow = d < 0 ? "↓" : d > 0 ? "↑" : "";
|
|
357
|
+
return \`\${d > 0 ? "+" : ""}\${fmt(d)} \${arrow}\`.trim();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function render(base, pr) {
|
|
361
|
+
const paths = [...new Set([...Object.keys(base), ...Object.keys(pr)])].sort();
|
|
362
|
+
let anyGate = false;
|
|
363
|
+
const lines = [
|
|
364
|
+
"### ⚡ Perf — preview do PR × preview do main",
|
|
365
|
+
"",
|
|
366
|
+
"Ambas as previews rodam em \`*.workers.dev\` (sem edge cache), mesmo runner e UA mobile — o delta é efeito do código, não de cache quente/frio.",
|
|
367
|
+
"",
|
|
368
|
+
"**Gate:** CLS + TBT (estrutura / JS). LCP e score são informativos (ruído esperado em SSR frio).",
|
|
369
|
+
"",
|
|
370
|
+
"| Página | | CLS (base→PR) | TBT (base→PR) | LCP | Score |",
|
|
371
|
+
"|---|:--:|---|---|---|---|",
|
|
372
|
+
];
|
|
373
|
+
for (const p of paths) {
|
|
374
|
+
const b = base[p];
|
|
375
|
+
const r = pr[p];
|
|
376
|
+
if (!b || !r) {
|
|
377
|
+
lines.push(\`| \\\`\${p}\\\` | ⚠️ | \${b ? "faltou PR" : "faltou base"} | | | |\`);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
const v = verdict(b, r);
|
|
381
|
+
anyGate = anyGate || v.gate;
|
|
382
|
+
lines.push(
|
|
383
|
+
\`| \\\`\${p}\\\` | \${v.icon} | \${cls(b.cls)}→\${cls(r.cls)} (\${delta(b.cls, r.cls, cls)}) | \${ms(b.tbt)}→\${ms(r.tbt)} (\${delta(b.tbt, r.tbt, ms)}) | \${ms(b.lcp)}→\${ms(r.lcp)} | \${b.score}→\${r.score} |\`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
lines.push("");
|
|
387
|
+
lines.push(
|
|
388
|
+
anyGate
|
|
389
|
+
? "🔴 **Regressão de CLS/TBT detectada** (advisory — não trava o merge)."
|
|
390
|
+
: "🟢 Sem regressão de CLS/TBT.",
|
|
391
|
+
);
|
|
392
|
+
return { md: lines.join("\\n"), anyGate };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function selftest() {
|
|
396
|
+
const ok = (c, m) => { if (!c) throw new Error("FAIL: " + m); };
|
|
397
|
+
ok(verdict({ tbt: 100, cls: 0.01 }, { tbt: 200, cls: 0.01 }).gate, "tbt +100ms should gate");
|
|
398
|
+
ok(!verdict({ tbt: 100, cls: 0.01 }, { tbt: 130, cls: 0.01 }).gate, "tbt +30ms is noise");
|
|
399
|
+
ok(verdict({ tbt: 100, cls: 0.11 }, { tbt: 100, cls: 0.2 }).gate, "cls 0.11→0.20 should gate");
|
|
400
|
+
ok(!verdict({ tbt: 100, cls: 0.02 }, { tbt: 100, cls: 0.08 }).gate, "cls under 0.1 floor no gate");
|
|
401
|
+
ok(verdict({ tbt: 300, cls: 0.01 }, { tbt: 100, cls: 0.01 }).icon === "🟢", "tbt drop is green");
|
|
402
|
+
ok(verdict({ tbt: 100, cls: 0.01 }, { tbt: 105, cls: 0.01 }).icon === "⚪", "tiny change is neutral");
|
|
403
|
+
console.log("compare.mjs selftest: OK");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (process.argv[2] === "--selftest") { selftest(); process.exit(0); }
|
|
407
|
+
|
|
408
|
+
const [baseDir, prDir] = process.argv.slice(2);
|
|
409
|
+
if (!baseDir || !prDir) { console.error("usage: compare.mjs <baseDir> <prDir> | --selftest"); process.exit(2); }
|
|
410
|
+
process.stdout.write(render(loadDir(baseDir), loadDir(prDir)).md + "\\n");
|
|
411
|
+
`;
|
|
412
|
+
|
|
413
|
+
const LIGHTHOUSERC_JSON = `{
|
|
414
|
+
"//": "Lighthouse CI config for the per-PR perf workflow (.github/workflows/perf.yml). Mobile UA matches worker-entry.ts buildSegment (MOBILE_RE) so warmup and measurement hit the same device cache key.",
|
|
415
|
+
"ci": {
|
|
416
|
+
"collect": {
|
|
417
|
+
"numberOfRuns": 3,
|
|
418
|
+
"settings": {
|
|
419
|
+
"onlyCategories": ["performance"],
|
|
420
|
+
"emulatedUserAgent": "Mozilla/5.0 (Linux; Android 13; Pixel 7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36"
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
`;
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Generator default output paths flipped from `src/server/{cms,admin}/` to
|
|
6
|
-
* `.deco/` (framework artifacts live in the framework's folder, not mixed
|
|
7
|
-
* into app source). Sites that never pass an explicit `--out`/`--out-file`
|
|
8
|
-
* flag pick up the new default silently — except when the OLD default file
|
|
9
|
-
* is still sitting on disk, which almost always means something (an
|
|
10
|
-
* importer, a `.gitignore` entry, a stale CI cache check) still points at
|
|
11
|
-
* it. In that case we warn once, to stderr, and then write to the NEW
|
|
12
|
-
* default anyway: the artifact is regenerated code, so there's no reason to
|
|
13
|
-
* block the run — the warning is just a nudge to go clean up the stale file
|
|
14
|
-
* and its importers.
|
|
15
|
-
*
|
|
16
|
-
* An explicit flag means the caller made a deliberate choice about where
|
|
17
|
-
* output goes; it gets no warning and no guard.
|
|
18
|
-
*/
|
|
19
|
-
export function warnLegacyArtifact(oldPath: string, newPath: string): void {
|
|
20
|
-
console.warn(
|
|
21
|
-
`[deco] Generator default output moved: ${oldPath} -> ${newPath}. Update importers to use the new path, then delete the old file.`,
|
|
22
|
-
);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Keeps the legacy `oldPath` in sync with the freshly-generated `newPath` so
|
|
27
|
-
* sites mid-migration whose imports still point at the old location keep
|
|
28
|
-
* working without a full migration first. Call this after generation
|
|
29
|
-
* completes, only when the old file was detected.
|
|
30
|
-
*
|
|
31
|
-
* For `.ts`/`.tsx` artifacts a verbatim copy would be broken: the generated
|
|
32
|
-
* file's import specifiers are relative to `newPath`'s directory (`.deco/`,
|
|
33
|
-
* one level deep), but `oldPath` lives deeper (`src/server/cms/`), so every
|
|
34
|
-
* `../src/...` specifier would resolve to a nonexistent path and crash the
|
|
35
|
-
* dev server on boot. Instead we write a re-export shim pointing at `newPath`
|
|
36
|
-
* — one specifier, correct at any depth, and self-documenting the deprecation.
|
|
37
|
-
* The generated `.gen.ts` files only have named exports, so `export *` forwards
|
|
38
|
-
* everything (including type exports). Non-TS artifacts (e.g. `.json` schema)
|
|
39
|
-
* have no import paths, so a byte copy is still correct.
|
|
40
|
-
*/
|
|
41
|
-
export function syncLegacyArtifact(oldPath: string, newPath: string): void {
|
|
42
|
-
fs.mkdirSync(path.dirname(oldPath), { recursive: true });
|
|
43
|
-
|
|
44
|
-
if (!/\.tsx?$/.test(oldPath)) {
|
|
45
|
-
fs.copyFileSync(newPath, oldPath);
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const newRel = path.relative(process.cwd(), newPath).replace(/\\/g, "/");
|
|
50
|
-
let specifier = path
|
|
51
|
-
.relative(path.dirname(oldPath), newPath)
|
|
52
|
-
.replace(/\\/g, "/")
|
|
53
|
-
.replace(/\.tsx?$/, "");
|
|
54
|
-
if (!specifier.startsWith(".")) specifier = `./${specifier}`;
|
|
55
|
-
|
|
56
|
-
fs.writeFileSync(
|
|
57
|
-
oldPath,
|
|
58
|
-
`// AUTO-GENERATED — DO NOT EDIT.\n` +
|
|
59
|
-
`// Deprecated path: the generator now writes to "${newRel}".\n` +
|
|
60
|
-
`// This file only re-exports it so mid-migration imports keep working.\n` +
|
|
61
|
-
`// Update your imports to the new path, then delete this file.\n` +
|
|
62
|
-
`export * from "${specifier}";\n`,
|
|
63
|
-
);
|
|
64
|
-
}
|