test: replace legacy suite with core project checks
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
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 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']),
|
||||
)
|
||||
const importPattern = /(?:from\s*|import\s*)["']([^"']+)["']/g
|
||||
for (const filePath of sourceFiles) {
|
||||
const source = readText(filePath)
|
||||
if (filePath.endsWith('.vue') && !/<(?:template|script)(?:\s|>)/.test(source)) {
|
||||
fail(`${filePath} 缺少 <template> 或 <script>`)
|
||||
}
|
||||
for (const match of source.matchAll(importPattern)) {
|
||||
const specifier = match[1]
|
||||
if (!specifier.startsWith('.') && !specifier.startsWith('@/')) continue
|
||||
const basePath = specifier.startsWith('@/')
|
||||
? path.join(workspace, specifier.slice(2))
|
||||
: path.resolve(workspace, path.dirname(filePath), specifier)
|
||||
const candidates = [basePath, `${basePath}.js`, `${basePath}.vue`, path.join(basePath, 'index.js')]
|
||||
if (!candidates.some((candidate) => fs.existsSync(candidate))) {
|
||||
fail(`${filePath} 导入不存在:${specifier}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const visualFiles = listFiles(
|
||||
['App.vue', 'uni.scss', 'pages', 'components', 'styles'],
|
||||
new Set(['.vue', '.scss']),
|
||||
)
|
||||
const assetPattern = /["'(]((?:\/@?static\/|@\/static\/)[^"')\s]+)["')]/g
|
||||
for (const filePath of visualFiles) {
|
||||
for (const match of readText(filePath).matchAll(assetPattern)) {
|
||||
const assetPath = match[1].replace(/^@?\//, '')
|
||||
if (!fs.existsSync(path.join(workspace, assetPath))) fail(`${filePath} 引用不存在的资源:${match[1]}`)
|
||||
}
|
||||
}
|
||||
|
||||
let openApi = null
|
||||
try {
|
||||
openApi = YAML.parse(readText('genealogy-app-openapi.yaml'))
|
||||
} catch (error) {
|
||||
fail(`genealogy-app-openapi.yaml 无法解析:${error.message}`)
|
||||
}
|
||||
if (fs.existsSync(path.join(workspace, '家谱.openapi.json'))) {
|
||||
fail('检测到旧 OpenAPI:家谱.openapi.json;genealogy-app-openapi.yaml 必须是唯一所有者')
|
||||
}
|
||||
|
||||
const normalizeEndpointPath = (endpointPath) => endpointPath
|
||||
.split('?')[0]
|
||||
.replace(/\$\{[^}]+\}/g, '{}')
|
||||
.replace(/\{[^}]+\}/g, '{}')
|
||||
const documentedPaths = new Set(Object.keys(openApi?.paths || {}).map(normalizeEndpointPath))
|
||||
const dynamicEndpointBuilders = new Set([
|
||||
'/genealogy/app/genealogies/{}/{}',
|
||||
'/genealogy/app/genealogies/{}/lineage/persons/{}/{}',
|
||||
])
|
||||
const apiFiles = listFiles(['services/api'], new Set(['.js']))
|
||||
.filter((filePath) => filePath.endsWith('-service.js') || filePath.endsWith('request-client.js'))
|
||||
const endpointPattern = /([`'"])(\/(?:genealogy|captcha|auth)[\s\S]*?)\1/g
|
||||
for (const filePath of apiFiles) {
|
||||
for (const match of readText(filePath).matchAll(endpointPattern)) {
|
||||
const endpoint = match[2]
|
||||
if (endpoint.includes('\n')) continue
|
||||
const normalizedPath = normalizeEndpointPath(endpoint)
|
||||
if (!documentedPaths.has(normalizedPath) && !dynamicEndpointBuilders.has(normalizedPath)) {
|
||||
fail(`${filePath} 使用了 OpenAPI 未声明的路径:${endpoint}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error(`PROJECT CHECK FAILED (${failures.length})`)
|
||||
failures.forEach((message) => console.error(`- ${message}`))
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`PROJECT CHECK PASS pages=${configuredPages.length} routes=${routeKeys.length} sources=${sourceFiles.length}`)
|
||||
Reference in New Issue
Block a user