-
Notifications
You must be signed in to change notification settings - Fork 28
Expand file tree
/
Copy pathbundle-diff.mjs
More file actions
147 lines (137 loc) · 5.51 KB
/
Copy pathbundle-diff.mjs
File metadata and controls
147 lines (137 loc) · 5.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
#!/usr/bin/env node
import { gzipSync } from "node:zlib";
import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { basename, dirname, join, resolve } from "node:path";
const root = resolve(import.meta.dirname, "..");
const distAssetsDir = join(root, "dist", "assets");
const baselinePath = join(root, "docs", "internal-review", "bundle-baseline.md");
const defaultOutputPath = resolve(root, "..", ".omx", "logs", "phase-4-bundle-diff.md");
const args = new Map();
for (let index = 2; index < process.argv.length; index += 1) {
const arg = process.argv[index];
if (!arg.startsWith("--")) continue;
const key = arg.slice(2);
const value = process.argv[index + 1]?.startsWith("--") ? "true" : process.argv[index + 1];
args.set(key, value ?? "true");
if (value !== "true") index += 1;
}
const outputPath = resolve(String(args.get("output") ?? defaultOutputPath));
const budgetGzipKb = Number(args.get("budget-gzip-kb") ?? 80);
const toleranceKb = Number(args.get("tolerance-kb") ?? 5);
const readBaseline = () => {
const text = readFileSync(baselinePath, "utf8");
const rows = new Map();
const rowPattern = /^\|\s*`([^`]+)`\s*\|\s*([\d.]+) kB\s*\|\s*([\d.]+) kB\s*\|/gm;
let match;
while ((match = rowPattern.exec(text))) {
rows.set(match[1], {
sizeKb: Number(match[2]),
gzipKb: Number(match[3]),
});
}
return rows;
};
const CSS_SUFFIX = ".css";
/*
* 入口脚本和入口样式表同名(index-<hash>.js / index-<hash>.css),推导出的 stem 会撞在
* 一起,然后 gzip 更大的那个把另一个顶掉——监控对象是脚本还是样式表,取决于当下谁更大,
* 而不是基线写了什么。自托管字体让 index.css 反超 index.js 之后这一点才暴露出来。
* 所以样式表单独挂 `<stem>.css` 这个 key,两者各自对账。
*/
const findCurrentChunks = (baselineKeys) => {
const chunks = new Map();
for (const name of readdirSync(distAssetsDir)) {
const path = join(distAssetsDir, name);
if (!statSync(path).isFile()) continue;
const isCss = name.endsWith(CSS_SUFFIX);
const comparableKeys = baselineKeys
.filter((key) => key.endsWith(CSS_SUFFIX) === isCss)
.map((key) => (isCss ? key.slice(0, -CSS_SUFFIX.length) : key));
const stemBase =
comparableKeys.find(
(key) => name === key || name.startsWith(`${key}-`) || name.startsWith(`${key}.`),
) ??
(() => {
const withoutExt = name.replace(/\.(?:[cm]?js|css|svg)$/, "");
const separatorIndex = withoutExt.lastIndexOf("-");
return separatorIndex === -1 ? withoutExt : withoutExt.slice(0, separatorIndex);
})();
const stem = isCss ? `${stemBase}${CSS_SUFFIX}` : stemBase;
const data = readFileSync(path);
const sizeKb = data.byteLength / 1024;
const gzipKb = gzipSync(data).byteLength / 1024;
const previous = chunks.get(stem);
if (!previous || gzipKb > previous.gzipKb) {
chunks.set(stem, { file: basename(path), sizeKb, gzipKb });
}
}
return chunks;
};
const formatKb = (value) => `${value.toFixed(2)} kB`;
const formatDelta = (value) => {
if (!Number.isFinite(value)) return "--";
const prefix = value > 0 ? "+" : "";
return `${prefix}${value.toFixed(2)} kB`;
};
const baseline = readBaseline();
const current = findCurrentChunks(Array.from(baseline.keys()));
const tracked = Array.from(baseline.keys()).filter((key) => current.has(key));
const missing = Array.from(baseline.keys()).filter((key) => !current.has(key));
const rows = tracked.map((key) => {
const base = baseline.get(key);
const now = current.get(key);
const deltaGzip = now.gzipKb - base.gzipKb;
// 页面 gzip 预算只约束按页加载的 chunk;入口与 vendor 不受它管,样式表同理。
const stem = key.endsWith(CSS_SUFFIX) ? key.slice(0, -CSS_SUFFIX.length) : key;
const overPageBudget =
stem !== "index" && !stem.startsWith("vendor-") && now.gzipKb > budgetGzipKb;
const overTolerance = deltaGzip > toleranceKb;
return {
key,
base,
now,
deltaGzip,
overPageBudget,
overTolerance,
};
});
const failures = rows.filter((row) => row.overPageBudget || row.overTolerance);
const generatedAt = new Date().toISOString();
const markdown = [
"# Bundle Diff Report",
"",
`Generated at: \`${generatedAt}\``,
`Baseline: \`${baselinePath}\``,
`Budget: non-vendor non-entry gzip <= \`${budgetGzipKb} kB\`; gzip delta tolerance <= \`${toleranceKb} kB\``,
"",
"| Chunk | Current | Current gzip | Baseline gzip | Delta gzip | Status |",
"| --- | ---: | ---: | ---: | ---: | --- |",
...rows.map((row) => {
const status =
row.overPageBudget || row.overTolerance
? [
row.overPageBudget ? "over page budget" : null,
row.overTolerance ? "over delta tolerance" : null,
]
.filter(Boolean)
.join("; ")
: "ok";
return `| \`${row.key}\` | ${formatKb(row.now.sizeKb)} | ${formatKb(row.now.gzipKb)} | ${formatKb(row.base.gzipKb)} | ${formatDelta(row.deltaGzip)} | ${status} |`;
}),
"",
missing.length
? `Missing tracked chunks: ${missing.map((key) => `\`${key}\``).join(", ")}`
: "Missing tracked chunks: none",
"",
failures.length
? `Result: FAIL (${failures.length} tracked chunk budget issue${failures.length === 1 ? "" : "s"})`
: "Result: PASS",
"",
].join("\n");
mkdirSync(dirname(outputPath), { recursive: true });
writeFileSync(outputPath, markdown);
console.log(`Bundle diff written to ${outputPath}`);
if (failures.length) {
console.error(markdown);
process.exit(1);
}