import { readFileSync, readdirSync, statSync } from 'node:fs'; import { resolve } from 'node:path'; const assetsDirectory = resolve(process.cwd(), 'dist/assets'); const assets = readdirSync(assetsDirectory); const excelAssets = assets.filter((name) => /^exceljs(?:\.min)?-[\w-]+\.js$/.test(name)); const mileageWorkers = assets.filter((name) => /^mileageExport\.worker-[\w-]+\.js$/.test(name)); const monitorAssets = assets.filter((name) => /^MonitorPage-[\w-]+\.js$/.test(name)); const runtimeConfig = readFileSync(resolve(process.cwd(), 'dist/app-config.js'), 'utf8'); const safeRuntimeTemplate = readFileSync(resolve(process.cwd(), 'public/app-config.example.js'), 'utf8'); const builtIndex = readFileSync(resolve(process.cwd(), 'dist/index.html'), 'utf8'); const releaseManifest = readFileSync(resolve(process.cwd(), 'dist/.release-assets'), 'utf8').split(/\r?\n/).filter(Boolean); if (excelAssets.length !== 1) { throw new Error(`production build must contain exactly one ExcelJS asset, found ${excelAssets.length}: ${excelAssets.join(', ') || 'none'}`); } if (mileageWorkers.length !== 1) { throw new Error(`production build must contain exactly one mileage export worker, found ${mileageWorkers.length}: ${mileageWorkers.join(', ') || 'none'}`); } if (monitorAssets.length !== 1) { throw new Error(`production build must contain exactly one monitor page asset, found ${monitorAssets.length}: ${monitorAssets.join(', ') || 'none'}`); } if (runtimeConfig !== safeRuntimeTemplate) { throw new Error('production dist/app-config.js must match the credential-free runtime template'); } if (!builtIndex.includes('id="platform-boot"') || !builtIndex.includes('vehicle-platform:ready') || !builtIndex.includes('window.setTimeout(fail, 10_000)')) { throw new Error('production index must retain the static boot recovery boundary'); } const mainAsset = builtIndex.match(/]+type="module"[^>]+src="\/assets\/([^"/]+\.js)"/)?.[1]; if (!mainAsset || !assets.includes(mainAsset) || !readFileSync(resolve(assetsDirectory, mainAsset), 'utf8').includes('vehicle-platform:ready')) { throw new Error('production entry must retain the React boot-ready handshake'); } if (releaseManifest.length !== assets.length || releaseManifest.some((name, index) => name !== [...assets].sort()[index])) { throw new Error('production dist/.release-assets must list every generated asset exactly once in sorted order'); } if (releaseManifest.some((name) => name.includes('/') || name === '.' || name === '..')) { throw new Error('production dist/.release-assets contains an unsafe asset name'); } const configuredSecurityCode = runtimeConfig.match(/["']?amapSecurityJsCode["']?\s*:\s*(["'])(.*?)\1/s)?.[2].trim(); if (configuredSecurityCode) { throw new Error('production dist/app-config.js must not contain an AMap security code'); } const excelBytes = statSync(resolve(assetsDirectory, excelAssets[0])).size; const workerBytes = statSync(resolve(assetsDirectory, mileageWorkers[0])).size; const monitorSource = readFileSync(resolve(assetsDirectory, monitorAssets[0]), 'utf8'); const dynamicMonitorAssets = [...monitorSource.matchAll(/import\("\.\/([^"/]+\.js)"\)/g)].map((match) => match[1]); const qrAssets = dynamicMonitorAssets.filter((name) => { if (!assets.includes(name)) return false; const source = readFileSync(resolve(assetsDirectory, name), 'utf8'); return source.includes('No input text') && source.includes('toDataURL'); }); if (qrAssets.length !== 1) { throw new Error(`monitor QR generator must be one deferred dynamic asset, found ${qrAssets.length}: ${qrAssets.join(', ') || 'none'}`); } const monitorBytes = statSync(resolve(assetsDirectory, monitorAssets[0])).size; if (monitorBytes > 35_000) { throw new Error(`monitor critical-path asset exceeds 35000 bytes: ${monitorBytes}`); } const qrBytes = statSync(resolve(assetsDirectory, qrAssets[0])).size; process.stdout.write(`web_build_gate=ok release_assets=${releaseManifest.length} exceljs_assets=1 exceljs_bytes=${excelBytes} mileage_workers=1 worker_bytes=${workerBytes} monitor_bytes=${monitorBytes} qr_deferred=1 qr_bytes=${qrBytes} runtime_config_sanitized=1 boot_recovery=1\n`);