Files
claude-code/scripts/check-bundle-integrity.ts
2026-05-01 21:39:30 +08:00

373 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bun
/**
* 构建产物完整性检查脚本
*
* 检查 Bun.build({ splitting: true }) 输出的 dist/ 目录中是否存在:
* 1. 引用了不存在的 chunk 文件(断链)
* 2. 通过 __require() 或 import() 引用的第三方模块(非 Node.js 内置),在生产环境中会找不到
* 3. 缺失的静态 import 依赖(跨 chunk 引用目标不存在)
*
* 用法:
* bun scripts/check-bundle-integrity.ts # 检查当前 dist/
* bun scripts/check-bundle-integrity.ts ./dist # 指定目录
*/
import { readdir, readFile } from 'fs/promises'
import { join, resolve, dirname } from 'path'
import { fileURLToPath } from 'url'
// ─── 从 package.json 读取 dependencies 作为白名单 ────────────────
const __dirname = dirname(fileURLToPath(import.meta.url))
const pkg = JSON.parse(
await readFile(join(__dirname, '..', 'package.json'), 'utf-8'),
)
const PKG_DEPS = new Set(Object.keys(pkg.dependencies ?? {}))
// ─── Node.js 内置模块白名单 ────────────────────────────────────────
const NODE_BUILTINS = new Set([
'assert',
'async_hooks',
'buffer',
'child_process',
'cluster',
'console',
'constants',
'crypto',
'dgram',
'diagnostics_channel',
'dns',
'domain',
'events',
'fs',
'fs/promises',
'http',
'http2',
'https',
'inspector',
'module',
'net',
'os',
'path',
'perf_hooks',
'process',
'punycode',
'querystring',
'readline',
'repl',
'stream',
'string_decoder',
'sys',
'timers',
'tls',
'tty',
'url',
'util',
'v8',
'vm',
'worker_threads',
'zlib',
'node:test',
])
// Node 18+ 内置但不在传统列表中的模块
const NODE_18_PLUS_BUILTINS = new Set(['undici'])
// Bun 专用模块(仅在 Bun 运行时可用Node.js 环境会失败)
const BUN_MODULES = new Set(['bun', 'bun:ffi', 'bun:test', 'bun:sqlite'])
// macOS JXA / native 框架(通过 ObjC.import非真正的 require
const NATIVE_FRAMEWORKS = new Set([
'AppKit',
'CoreGraphics',
'Foundation',
'UIKit',
])
// ─── 模式 ──────────────────────────────────────────────────────────
// 匹配 import { ... } from "./chunk-xxxxx.js" 或 import"./chunk-xxxxx.js"
const STATIC_IMPORT_RE = /(?:from\s+|import\s+)"(\.\/[^"]+\.js)"/g
// 匹配 __require("xxx")
const REQUIRE_RE = /__require\("([^"]+)"\)/g
// 匹配动态 import("xxx"),排除 ./chunk-xxx.js 的内部引用
const DYNAMIC_IMPORT_RE = /import\("([^"]+)"\)/g
// 匹配 nodeRequire("xxx")createRequire 创建的 require 别名)
const NODE_REQUIRE_RE = /nodeRequire\("([^"]+)"\)/g
interface Finding {
type:
| 'broken-chunk-ref'
| 'third-party-require'
| 'third-party-import'
| 'third-party-node-require'
| 'bun-runtime-only'
severity: 'error' | 'warning'
file: string
line: number
module: string
snippet: string
}
async function main() {
const distDir = resolve(process.argv[2] || './dist')
console.log(`\n🔍 检查构建产物完整性: ${distDir}\n`)
// 1. 列出所有 chunk 文件
let files: string[]
try {
files = (await readdir(distDir)).filter(f => f.endsWith('.js'))
} catch {
console.error(`❌ 无法读取目录: ${distDir}`)
console.error(' 请先运行 bun run build')
process.exit(1)
}
const fileSet = new Set(files)
console.log(`📦 找到 ${files.length} 个 JS 文件\n`)
const findings: Finding[] = []
// 2. 逐文件扫描
for (const file of files) {
const filePath = join(distDir, file)
const content = await readFile(filePath, 'utf-8')
const lines = content.split('\n')
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
const lineNum = i + 1
// 2a. 检查静态 chunk 引用是否断链
const staticImportMatches = line.matchAll(STATIC_IMPORT_RE)
for (const m of staticImportMatches) {
const ref = m[1]
// 提取文件名部分(去掉 ./
const refFile = ref.replace(/^\.\//, '')
if (!fileSet.has(refFile)) {
findings.push({
type: 'broken-chunk-ref',
severity: 'error',
file,
line: lineNum,
module: ref,
snippet: line.trim().slice(0, 120),
})
}
}
// 2b. 检查 __require 中的第三方模块
const requireMatches = line.matchAll(REQUIRE_RE)
for (const m of requireMatches) {
const mod = m[1]
// 跳过 ObjC.importJXA 语法,不是真正的 require
if (NATIVE_FRAMEWORKS.has(mod)) continue
if (
NODE_BUILTINS.has(mod) ||
NODE_18_PLUS_BUILTINS.has(mod) ||
PKG_DEPS.has(mod) ||
mod.startsWith('node:')
)
continue
if (BUN_MODULES.has(mod)) {
findings.push({
type: 'bun-runtime-only',
severity: 'warning',
file,
line: lineNum,
module: mod,
snippet: line.trim().slice(0, 120),
})
continue
}
// 第三方模块 — 在生产环境(全局 npm install中找不到
findings.push({
type: 'third-party-require',
severity: 'error',
file,
line: lineNum,
module: mod,
snippet: line.trim().slice(0, 120),
})
}
// 2c. 检查动态 import() 中的第三方模块
const dynImportMatches = line.matchAll(DYNAMIC_IMPORT_RE)
for (const m of dynImportMatches) {
const mod = m[1]
// 跳过内部 chunk 引用和相对路径
if (mod.startsWith('./') || mod.startsWith('../')) continue
// 跳过 ObjC.import
if (NATIVE_FRAMEWORKS.has(mod)) continue
if (
NODE_BUILTINS.has(mod) ||
NODE_18_PLUS_BUILTINS.has(mod) ||
PKG_DEPS.has(mod) ||
mod.startsWith('node:')
)
continue
if (BUN_MODULES.has(mod)) {
// bun:test 等只在 Bun 运行时可用Node.js 运行时会失败
findings.push({
type: 'bun-runtime-only',
severity: 'warning',
file,
line: lineNum,
module: mod,
snippet: line.trim().slice(0, 120),
})
continue
}
// 第三方动态 import
findings.push({
type: 'third-party-import',
severity: 'error',
file,
line: lineNum,
module: mod,
snippet: line.trim().slice(0, 120),
})
}
// 2d. 检查 nodeRequire("xxx") 中的第三方模块createRequire 别名)
const nodeRequireMatches = line.matchAll(NODE_REQUIRE_RE)
for (const m of nodeRequireMatches) {
const mod = m[1]
if (NATIVE_FRAMEWORKS.has(mod)) continue
if (
NODE_BUILTINS.has(mod) ||
NODE_18_PLUS_BUILTINS.has(mod) ||
PKG_DEPS.has(mod) ||
mod.startsWith('node:')
)
continue
if (BUN_MODULES.has(mod)) {
findings.push({
type: 'bun-runtime-only',
severity: 'warning',
file,
line: lineNum,
module: mod,
snippet: line.trim().slice(0, 120),
})
continue
}
findings.push({
type: 'third-party-node-require',
severity: 'error',
file,
line: lineNum,
module: mod,
snippet: line.trim().slice(0, 120),
})
}
}
}
// 3. 汇总报告
const errors = findings.filter(f => f.severity === 'error')
const warnings = findings.filter(f => f.severity === 'warning')
// 按 type 分组
const brokenRefs = errors.filter(f => f.type === 'broken-chunk-ref')
const thirdPartyRequires = errors.filter(
f => f.type === 'third-party-require',
)
const thirdPartyImports = errors.filter(f => f.type === 'third-party-import')
const thirdPartyNodeRequires = errors.filter(
f => f.type === 'third-party-node-require',
)
const bunRuntimeOnly = warnings.filter(f => f.type === 'bun-runtime-only')
if (brokenRefs.length > 0) {
console.log('❌ 断裂的 chunk 引用(引用了不存在的文件):')
for (const f of brokenRefs) {
console.log(` ${f.file}:${f.line}${f.module}`)
}
console.log()
}
if (thirdPartyRequires.length > 0) {
console.log('❌ 通过 __require() 引用的第三方模块(生产环境会找不到):')
const grouped = groupByModule(thirdPartyRequires)
for (const [mod, items] of grouped) {
console.log(` "${mod}" — 出现 ${items.length} 次:`)
for (const f of items.slice(0, 5)) {
console.log(` ${f.file}:${f.line}`)
}
if (items.length > 5) console.log(` ... 还有 ${items.length - 5}`)
}
console.log()
}
if (thirdPartyImports.length > 0) {
console.log('❌ 通过 import() 动态引用的第三方模块(生产环境会找不到):')
const grouped = groupByModule(thirdPartyImports)
for (const [mod, items] of grouped) {
console.log(` "${mod}" — 出现 ${items.length} 次:`)
for (const f of items.slice(0, 5)) {
console.log(` ${f.file}:${f.line}`)
}
if (items.length > 5) console.log(` ... 还有 ${items.length - 5}`)
}
console.log()
}
if (thirdPartyNodeRequires.length > 0) {
console.log(
'❌ 通过 nodeRequire() 引用的第三方模块(绕过打包,生产环境会找不到):',
)
const grouped = groupByModule(thirdPartyNodeRequires)
for (const [mod, items] of grouped) {
console.log(` "${mod}" — 出现 ${items.length} 次:`)
for (const f of items.slice(0, 5)) {
console.log(` ${f.file}:${f.line}`)
}
if (items.length > 5) console.log(` ... 还有 ${items.length - 5}`)
}
console.log()
}
if (bunRuntimeOnly.length > 0) {
console.log('⚠️ Bun 运行时专用模块Node.js 环境会失败):')
const grouped = groupByModule(bunRuntimeOnly)
for (const [mod, items] of grouped) {
console.log(` "${mod}" — 出现 ${items.length}`)
}
console.log()
}
// 4. 总结
console.log('─'.repeat(50))
if (errors.length === 0 && warnings.length === 0) {
console.log('✅ 构建产物完整性检查通过,未发现问题。')
} else {
console.log(`📊 总计: ${errors.length} 个错误, ${warnings.length} 个警告`)
if (errors.length > 0) {
console.log(
`\n💡 修复建议:
- 第三方模块问题:在 build.ts 中通过 external 选项排除,或确保它们被正确打包到 chunk 中
- 断链问题:检查 build 时是否有文件被意外删除或构建不完整
- Bun 专用模块:确保运行时使用 bun 而非 node`,
)
}
}
process.exit(errors.length > 0 ? 1 : 0)
}
function groupByModule(items: Finding[]): Map<string, Finding[]> {
const map = new Map<string, Finding[]>()
for (const item of items) {
const list = map.get(item.module) || []
list.push(item)
map.set(item.module, list)
}
// 按出现次数降序
return new Map([...map.entries()].sort((a, b) => b[1].length - a[1].length))
}
main().catch(err => {
console.error('Fatal error:', err)
process.exit(2)
})