46 lines
2.8 KiB
JavaScript
46 lines
2.8 KiB
JavaScript
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`);
|