import { createHash } from 'node:crypto'; import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; import { dirname, relative, resolve, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const distRoot = resolve(projectRoot, 'apps/viewer-3d/dist'); const maxDistBytes = 9_000_000; function listFiles(directory) { return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { const path = resolve(directory, entry.name); return entry.isDirectory() ? listFiles(path) : [path]; }); } function toRelative(path) { return relative(distRoot, path).split(sep).join('/'); } if (!existsSync(distRoot)) { throw new Error('3D production dist가 없습니다. 먼저 npm run build를 실행하세요.'); } const files = listFiles(distRoot); const relativeFiles = files.map(toRelative); const totalBytes = files.reduce((sum, path) => sum + statSync(path).size, 0); const failures = []; for (const path of relativeFiles) { if (path.endsWith('.map')) failures.push(`production source map 포함: ${path}`); if (path === 'web-ifc/web-ifc-mt.wasm') { failures.push(`미사용 multi-thread IFC WASM 포함: ${path}`); } if (path.startsWith('draco/') || path.startsWith('basis/')) { failures.push(`Three.js bundled decoder의 public 중복 포함: ${path}`); } } for (const pattern of [ /^assets\/draco_decoder-.+\.wasm$/, /^assets\/basis_transcoder-.+\.wasm$/, /^web-ifc\/web-ifc\.wasm$/, ]) { if (!relativeFiles.some((path) => pattern.test(path))) { failures.push(`필수 runtime artifact 누락: ${pattern}`); } } const hashes = new Map(); for (const path of files) { if (statSync(path).size < 50_000) continue; const hash = createHash('sha256').update(readFileSync(path)).digest('hex'); const paths = hashes.get(hash) ?? []; paths.push(toRelative(path)); hashes.set(hash, paths); } for (const paths of hashes.values()) { if (paths.length > 1) failures.push(`50 kB 이상 중복 artifact: ${paths.join(', ')}`); } if (totalBytes > maxDistBytes) { failures.push( `3D production dist budget 초과: ${totalBytes} bytes > ${maxDistBytes} bytes`, ); } if (failures.length > 0) { console.error(failures.map((failure) => `- ${failure}`).join('\n')); process.exitCode = 1; } else { console.log( `[artifact-check] ${relativeFiles.length} files, ${totalBytes} bytes, duplicate 0`, ); }