import { readdir, readFile, stat } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import contentPolicy from "../../shared/content-policy.json" with { type: "json" }; const SCRIPT_PATH = fileURLToPath(import.meta.url); const PROHIBITED_MODULE_PATTERNS = [ /(?:^|\/)src\/ArenaRuntime\.tsx$/, /(?:^|\/)src\/app\/ShelvedModeRoutes\.tsx$/, /(?:^|\/)src\/app\/ShelvedGameRuntime\.enabled\.tsx$/, /(?:^|\/)src\/app\/ShelvedOnlineGroupRuntime\.enabled\.ts$/, /(?:^|\/)src\/app\/(?:arenaClient|gmManastormRuntime|manastormShellRuntime)\.[jt]sx?$/, /(?:^|\/)src\/game\/(?:arena|manastormAdmin|manastormCatalog|manastormInstalled)(?:\/|\.[jt]sx?$)/, /(?:^|\/)src\/game\/ManastormBridge\.[jt]sx?$/, /(?:^|\/)src\/ui\/(?:Arena|GmAdmin|GmReview|GmWorld|Manastorm)[^/]*\.[jt]sx?$/, /(?:^|\/)src\/scene\/ShelvedGameScene\.enabled\.tsx$/, /(?:^|\/)src\/game\/ShelvedDungeonSession\.enabled\.ts$/, /(?:^|\/)src\/ui\/(?:ShelvedCompanionPanel|ShelvedHud)\.enabled\.tsx$/, ]; function fail(message) { throw new Error(`Release bundle verification failed: ${message}`); } async function walkRelativeFiles(root) { const files = []; async function visit(directory) { for (const entry of await readdir(directory, { withFileTypes: true })) { const filePath = path.join(directory, entry.name); if (entry.isDirectory()) await visit(filePath); else if (entry.isFile()) files.push(path.relative(root, filePath).split(path.sep).join("/")); } } await visit(root); return files; } export async function verifyReleaseBundle(root) { const manifest = JSON.parse(await readFile(path.join(root, "content", "bootstrap-manifest.json"), "utf8")); const moduleReport = JSON.parse(await readFile(path.join(root, "bundle-module-report.json"), "utf8")); if (moduleReport.shelvedModesEnabled !== false) fail("normal releases must compile with shelved modes disabled"); const expectedPacks = [...contentPolicy.bootstrapPackIds].sort(); const actualPacks = (manifest.packs ?? []).map((pack) => pack.id).sort(); if (JSON.stringify(actualPacks) !== JSON.stringify(expectedPacks)) { fail(`bootstrap packs were ${actualPacks.join(", ")}; expected ${expectedPacks.join(", ")}`); } const hiddenPack = actualPacks.find((packId) => ( !contentPolicy.bootstrapPackIds.includes(packId) && (contentPolicy.hiddenContentPackIds.includes(packId) || contentPolicy.hiddenContentPackPrefixes.some((prefix) => packId.startsWith(prefix))) )); if (hiddenPack) fail(`bootstrap contains hidden pack ${hiddenPack}`); const objects = new Map(); for (const entry of Object.values(manifest.files ?? {})) { const object = entry.download ?? entry; if (!objects.has(object.url)) objects.set(object.url, object.bytes); } const storedBytes = [...objects.values()].reduce((sum, bytes) => sum + Number(bytes ?? 0), 0); if (storedBytes > contentPolicy.maxBootstrapStoredBytes) { fail(`bootstrap uses ${storedBytes} stored bytes; limit is ${contentPolicy.maxBootstrapStoredBytes}`); } const prohibitedModules = moduleReport.chunks.flatMap((chunk) => chunk.modules).filter((moduleId) => ( PROHIBITED_MODULE_PATTERNS.some((pattern) => pattern.test(moduleId)) )); if (prohibitedModules.length) fail(`shelved client modules were emitted: ${[...new Set(prohibitedModules)].join(", ")}`); const files = await walkRelativeFiles(root); const arenaAsset = files.find((file) => /(?:^|\/)assets\/game\/arenas(?:\/|$)/i.test(file)); if (arenaAsset) fail(`arena asset was emitted at ${arenaAsset}`); for (const objectUrl of objects.keys()) { const objectPath = path.join(root, objectUrl.replace(/^\/+/, "")); const objectStats = await stat(objectPath); if (!objectStats.isFile()) fail(`bootstrap object is missing: ${objectUrl}`); } return { root, packs: actualPacks, storedBytes, storedMiB: Number((storedBytes / 1024 / 1024).toFixed(2)), objectCount: objects.size, chunkCount: moduleReport.chunks.length, }; } if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(SCRIPT_PATH)) { const root = path.resolve(process.argv[2] ?? "dist"); console.log(JSON.stringify(await verifyReleaseBundle(root), null, 2)); }