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 vehicleAssets = assets.filter((name) => /^VehiclePage-[\w-]+\.js$/.test(name)); const vehicleSearchAssets = assets.filter((name) => /^VehicleSearchWorkspace-[\w-]+\.js$/.test(name)); const profileSyncAssets = assets.filter((name) => /^VehicleProfileSyncPanel-[\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 builtSemiPrototype = readFileSync(resolve(process.cwd(), 'dist/semi-prototype.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 (vehicleAssets.length !== 1 || vehicleSearchAssets.length !== 1 || profileSyncAssets.length !== 1) { throw new Error(`vehicle workspaces must remain deferred route assets: vehicle=${vehicleAssets.join(', ') || 'none'} search=${vehicleSearchAssets.join(', ') || 'none'} sync=${profileSyncAssets.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'); } const semiPrototypeAsset = builtSemiPrototype.match(/]+type="module"[^>]+src="\/assets\/([^"/]+\.js)"/)?.[1]; if (!builtSemiPrototype.includes('Semi UI 原型 · 羚牛车辆数据中台') || !semiPrototypeAsset || !assets.includes(semiPrototypeAsset)) { throw new Error('production build must contain the standalone Semi UI prototype entry and asset'); } 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 vehicleBytes = statSync(resolve(assetsDirectory, vehicleAssets[0])).size; if (vehicleBytes > 30_000) { throw new Error(`vehicle detail critical-path asset exceeds 30000 bytes: ${vehicleBytes}`); } const profileSyncBytes = statSync(resolve(assetsDirectory, profileSyncAssets[0])).size; const vehicleSource = readFileSync(resolve(assetsDirectory, vehicleAssets[0]), 'utf8'); const vehicleSearchSource = readFileSync(resolve(assetsDirectory, vehicleSearchAssets[0]), 'utf8'); if (!vehicleSource.includes(vehicleSearchAssets[0]) || !vehicleSearchSource.includes(profileSyncAssets[0])) { throw new Error('vehicle directory and profile sync assets must remain a two-level deferred import from the vehicle page'); } 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} vehicle_bytes=${vehicleBytes} profile_sync_deferred=1 profile_sync_bytes=${profileSyncBytes} qr_deferred=1 qr_bytes=${qrBytes} runtime_config_sanitized=1 boot_recovery=1\n`);