test: verify bundled decoders and artifact budgets (#7)

This commit is contained in:
2026-07-29 11:20:46 +09:00
parent 0c656808e0
commit 49d6b0e687
10 changed files with 285 additions and 66 deletions
+77 -42
View File
@@ -4,8 +4,24 @@ 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;
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) => {
@@ -14,55 +30,69 @@ function listFiles(directory) {
});
}
function toRelative(path) {
return relative(distRoot, path).split(sep).join('/');
function toRelative(root, path) {
return relative(root, 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 = [];
const results = [];
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}`);
for (const spec of distSpecs) {
if (!existsSync(spec.root)) {
failures.push(`${spec.name} production dist가 없습니다. 먼저 npm run build를 실행하세요.`);
continue;
}
if (path.startsWith('draco/') || path.startsWith('basis/')) {
failures.push(`Three.js bundled decoder의 public 중복 포함: ${path}`);
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 [
/^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}`);
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(path));
hashes.set(hash, paths);
}
for (const paths of hashes.values()) {
if (paths.length > 1) failures.push(`50 kB 이상 중복 artifact: ${paths.join(', ')}`);
}
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 > maxDistBytes) {
failures.push(
`3D production dist budget 초과: ${totalBytes} bytes > ${maxDistBytes} bytes`,
);
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) {
@@ -70,6 +100,11 @@ if (failures.length > 0) {
process.exitCode = 1;
} else {
console.log(
`[artifact-check] ${relativeFiles.length} files, ${totalBytes} bytes, duplicate 0`,
results
.map(
({ name, files, totalBytes, duplicates }) =>
`[artifact-check] ${name}: ${files} files, ${totalBytes} bytes, duplicate ${duplicates}`,
)
.join('\n'),
);
}