返回 DeepSeek-Reasonix
check-bundle-budget.mjs
根目录 / desktop / frontend / scripts / check-bundle-budget.mjs
1 import { readFileSync, readdirSync, statSync } from "node:fs";
2 import { basename, resolve } from "node:path";
3 import { gzipSync } from "node:zlib";
4
5 const distDir = resolve("dist");
6 const indexPath = resolve(distDir, "index.html");
7 const html = readFileSync(indexPath, "utf8");
8
9 function gzipBytes(path) {
10 return gzipSync(readFileSync(path), { level: 9 }).byteLength;
11 }
12
13 function initialAssetPaths(extension) {
14 const pattern = extension === ".js"
15 ? /<(?:script|link)\b[^>]+(?:src|href)=["']([^"']+\.js)["'][^>]*>/g
16 : /<link\b[^>]+href=["']([^"']+\.css)["'][^>]*>/g;
17 return [...new Set([...html.matchAll(pattern)].map((match) => resolve(distDir, match[1])))];
18 }
19
20 function formatKiB(bytes) {
21 return `${(bytes / 1024).toFixed(1)} KiB`;
22 }
23
24 function assertBudget(label, actual, budget) {
25 if (actual > budget) {
26 throw new Error(`${label} is ${formatKiB(actual)}; budget is ${formatKiB(budget)}`);
27 }
28 process.stdout.write(` PASS ${label}: ${formatKiB(actual)} / ${formatKiB(budget)}\n`);
29 }
30
31 const initialJS = initialAssetPaths(".js");
32 const initialCSS = initialAssetPaths(".css");
33 if (!initialJS.length) throw new Error("no initial JavaScript assets found in dist/index.html");
34 if (!initialCSS.length) throw new Error("no initial CSS assets found in dist/index.html");
35
36 const initialJSGzip = initialJS.reduce((total, path) => total + gzipBytes(path), 0);
37 const initialCSSGzip = initialCSS.reduce((total, path) => total + gzipBytes(path), 0);
38 const largestInitialJS = Math.max(...initialJS.map(gzipBytes));
39 const largestInitialJSRaw = Math.max(...initialJS.map((path) => statSync(path).size));
40 const localeChunks = readdirSync(resolve(distDir, "assets"))
41 .filter((name) => /^(?:zh|zh-TW)-.+\.js$/.test(name))
42 .map((name) => resolve(distDir, "assets", name));
43
44 console.log("\nbundle budgets");
45 assertBudget("initial JavaScript gzip", initialJSGzip, 430 * 1024);
46 assertBudget("largest initial JavaScript chunk gzip", largestInitialJS, 295 * 1024);
47 // Extension surfaces, Task Monitor, and compact decision receipts share the
48 // always-loaded shell. Keep their combined allowance bounded to 113 KiB gzip.
49 assertBudget("initial CSS gzip", initialCSSGzip, 113 * 1024);
50 if (localeChunks.length !== 2) {
51 throw new Error(`expected 2 on-demand Chinese locale chunks, found ${localeChunks.length}`);
52 }
53 for (const path of localeChunks) {
54 const name = basename(path);
55 // Task Monitor adds 37 user-facing labels to each on-demand locale, while
56 // Extension UI adds its own status and action copy. Shell execution contract
57 // cards add a small set of verification/risk strings. Keep both dictionaries
58 // within narrowly measured, explicit allowances.
59 const budget = name.startsWith("zh-TW-") ? 53.5 * 1024 : 52.75 * 1024;
60 assertBudget(`${name} gzip`, gzipBytes(path), budget);
61 }
62
63 const rawInitialBytes = [...initialJS, ...initialCSS].reduce((total, path) => total + statSync(path).size, 0);
64 // Extension UI adds always-loaded cards, status chips, and palette styles;
65 // Task Monitor adds a small always-loaded style surface while its panel code
66 // remains lazy-loaded. The gzip budgets above remain the user-visible transfer
67 // constraint, with a narrowly measured raw allowance for both surfaces.
68 assertBudget("initial raw JavaScript and CSS", rawInitialBytes, 2_270 * 1024);
69 assertBudget("largest initial JavaScript chunk raw", largestInitialJSRaw, 1_100 * 1024);
70
70 lines Plain Text