import { existsSync, readFileSync, readdirSync } from 'node:fs' import { join, resolve } from 'node:path' export const STANDALONE_PACKAGE_NAME = 'crawler-plugin-admin-frontend-vue' /** 后台壳层允许存在的源码目录(任务 1 冻结的工作区边界,供验收脚本复用)。 */ export const ADMIN_SOURCE_DIRS = [ 'src/layout', 'src/router', 'src/styles', 'src/types', 'src/config', 'src/pages', ] as const export const REQUIRED_MANIFEST_FILES = [ 'package.json', 'index.html', 'vite.config.ts', 'tsconfig.json', 'src/main.ts', 'src/App.vue', ] as const // 客户端工程耦合标记:后台工程内出现即代表越界。用带路径/产物语义的标记, // 避免与本工程自身包名后缀(…admin-frontend-vue)误匹配。扫描时排除本文件自身。 export const FORBIDDEN_COUPLING_TOKENS = [ 'new_web_source', 'frontend-vue/', 'app_client', ] as const const SELF_SCAN_EXCLUSIONS = new Set(['src/config/workspace.ts']) const SCAN_EXTENSIONS = /\.(ts|tsx|vue|css|html|json|js)$/ export function walk(rootDir: string, base = ''): string[] { const found: string[] = [] for (const entry of readdirSync(join(rootDir, base), { withFileTypes: true })) { if (['node_modules', 'dist', '.git', 'tests'].includes(entry.name)) continue const rel = base ? `${base}/${entry.name}` : entry.name if (entry.isDirectory()) found.push(...walk(rootDir, rel)) else if (SCAN_EXTENSIONS.test(entry.name)) found.push(rel) } return found } function fileExists(rootDir: string, path: string): boolean { return existsSync(join(rootDir, path)) } /** 从当前目录向上定位独立后台工程根目录,找不到时给出可操作错误。 */ export function resolveWorkspaceRoot(cwd = process.cwd()): string { let dir = resolve(cwd) for (let i = 0; i < 6; i += 1) { if (fileExists(dir, 'package.json')) return dir const parent = resolve(dir, '..') if (parent === dir) break dir = parent } throw new Error(`在 ${cwd} 向上 6 层内找不到 package.json,请在 admin-frontend-vue 工程目录内执行`) } export interface WorkspacePackage { name: string private: boolean scripts: Record dependencies: Record devDependencies: Record } export function readWorkspacePackage(rootDir = resolveWorkspaceRoot()): WorkspacePackage { const raw = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf8')) as Partial & { private?: boolean } return { name: raw.name || '', private: raw.private !== false, scripts: raw.scripts || {}, dependencies: raw.dependencies || {}, devDependencies: raw.devDependencies || {}, } } /** 校验独立工程边界,返回违规清单(空数组 = 边界成立)。 */ export function collectBoundaryViolations(rootDir = resolveWorkspaceRoot()): string[] { const violations: string[] = [] const pkgPath = join(rootDir, 'package.json') const pkg: WorkspacePackage = fileExists(rootDir, 'package.json') ? readWorkspacePackage(rootDir) : { name: '', private: false, scripts: {}, dependencies: {}, devDependencies: {} } if (!fileExists(rootDir, 'package.json')) violations.push(`缺少必需清单文件 package.json(在 ${pkgPath})`) if (pkg.name !== STANDALONE_PACKAGE_NAME) { violations.push(`package.name 应为 ${STANDALONE_PACKAGE_NAME},实际为 ${pkg.name || '(空)'}`) } if (!pkg.private) violations.push('package.private 应为 true(后台工程不允许被外部发布依赖)') for (const file of REQUIRED_MANIFEST_FILES) { if (!fileExists(rootDir, file)) violations.push(`缺少必需清单文件 ${file}`) } for (const dir of ADMIN_SOURCE_DIRS) { if (!fileExists(rootDir, dir)) violations.push(`缺少允许源码目录 ${dir}`) } for (const file of walk(rootDir)) { if (SELF_SCAN_EXCLUSIONS.has(file)) continue const content = readFileSync(join(rootDir, file), 'utf8') for (const token of FORBIDDEN_COUPLING_TOKENS) { if (content.includes(token)) { violations.push(`${file} 引用了客户端耦合 token "${token}",超出后台工程边界`) } } } return violations }