#!/usr/bin/env node /** * 校验一份插件快照能否独立启动 —— 逐个解析入口的静态 import。 * * # 为什么不能只看 package.json 的 dependencies * * 实测:`plugins/pi-mail-bridge/package.json` 的 `dependencies` 是 `{}`, * 而它的入口 `import` 了 `@earendil-works/pi-coding-agent`。也就是说**声明是假的** * (依赖实际靠 node_modules 里的存在,而不是靠声明)。只比对声明项,这个检查 * 就是空跑。 * * 漏掉的代价发生在最糟的时刻:拷贝完成、`current` 已切、服务重启, * 然后插件起不来 —— 而那时旧版本已经被换掉了。 * * # 做法 * * 从入口出发按相对路径递归,收集所有静态 `import` / `export ... from` 的 * 裸说明符(bare specifier),再用 Node 的解析器逐个解析。解析失败即为缺失。 * * 只做**解析**不做执行:这个脚本不会启动插件(启动会连 Gateway、起 SSE 循环, * 那不是一次门禁该干的事)。 * * 用法: node check-plugin-snapshot.mjs <快照目录> <入口相对路径> * 退出码: 0=全部可解析 1=有缺失 2=用法错误 */ import { readFileSync, existsSync, statSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; const [snapshot, entry] = process.argv.slice(2); if (!snapshot || !entry) { console.error('用法: node check-plugin-snapshot.mjs <快照目录> <入口相对路径>'); process.exit(2); } if (!existsSync(snapshot) || !statSync(snapshot).isDirectory()) { console.error(`快照目录不存在: ${snapshot}`); process.exit(2); } const entryPath = join(snapshot, entry); if (!existsSync(entryPath)) { console.error(`入口不存在: ${entryPath}`); process.exit(2); } // 静态 import 的三种写法。动态 import() 与 require() 也一并认, // 它们同样会在启动时炸;宁可多查一个说明符,也不要漏掉一个。 const SPECIFIER_PATTERNS = [ /\bimport\s+[^'"()]*from\s*['"]([^'"]+)['"]/g, /\bimport\s*['"]([^'"]+)['"]/g, /\bexport\s+[^'"()]*from\s*['"]([^'"]+)['"]/g, /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g, /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g ]; const seenFiles = new Set(); const specifiers = new Map(); // specifier -> 首次出现它的文件 function scan(file) { if (seenFiles.has(file)) return; seenFiles.add(file); let src; try { src = readFileSync(file, 'utf8'); } catch { return; } for (const re of SPECIFIER_PATTERNS) { for (const m of src.matchAll(re)) { const spec = m[1]; if (!specifiers.has(spec)) specifiers.set(spec, file); // 相对/绝对路径继续往下走(只跟本地文件,不跟 node_modules) if (spec.startsWith('.')) { const base = resolve(dirname(file), spec); for (const cand of [base, `${base}.js`, `${base}.mjs`, `${base}.cjs`, join(base, 'index.js')]) { if (existsSync(cand) && statSync(cand).isFile()) { scan(cand); break; } } } } } } scan(entryPath); /* * 解析必须走 **ESM**,不能用 require.resolve。 * * 实测教训:`@earendil-works/pi-coding-agent` 的 package.json 里 * `exports` 只声明了 `"import"`(没有 `"require"`),于是 * `require.resolve` 抛 ERR_PACKAGE_PATH_NOT_EXPORTED —— 而插件是 * `import` 它的,实际解析毫无问题。用错 API 会让这个门禁**报假缺陷**, * 而假缺陷比不检查更糟:它会让人去"修"一个根本没坏的东西。 * * 所以在子进程里按快照目录做 ESM 解析,一次批量传所有说明符。 */ const probe = ` // 子进程是独立上下文:这里必须自己 import(漏了就是 ReferenceError, // 而那个错误看起来像「探针坏了」,容易被当成环境问题绕过去)。 import { createRequire } from 'node:module'; const require_ = createRequire(process.cwd() + '/package.json'); const specs = JSON.parse(process.env.SNAPSHOT_SPECS || '[]'); const missing = []; for (const spec of specs) { let ok = false; // 先试 ESM(插件真正走的路),再退到 CJS(少数插件用 require) try { import.meta.resolve(spec); ok = true; } catch {} if (!ok) { try { require_.resolve(spec); ok = true; } catch {} } if (!ok) missing.push(spec); } process.stdout.write(JSON.stringify(missing)); `; const { spawnSync } = await import('node:child_process'); const proc = spawnSync( process.execPath, ['--input-type=module', '-e', probe], { cwd: snapshot, encoding: 'utf8', env: { ...process.env, SNAPSHOT_SPECS: JSON.stringify([...specifiers.keys()]) } } ); if (proc.status !== 0) { console.error(' 解析探针自身失败,无法判定(不要当成通过):'); console.error(` ${(proc.stderr || '').trim().split('\n').slice(-3).join('\n ')}`); process.exit(2); } const missingSpecs = JSON.parse(proc.stdout || '[]'); const missing = missingSpecs.map(spec => ({ spec, from: (specifiers.get(spec) || '').replace(`${snapshot}/`, '') })); const okList = [...specifiers.keys()].filter(s => !missingSpecs.includes(s)); console.log(` 扫描 ${seenFiles.size} 个本地模块,${specifiers.size} 个导入说明符`); if (missing.length === 0) { console.log(` 全部 ${okList.length} 个说明符可在快照内解析`); process.exit(0); } console.error(` 有 ${missing.length} 个说明符**无法**在快照内解析 —— 启动时必然失败:`); for (const m of missing) { console.error(` ${m.spec} (首次出现于 ${m.from})`); } console.error(' 提示:这类缺失通常是因为 package.json 没声明该依赖,'); console.error(' 于是拷贝时也没人可以依据 —— 只靠「node_modules 里碰巧有」是不够的。'); process.exit(1);