Files
xuetang/TUICallKit-Vue3/build/validate-tang-detective-output.mjs
T
2026-09-09 14:47:29 +08:00

111 lines
6.8 KiB
JavaScript

import fs from 'node:fs'
import path from 'node:path'
import vm from 'node:vm'
import { createRequire } from 'node:module'
import { fileURLToPath } from 'node:url'
import { createNativeManifest, createNativeOutputGuard, listFiles, sha256 } from './tang-detective-native-plugin.mjs'
import { isMediaFile, loadCosMediaManifest } from './tang-detective-cos-media.mjs'
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
export function validateNativeOutput(outputDirectory, { mediaManifest, mediaManifestPath } = {}) {
const outputGuard = createNativeOutputGuard(outputDirectory)
const output = outputGuard.root
const nativeRoot = outputGuard.path('tang-detective', 'directory')
const sourceRoot = path.join(projectRoot, 'native/tang-detective')
const sourceApp = JSON.parse(fs.readFileSync(path.join(sourceRoot, 'app.json'), 'utf8'))
const manifest = createNativeManifest(sourceApp)
const hostApp = JSON.parse(fs.readFileSync(outputGuard.path('app.json'), 'utf8'))
const imported = JSON.parse(fs.readFileSync(outputGuard.path('tang-detective/.native-import-state.json'), 'utf8'))
const sourceManifest = JSON.parse(fs.readFileSync(path.join(projectRoot, 'build/tang-detective-source-manifest.json'), 'utf8'))
const localRequire = createRequire(path.join(output, 'native-validation.cjs'))
const errors = []
const check = (condition, message) => { if (!condition) errors.push(message) }
const remoteMode = imported.media?.mode === 'cos'
let media = null
if (remoteMode) {
try {
media = loadCosMediaManifest({ sourceDirectory: sourceRoot, mediaManifest, mediaManifestPath })
check(Boolean(media), 'COS output requires its verified media manifest')
if (media) {
check(imported.media.manifestSha256 === media.manifestSha256, 'COS manifest changed after import')
check(imported.media.sourceManifestSha256 === media.sourceManifestSha256, 'COS source manifest changed after import')
}
} catch (error) { errors.push(error.message) }
}
const allRoutes = [...hostApp.pages, ...(hostApp.subPackages || hostApp.subpackages || []).flatMap(item => item.pages.map(page => `${item.root}/${page}`))]
const packageOwner = relative => manifest.subPackages.find(item => relative.startsWith(`${item.root}/`))?.root || 'main'
let mediaFiles = 0
for (const file of sourceManifest.files) {
check(sha256(fs.readFileSync(path.join(sourceRoot, file.path))) === file.sha256, `Source snapshot drift: ${file.path}`)
if (!isMediaFile(file.path)) continue
mediaFiles += 1
outputGuard.path(`tang-detective/${file.path}`)
if (remoteMode) {
check(!fs.existsSync(path.join(nativeRoot, file.path)), `Packaged media remains in COS mode: ${file.path}`)
check(media?.entries.get(file.path)?.sha256 === file.sha256, `Remote media mapping changed: ${file.path}`)
} else {
check(sha256(fs.readFileSync(path.join(nativeRoot, file.path))) === file.sha256, `Media changed: ${file.path}`)
}
}
for (const file of imported.files) {
check(sha256(fs.readFileSync(outputGuard.path(`tang-detective/${file.path}`))) === file.sha256, `Native output changed after import: ${file.path}`)
}
const packagedMediaFiles = listFiles(nativeRoot).filter(isMediaFile).length
if (remoteMode) check(packagedMediaFiles === 0, 'Unexpected native packaged media in COS mode')
for (const page of manifest.allPages) {
check(allRoutes.filter(route => route === page).length === 1, `Page registration missing or duplicated: ${page}`)
for (const extension of ['.js', '.json', '.wxml', '.wxss']) check(fs.existsSync(path.join(output, `${page}${extension}`)), `Missing page file: ${page}${extension}`)
const config = JSON.parse(fs.readFileSync(path.join(output, `${page}.json`), 'utf8'))
check(config.navigationStyle === 'custom' && config.pageOrientation === 'landscape', `Native display config missing: ${page}`)
const script = fs.readFileSync(path.join(output, `${page}.js`), 'utf8')
check(!/^Page\(\{/m.test(script) && /require\(["']\.\.\/(?:\.\.\/)*utils\/tangPage\.js["']\)\(\{/.test(script), `Page lifecycle wrapper missing: ${page}`)
const style = fs.readFileSync(path.join(output, `${page}.wxss`), 'utf8')
check(/@import\s+["']\/tang-detective\/shared\.wxss["'];/.test(style), `Native style import missing: ${page}`)
}
let dependencies = 0
for (const relative of listFiles(nativeRoot).filter(file => file.endsWith('.js'))) {
const filename = path.join(nativeRoot, relative)
const script = fs.readFileSync(filename, 'utf8')
try { new vm.Script(script, { filename: relative }) } catch (error) { errors.push(error.message) }
for (const [, specifier] of script.matchAll(/require\(['"]([^'"]+)['"]\)/g)) {
dependencies += 1
if (!specifier.startsWith('.')) { errors.push(`Non-relative native dependency: ${relative} -> ${specifier}`); continue }
try {
const resolved = localRequire.resolve(path.resolve(path.dirname(filename), specifier))
const dependency = path.relative(output, resolved).split(path.sep).join('/')
check(dependency.startsWith('tang-detective/'), `Dependency escaped namespace: ${relative} -> ${specifier}`)
check(packageOwner(dependency) === 'main' || packageOwner(dependency) === packageOwner(`tang-detective/${relative}`), `Illegal sibling package import: ${relative} -> ${specifier}`)
} catch (error) { errors.push(`Unresolved require: ${relative} -> ${specifier}: ${error.message}`) }
}
}
const outputSizes = { totalFileBytes: 0, mainPackageFileBytes: 0, subPackages: {} }
const outputPackages = hostApp.subPackages || hostApp.subpackages || []
for (const relative of listFiles(output)) {
const bytes = fs.statSync(path.join(output, relative)).size
outputSizes.totalFileBytes += bytes
const subpackage = outputPackages.find(item => relative.startsWith(`${item.root}/`))
if (subpackage) outputSizes.subPackages[subpackage.root] = (outputSizes.subPackages[subpackage.root] || 0) + bytes
else outputSizes.mainPackageFileBytes += bytes
}
return {
passed: errors.length === 0,
nativePages: manifest.allPages.length,
mediaFiles,
packagedMediaFiles,
mediaMode: remoteMode ? 'cos' : 'local',
relativeDependencies: dependencies,
nativeSizes: imported.sizes,
outputSizes,
errors,
boundary: 'Static source/asset/dependency/manifest verification only. Raw file sizes are not WeChat upload package sizes; no simulator, device, audio listening, upload, or release acceptance was performed.',
}
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
const report = validateNativeOutput(path.resolve(projectRoot, process.argv[2] || 'dist/build/mp-weixin'))
if (process.argv[3]) fs.writeFileSync(path.resolve(projectRoot, process.argv[3]), `${JSON.stringify(report, null, 2)}\n`)
console.log(JSON.stringify(report, null, 2))
if (!report.passed) process.exitCode = 1
}