Files
jiapuapp/scripts/check-project.mjs
T

134 lines
5.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.jsongenealogy-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}`)