Files

49 lines
1.9 KiB
JavaScript

import { readFile, stat } from "node:fs/promises";
import path from "node:path";
import process from "node:process";
const root = path.resolve(process.argv[2] || "web/admin/dist");
function resolveOutput(name) {
const normalized = String(name || "").replaceAll("\\", "/");
if (!normalized || normalized.startsWith("/") || normalized.split("/").includes("..")) {
throw new Error(`Invalid asset path in manifest: ${JSON.stringify(name)}`);
}
const resolved = path.resolve(root, ...normalized.split("/"));
if (resolved !== root && !resolved.startsWith(`${root}${path.sep}`)) {
throw new Error(`Asset path escapes dist: ${JSON.stringify(name)}`);
}
return resolved;
}
async function requireFile(name) {
const file = resolveOutput(name);
const info = await stat(file);
if (!info.isFile() || info.size === 0) {
throw new Error(`Missing or empty admin asset: ${name}`);
}
}
const manifest = JSON.parse(await readFile(path.join(root, "asset-manifest.json"), "utf8"));
const build = JSON.parse(await readFile(path.join(root, "admin-build.json"), "utf8"));
if (!build.buildId || typeof build.buildId !== "string") {
throw new Error("admin-build.json does not contain a buildId");
}
if (!manifest || typeof manifest !== "object" || Object.keys(manifest).length === 0) {
throw new Error("asset-manifest.json is empty");
}
await requireFile("index.html");
for (const [key, entry] of Object.entries(manifest)) {
if (!entry?.file) throw new Error(`Manifest entry ${key} has no output file`);
for (const dependency of [...(entry.imports || []), ...(entry.dynamicImports || [])]) {
if (!manifest[dependency]) throw new Error(`Manifest entry ${key} references missing entry ${dependency}`);
}
for (const file of [entry.file, ...(entry.css || []), ...(entry.assets || [])]) {
await requireFile(file);
}
}
process.stdout.write(`Admin build ${build.buildId}: ${Object.keys(manifest).length} manifest entries verified.\n`);