Files
Find-Word/scripts/check-build.mjs
T
2026-10-14 06:19:24 +00:00

54 lines
2.3 KiB
JavaScript

import { readFileSync } from "node:fs";
import { createHash } from "node:crypto";
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
// 빌드가 성공해도 public 파일이 빠지면 게임은 실행 중 실패한다. 따라서 배포
// 폴더 자체의 파일 해시를 원본 manifest와 비교하는 계약을 빌드 마지막에 둔다.
export function verifyBuild(directory) {
// 번들에 실제 포함되는 전이 의존성도 잠금 파일에서 찾는다. 프로젝트의
// 비공개 라이선스와 별개로 설치된 MIT 전문·버전을 사본 세 곳에서 보존한다.
try {
const notice = readFileSync("THIRD_PARTY_NOTICES.md", "utf8");
const lock = JSON.parse(readFileSync("package-lock.json", "utf8"));
for (const [path, pkg] of Object.entries(lock.packages)) {
if (!path || pkg.dev) continue;
const name = path.split("node_modules/").at(-1);
const license = readFileSync(`${path}/LICENSE`, "utf8").trim();
if (!notice.includes(`## ${name} ${pkg.version}\n\n${license}`))
throw new Error(`${name} ${pkg.version}의 전문이 없습니다.`);
}
for (const path of [
"public/THIRD_PARTY_NOTICES.md",
resolve(directory, "THIRD_PARTY_NOTICES.md"),
]) {
if (readFileSync(path, "utf8") !== notice)
throw new Error(`${path}가 원본과 다릅니다.`);
}
} catch (cause) {
throw new Error("런타임 의존성 notice 누락 또는 불일치", { cause });
}
const manifest = JSON.parse(
readFileSync("public/audio/manifest.json", "utf8"),
);
const builtManifest = JSON.parse(
readFileSync(resolve(directory, "audio/manifest.json"), "utf8"),
);
if (JSON.stringify(manifest) !== JSON.stringify(builtManifest))
throw new Error("빌드 음성 목록이 원본과 다릅니다.");
for (const entry of manifest.files) {
const bytes = readFileSync(resolve(directory, "audio", entry.file));
if (createHash("sha256").update(bytes).digest("hex") !== entry.sha256)
throw new Error(`빌드 자산 해시 불일치: ${entry.file}`);
}
return manifest.files.length;
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(resolve(process.argv[1])).href
) {
console.log(
`빌드 음성 자산 ${verifyBuild("dist")}개 및 런타임 notice 전문 확인`,
);
}