import fs from 'node:fs' import path from 'node:path' import YAML from 'yaml' const workspace = process.cwd() const failures = [] const fail = (message) => failures.push(message) const readText = (filePath) => fs.readFileSync(path.join(workspace, filePath), 'utf8') const listFiles = (entryPaths, extensions) => { const files = [] const visit = (entryPath) => { const absolutePath = path.join(workspace, entryPath) if (!fs.existsSync(absolutePath)) return const stat = fs.statSync(absolutePath) if (stat.isDirectory()) { for (const name of fs.readdirSync(absolutePath)) { visit(path.join(entryPath, name)) } return } if (extensions.has(path.extname(entryPath))) files.push(entryPath) } entryPaths.forEach(visit) return files } const parseJson = (filePath) => { try { return JSON.parse(readText(filePath)) } catch (error) { fail(`${filePath} 不是有效 JSON:${error.message}`) return null } } const pagesConfig = parseJson('pages.json') parseJson('manifest.json') parseJson('package.json') parseJson('package-lock.json') const runtimeConfigSource = readText('utils/runtime-config.js') if (runtimeConfigSource.includes('import.meta.env')) { fail('运行时配置直接读取 import.meta.env,会把构建机环境写入生产资源') } if (/\bmock\b|isMockMode|resolveRuntimeMode/.test(runtimeConfigSource)) { fail('正式运行时配置仍保留不可达的 mock 双轨逻辑') } for (const productionValue of [ "const configuredBaseUrl = 'https://backend-api.ddxcjp.cn'", "const configuredClientId = '428a8310cd442757ae699df5d894f051'", "const configuredTenantId = '000000'", ]) { if (!runtimeConfigSource.includes(productionValue)) { fail(`运行时正式配置不完整:${productionValue}`) } } if (fs.existsSync(path.join(workspace, 'data', 'preview'))) { fail('正式项目仍保留不可达的本地预览数据目录 data/preview') } const routeSource = readText('utils/navigation/routes.js') const routeEntries = [...routeSource.matchAll(/^\s{2}([A-Z]\d{2}): defineRoute\(\{[\s\S]*?^\s{2}\}\),/gm)] const routeKeys = routeEntries.map((match) => match[1]) const routePaths = routeEntries.map((match) => match[0].match(/path: "([^"]+)"/)?.[1]?.replace(/^\//, '')) const configuredPages = pagesConfig?.pages?.map((page) => page.path) || [] if (new Set(routeKeys).size !== routeKeys.length) fail('路由键存在重复') if (routePaths.some((routePath) => !routePath)) fail('存在没有静态 path 的路由') for (const pagePath of configuredPages) { if (!fs.existsSync(path.join(workspace, `${pagePath}.vue`))) fail(`页面文件缺失:${pagePath}.vue`) if (!routePaths.includes(pagePath)) fail(`页面没有路由语义:${pagePath}`) } for (const routePath of routePaths) { if (!configuredPages.includes(routePath)) fail(`路由没有在 pages.json 注册:${routePath}`) } const sourceFiles = listFiles( ['App.vue', 'main.js', 'pages', 'components', 'composables', 'services', 'utils'], new Set(['.js', '.vue']), ) if (/hasRemoteConfig|REMOTE_(?:READ|WRITE)_REQUIRED|WRITE_UNAVAILABLE|本地预览/.test( sourceFiles.map((filePath) => readText(filePath)).join('\n') )) { fail('正式源码仍保留已经不可达的本地预览守卫或错误码') } const importPattern = /(?:from\s*|import\s*)["']([^"']+)["']/g for (const filePath of sourceFiles) { const source = readText(filePath) if (source.includes('@/data/preview')) { fail(`${filePath} 仍引用本地预览数据`) } if (filePath.endsWith('.vue') && !/<(?:template|script)(?:\s|>)/.test(source)) { fail(`${filePath} 缺少