import { existsSync, readFileSync, readdirSync } from 'node:fs' import path from 'node:path' const SOURCE_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx'] function inside(file, directory) { const relative = path.relative(directory, file) return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative) } function walk(directory) { if (!existsSync(directory)) return [] return readdirSync(directory, { withFileTypes: true }).flatMap(entry => { const absolute = path.join(directory, entry.name) if (entry.isDirectory()) return walk(absolute) return SOURCE_EXTENSIONS.includes(path.extname(entry.name)) ? [absolute] : [] }) } function sourceSpecifiers(file) { const source = readFileSync(file, 'utf8') const result = [] for (const pattern of [ /(?:import|export)\s+(?:[^'";]*?\s+from\s+)?['"]([^'"]+)['"]/g, /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g, /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g, ]) { for (const match of source.matchAll(pattern)) result.push(match[1]) } return result } function aliasMappings(projectRoot) { const conventional = [ { alias: '@app', root: path.join(projectRoot, 'src', 'app') }, { alias: '@shared', root: path.join(projectRoot, 'src', 'shared') }, { alias: '@plugins', root: path.join(projectRoot, 'src', 'plugins') }, ] const tsconfigFile = path.join(projectRoot, 'tsconfig.json') if (!existsSync(tsconfigFile)) return conventional let config try { config = JSON.parse(readFileSync(tsconfigFile, 'utf8')) } catch { // 带注释的 tsconfig 仍使用稳定约定别名;doctor 不应为了静态门禁要求宿主改格式。 return conventional } const paths = config.compilerOptions?.paths ?? {} return [ ...conventional, ...Object.entries(paths).flatMap(([pattern, targets]) => { const alias = pattern.replace(/\/\*$/, '') return (targets ?? []).map(target => ({ alias, root: path.resolve(projectRoot, target.replace(/\/\*$/, '')), })) }), ] } function resolveSource(importer, specifier, aliases) { let base if (specifier.startsWith('.')) { base = path.resolve(path.dirname(importer), specifier) } else { const mapping = aliases .sort((left, right) => right.alias.length - left.alias.length) .find(item => specifier === item.alias || specifier.startsWith(`${item.alias}/`)) if (!mapping) return null const suffix = specifier === mapping.alias ? '' : specifier.slice(mapping.alias.length + 1) base = path.join(mapping.root, suffix) } return [ base, ...SOURCE_EXTENSIONS.map(extension => `${base}${extension}`), ...SOURCE_EXTENSIONS.map(extension => path.join(base, `index${extension}`)), ].find(existsSync) } /** * 这些是所有多 Bundle 宿主都必须满足的交付边界,不属于任何具体 App 的业务规则。 */ export function validateSourceBoundaries(projectRoot, config) { if (!config.entryGeneration) return [] const aliases = aliasMappings(projectRoot) const appRoot = path.dirname(path.resolve(projectRoot, config.entryGeneration.appComponent)) const commonRoots = ( config.modules.find(module => module.type === 'common')?.ownershipRoots ?? [] ).map(root => path.resolve(projectRoot, root)) const pluginRoots = config.modules .filter(module => module.type === 'buz') .map(module => ({ id: module.id, root: path.dirname(path.resolve(projectRoot, module.entry)) })) const violations = [] function inspect(importer, owner, isForbidden) { for (const specifier of sourceSpecifiers(importer)) { const dependency = resolveSource(importer, specifier, aliases) if (!dependency || !isForbidden(dependency)) continue violations.push( `${owner}: ${path.relative(projectRoot, importer)} -> ${path.relative( projectRoot, dependency, )}`, ) } } for (const commonRoot of commonRoots) { for (const importer of walk(commonRoot)) { inspect( importer, 'common 不得依赖宿主或插件', dependency => inside(dependency, appRoot) || pluginRoots.some(plugin => inside(dependency, plugin.root)), ) } } for (const plugin of pluginRoots) { for (const importer of walk(plugin.root)) { inspect( importer, `插件 ${plugin.id} 只能依赖自身、common 和三方库`, dependency => inside(dependency, appRoot) || pluginRoots.some( candidate => candidate.id !== plugin.id && inside(dependency, candidate.root), ), ) } } for (const importer of walk(appRoot)) { inspect(importer, '宿主不得静态导入插件', dependency => pluginRoots.some(plugin => inside(dependency, plugin.root)), ) } return violations }