Skip to content

Commit e46e0e8

Browse files
committed
build(scripts): add first-screen reachability probe
1 parent 79fe940 commit e46e0e8

1 file changed

Lines changed: 86 additions & 0 deletions

File tree

‎scripts/bundle-eager-graph.mjs‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
#!/usr/bin/env node
2+
/**
3+
* @ai-context 首屏静态可达性探针(批 2 包体治理的连通性尺子)。
4+
*
5+
* Why:本仓唯一的「首屏」定义是**模块加载边界**,不是「组件看起来是不是按需渲染」。
6+
* 批 1 的「不删清单」被证伪五次,根因就是用「看起来还有人用」代替「删除后的可达性」;
7+
* 包体治理是同一类问题的镜像 —— `AiConversationDock` 看起来「按需唤起」,
8+
* 但它被 App.tsx 静态 import 且常驻挂载,于是整条 markdown+katex 栈都在首屏。
9+
* 唯一可靠的判据是:从 app/src/main.tsx 出发、只沿静态 ESM import 走。
10+
*
11+
* 口径:静态边 = `from "…"` 与裸 `import "…"`;`import(…)` 是动态边界,**不跟**。
12+
* 相对说明符按 Node 解析顺序补扩展名("", .ts, .tsx, .js, .jsx, /index.ts, /index.tsx);
13+
* 裸说明符按包名归并(`@scope/name` 取两段)。
14+
*
15+
* 副作用:只读文件系统;不写任何文件。
16+
* 边界:不做 tsconfig paths 解析、不处理 `export * as ns from`(本仓未使用)。
17+
* 用法:node scripts/bundle-eager-graph.mjs [--json] [--entry app/src/main.tsx]
18+
* 退出码:0 = 正常;1 = 入口不存在。
19+
*/
20+
import { readFileSync, existsSync, statSync } from "node:fs";
21+
import { dirname, resolve, relative, sep, join } from "node:path";
22+
import { fileURLToPath } from "node:url";
23+
24+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
25+
const argv = process.argv.slice(2);
26+
const AS_JSON = argv.includes("--json");
27+
const entryArg = argv.indexOf("--entry") >= 0 ? argv[argv.indexOf("--entry") + 1] : "app/src/main.tsx";
28+
const ENTRY = resolve(ROOT, entryArg);
29+
const EXT = ["", ".ts", ".tsx", ".js", ".jsx", "/index.ts", "/index.tsx"];
30+
const RE_FROM = /\bfrom\s*["']([^"']+)["']/g;
31+
const RE_BARE = /\bimport\s*["']([^"']+)["']/g;
32+
33+
function resolveSpec(fromFile, spec) {
34+
if (!spec.startsWith(".")) return null;
35+
const base = resolve(dirname(fromFile), spec);
36+
for (const e of EXT) {
37+
const p = base + e;
38+
if (existsSync(p) && statSync(p).isFile()) return p;
39+
}
40+
return null;
41+
}
42+
43+
export function eagerGraph(entry) {
44+
const visited = new Set();
45+
const packages = new Map();
46+
const queue = [entry];
47+
while (queue.length) {
48+
const file = queue.shift();
49+
if (visited.has(file)) continue;
50+
visited.add(file);
51+
let text;
52+
try { text = readFileSync(file, "utf8"); } catch { continue; }
53+
const specs = [];
54+
for (const re of [RE_FROM, RE_BARE]) {
55+
re.lastIndex = 0;
56+
let m;
57+
while ((m = re.exec(text)) !== null) specs.push(m[1]);
58+
}
59+
for (const spec of specs) {
60+
const resolved = resolveSpec(file, spec);
61+
if (resolved) { if (!visited.has(resolved)) queue.push(resolved); }
62+
else if (!spec.startsWith(".")) {
63+
const name = spec.startsWith("@") ? spec.split("/").slice(0, 2).join("/") : spec.split("/")[0];
64+
packages.set(name, (packages.get(name) ?? 0) + 1);
65+
}
66+
}
67+
}
68+
return { files: [...visited].sort(), packages: [...packages.keys()].sort() };
69+
}
70+
71+
if (process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url))) {
72+
if (!existsSync(ENTRY)) {
73+
console.error(`❌ 入口不存在:${relative(ROOT, ENTRY)}`);
74+
process.exit(1);
75+
}
76+
const { files, packages } = eagerGraph(ENTRY);
77+
if (AS_JSON) {
78+
console.log(JSON.stringify({ entry: relative(ROOT, ENTRY), files: files.map((f) => relative(ROOT, f)), packages }, null, 2));
79+
} else {
80+
console.log(`入口:${relative(ROOT, ENTRY)}`);
81+
console.log(`首屏静态可达应用源文件:${files.length}`);
82+
console.log(`首屏拉入的 npm 包:${packages.length}`);
83+
for (const p of packages) console.log(` - ${p}`);
84+
console.log(`逐文件清单:加 --json 输出`);
85+
}
86+
}

0 commit comments

Comments
 (0)