const fs = require('node:fs') const path = require('node:path') const { spawnSync } = require('node:child_process') const { isTextBuildFile, resolveH5BuildPolicy, tencentMapKeyPlaceholder } = require('./finalize-h5-build-policy.cjs') const projectRoot = path.resolve(__dirname, '..') const h5Root = path.join(projectRoot, 'dist', 'build', 'h5') const placeholder = tencentMapKeyPlaceholder const badPatterns = [ 'http://1.92.206.90:9000', '1.92.206.90', 'guide.whaoyue.com', placeholder ] const { allowPlaceholder, requireTencentMapKey, tencentMapKey } = resolveH5BuildPolicy({ args: process.argv.slice(2), projectRoot }) require('./copy-h5-nav-assets.cjs') if (!fs.existsSync(h5Root)) { throw new Error(`H5 build output not found: ${h5Root}`) } const walkFiles = (dir, files = []) => { for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { const filePath = path.join(dir, entry.name) if (entry.isDirectory()) { walkFiles(filePath, files) continue } if (entry.isFile()) { files.push(filePath) } } return files } const allFiles = walkFiles(h5Root) const textFiles = allFiles.filter(isTextBuildFile) const placeholderFiles = textFiles.filter((file) => fs.readFileSync(file, 'utf8').includes(placeholder)) const hasUsableTencentMapKey = tencentMapKey.length > 0 const missingTencentMapKeyMessage = [ 'Production H5 build requires a well-formed non-placeholder VITE_TENCENT_MAP_KEY.', 'Inject it through CI or an ignored .env.production.local file.' ].join(' ') if (requireTencentMapKey && !hasUsableTencentMapKey) { throw new Error(missingTencentMapKeyMessage) } if (placeholderFiles.length > 0) { if (!hasUsableTencentMapKey) { const message = `H5 build output still contains Tencent map key placeholder. ${missingTencentMapKeyMessage}` if (requireTencentMapKey && !allowPlaceholder) { throw new Error(message) } console.warn(`[finalize-h5-build] ${message}`) } else { // 所有构建产物文本替换必须由 Node 按 UTF-8 完成,避免 PowerShell 默认编码破坏中文注释或字符串。 for (const file of placeholderFiles) { const text = fs.readFileSync(file, 'utf8') fs.writeFileSync(file, text.split(placeholder).join(tencentMapKey), 'utf8') } } } const indexPath = path.join(h5Root, 'index.html') const indexHtml = fs.readFileSync(indexPath, 'utf8') const entryMatch = indexHtml.match(/src="\/?([^"]*assets\/index-[^"]+\.js)"/) if (!entryMatch) { throw new Error(`Entry JS not found in ${indexPath}`) } const entryFile = path.join(h5Root, entryMatch[1]) if (!fs.existsSync(entryFile)) { throw new Error(`Entry JS file not found: ${entryFile}`) } const jsFiles = allFiles.filter((file) => /\.js$/i.test(file)) for (const file of jsFiles) { const check = spawnSync(process.execPath, ['--check', file], { encoding: 'utf8' }) if (check.status !== 0) { process.stdout.write(check.stdout || '') process.stderr.write(check.stderr || '') throw new Error(`JavaScript syntax check failed: ${path.relative(projectRoot, file)}`) } } const badMatches = [] for (const file of textFiles) { const text = fs.readFileSync(file, 'utf8') const matchedPattern = badPatterns.find((pattern) => text.includes(pattern)) if (matchedPattern) { badMatches.push({ file: path.relative(projectRoot, file), pattern: matchedPattern }) } } if (badMatches.length > 0) { if (allowPlaceholder) { console.warn('[finalize-h5-build] Test H5 build allows non-production source hosts and the Tencent map key placeholder.') } else { console.error(JSON.stringify(badMatches.slice(0, 20), null, 2)) throw new Error('H5 build output contains forbidden test host, old domain, or placeholder.') } } const readJson = (relativePath) => JSON.parse(fs.readFileSync(path.join(h5Root, relativePath), 'utf8')) const manifest = readJson('static/guide-data/manifest.json') const guideContents = readJson('static/guide-data/guide-contents.json') const exhibits = readJson('static/guide-data/exhibits.json') const guideStops = readJson('static/guide-data/guide-stops.json') const requiredFiles = [ 'static/guide/museum-brand-icons.webp', 'static/icons/poi/shortcut-icons.svg', 'static/icons/marker-entrance.svg', 'static/Fonts/HarmonyOS_SansSC_Regular.ttf' ] const missingFiles = requiredFiles.filter((file) => !fs.existsSync(path.join(h5Root, file))) if (missingFiles.length > 0) { throw new Error(`Required H5 static assets are missing: ${missingFiles.join(', ')}`) } console.log(`H5_ENTRY=${entryMatch[1]}`) console.log(`H5_ENTRY_SIZE=${fs.statSync(entryFile).size}`) console.log(`H5_JS_SYNTAX_CHECKED=${jsFiles.length}`) console.log(`H5_TENCENT_PLACEHOLDER_REPLACED_FILES=${hasUsableTencentMapKey ? placeholderFiles.length : 0}`) console.log(`H5_BAD_STRING_FILE_COUNT=${badMatches.length}`) console.log(`H5_MANIFEST_SOURCE_HOST=${manifest.source?.sourceHost || ''}`) console.log(`H5_GUIDE_CONTENTS_ROWS=${guideContents.rows?.length || 0}`) console.log(`H5_EXHIBITS_ROWS=${exhibits.rows?.length || 0}`) console.log(`H5_GUIDE_STOPS_ROWS=${guideStops.rows?.length || 0}`)