import { existsSync, readFileSync } from 'node:fs' import { dirname, join, resolve } from 'node:path' const ROOT = process.cwd() const SRC = join(ROOT, 'src') const EXTENSIONS = ['', '.ts', '.tsx', '.vue', '.js', '.json'] const INDEX_CANDIDATES = ['/index.ts', '/index.tsx', '/index.vue', '/index.js'] /** 读取工程内源码文件(相对工程根)。 */ export function readSource(rel: string): string { return readFileSync(join(ROOT, rel), 'utf8') } /** 统计非重叠子串出现次数(幂等、只读)。 */ export function occurrences(text: string, token: string): number { if (!token) return 0 let count = 0 let index = text.indexOf(token) while (index !== -1) { count += 1 index = text.indexOf(token, index + token.length) } return count } /** 从源码里抓出所有 @/ 别名 import 说明符(含 import type)。 */ export function aliasImports(source: string): string[] { const specs = new Set() const re = /(?:import\s+type\s+)?[^'"]*from\s*['"](@\/[^'"]+)['"]/g let match: RegExpExecArray | null while ((match = re.exec(source))) specs.add(match[1]) // 处理动态 import('@/...') 与直接裸 @/ 引用 const dyn = /(?:import|import\s*\(\s*)['"](@\/[^'"]+)['"]/g while ((match = dyn.exec(source))) specs.add(match[1]) return [...specs] } /** 把 import 说明符解析为存在的模块文件;解析不到返回 null(含可操作路径)。 */ export function resolveModuleSpec(spec: string, fromRel: string): string | null { let base: string if (spec.startsWith('@/')) { base = join(SRC, spec.slice(2)) } else { base = resolve(dirname(join(ROOT, fromRel)), spec) } for (const ext of EXTENSIONS) { if (existsSync(base + ext)) return base + ext } for (const idx of INDEX_CANDIDATES) { if (existsSync(base + idx)) return base + idx } return null } /** 校验一个源文件的所有 @/ import 都能解析;返回缺失清单(空 = 全部可解析)。 */ export function missingImports(rel: string): string[] { const source = readSource(rel) const missing: string[] = [] for (const spec of aliasImports(source)) { if (!resolveModuleSpec(spec, rel)) missing.push(`${rel} 无法解析 import: ${spec}`) } return missing }