import assert from "node:assert/strict"; import crypto from "node:crypto"; import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const dist = path.join(root, "web/dist"); const notices = JSON.parse(fs.readFileSync(path.join(root, "docs/web/third-party-notices.json"), "utf8")); const packageLock = JSON.parse(fs.readFileSync(path.join(root, "web/package-lock.json"), "utf8")); const sbom = JSON.parse(fs.readFileSync(path.join(root, "docs/web/sbom.spdx.json"), "utf8")); for (const dependency of ["react", "react-dom", "three", "vite", "typescript", "@playwright/test"]) { const normalize = (value) => value.toLowerCase().replace("@playwright/test", "playwright").replace(/\.js$/, "").replace(/[^a-z0-9]/g, ""); const covered = notices.packages.some((entry) => normalize(entry.name) === normalize(dependency)); assert.ok(covered, `third-party notices do not cover ${dependency}`); } assert.ok(packageLock.lockfileVersion >= 3, "npm lockfile must use an integrity-bearing format"); assert.ok(fs.existsSync(path.join(root, "blender-5.2.0/COPYING")), "Blender GPL text is missing"); assert.ok(fs.existsSync(path.join(root, "web/app/src/vendor/three/LICENSE")), "Three.js license is missing"); assert.equal(sbom.spdxVersion, "SPDX-2.3", "SPDX SBOM schema is missing"); assert.ok(sbom.packages.length >= Object.keys(packageLock.packages).length, "SPDX SBOM omits locked dependencies"); assert.equal(new Set(sbom.packages.map((item) => item.SPDXID)).size, sbom.packages.length, "SPDX IDs must be unique"); assert.ok(fs.existsSync(path.join(dist, "index.html")), "offline dist is missing; run npm build first"); const files = []; function walk(directory) { for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { const absolute = path.join(directory, entry.name); if (entry.isDirectory()) walk(absolute); else files.push(absolute); } } walk(dist); assert.ok(files.some((file) => file.endsWith("web_engine.wasm")), "offline package omits WebEngine WASM"); for (const file of files.filter((candidate) => /\.(html|js|css|json)$/.test(candidate))) { const text = fs.readFileSync(file, "utf8"); assert.doesNotMatch(text, /(?:src|href|from)\s*[=:]\s*["']https?:\/\//i, `remote runtime dependency in ${path.relative(dist, file)}`); } const manifest = files.map((file) => ({ path: path.relative(dist, file).replaceAll(path.sep, "/"), bytes: fs.statSync(file).size, sha256: crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"), })).sort((left, right) => left.path.localeCompare(right.path)); assert.ok(manifest.every((entry) => entry.bytes > 0 && /^[a-f0-9]{64}$/.test(entry.sha256))); process.stdout.write(`release-package-ok files=${manifest.length} bytes=${manifest.reduce((sum, entry) => sum + entry.bytes, 0)}\n`);