111 lines
3.3 KiB
JavaScript
111 lines
3.3 KiB
JavaScript
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 distSpecs = [
|
|
{
|
|
name: '2D',
|
|
root: resolve(projectRoot, 'apps/viewer-2d-sample/dist'),
|
|
maxBytes: 4_500_000,
|
|
required: [/^assets\/acadrust_dwg_bg-.+\.wasm$/],
|
|
},
|
|
{
|
|
name: '3D',
|
|
root: resolve(projectRoot, 'apps/viewer-3d/dist'),
|
|
maxBytes: 9_000_000,
|
|
required: [
|
|
/^assets\/draco_decoder-.+\.wasm$/,
|
|
/^assets\/basis_transcoder-.+\.wasm$/,
|
|
/^web-ifc\/web-ifc\.wasm$/,
|
|
],
|
|
},
|
|
];
|
|
|
|
function listFiles(directory) {
|
|
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
const path = resolve(directory, entry.name);
|
|
return entry.isDirectory() ? listFiles(path) : [path];
|
|
});
|
|
}
|
|
|
|
function toRelative(root, path) {
|
|
return relative(root, path).split(sep).join('/');
|
|
}
|
|
|
|
const failures = [];
|
|
const results = [];
|
|
|
|
for (const spec of distSpecs) {
|
|
if (!existsSync(spec.root)) {
|
|
failures.push(`${spec.name} production dist가 없습니다. 먼저 npm run build를 실행하세요.`);
|
|
continue;
|
|
}
|
|
|
|
const files = listFiles(spec.root);
|
|
const relativeFiles = files.map((path) => toRelative(spec.root, path));
|
|
const totalBytes = files.reduce((sum, path) => sum + statSync(path).size, 0);
|
|
|
|
for (const path of relativeFiles) {
|
|
if (path.endsWith('.map')) {
|
|
failures.push(`${spec.name} production source map 포함: ${path}`);
|
|
}
|
|
if (spec.name === '3D' && path === 'web-ifc/web-ifc-mt.wasm') {
|
|
failures.push(`미사용 multi-thread IFC WASM 포함: ${path}`);
|
|
}
|
|
if (
|
|
spec.name === '3D' &&
|
|
(path.startsWith('draco/') || path.startsWith('basis/'))
|
|
) {
|
|
failures.push(`Three.js bundled decoder의 public 중복 포함: ${path}`);
|
|
}
|
|
}
|
|
|
|
for (const pattern of spec.required) {
|
|
if (!relativeFiles.some((path) => pattern.test(path))) {
|
|
failures.push(`${spec.name} 필수 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(spec.root, path));
|
|
hashes.set(hash, paths);
|
|
}
|
|
const duplicates = [...hashes.values()].filter((paths) => paths.length > 1);
|
|
for (const paths of duplicates) {
|
|
failures.push(`${spec.name} 50 kB 이상 중복 artifact: ${paths.join(', ')}`);
|
|
}
|
|
|
|
if (totalBytes > spec.maxBytes) {
|
|
failures.push(
|
|
`${spec.name} production dist budget 초과: ${totalBytes} bytes > ${spec.maxBytes} bytes`,
|
|
);
|
|
}
|
|
|
|
results.push({
|
|
name: spec.name,
|
|
files: relativeFiles.length,
|
|
totalBytes,
|
|
duplicates: duplicates.length,
|
|
});
|
|
}
|
|
|
|
if (failures.length > 0) {
|
|
console.error(failures.map((failure) => `- ${failure}`).join('\n'));
|
|
process.exitCode = 1;
|
|
} else {
|
|
console.log(
|
|
results
|
|
.map(
|
|
({ name, files, totalBytes, duplicates }) =>
|
|
`[artifact-check] ${name}: ${files} files, ${totalBytes} bytes, duplicate ${duplicates}`,
|
|
)
|
|
.join('\n'),
|
|
);
|
|
}
|