69 lines
3.3 KiB
JavaScript
69 lines
3.3 KiB
JavaScript
#!/usr/bin/env node
|
|
// Site build for the delivery-arc page (CSP-clean: script-src 'self', no inline JS).
|
|
// Reads pilot/arc.tpl.html, splits the app <script> out to arc.js, injects the scene
|
|
// manifests, points heroes at /assets/ files, and wraps a full HTML document.
|
|
// Runs on bare node (the build image has no python). Artifact build = assemble-arc.py.
|
|
import { readFileSync, writeFileSync, mkdirSync, copyFileSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const DIST = process.argv[2] || join(ROOT, "dist");
|
|
|
|
const man = t => JSON.stringify(JSON.parse(readFileSync(join(ROOT, "pilot", `manifest-${t}.json`), "utf8")));
|
|
|
|
let tpl = readFileSync(join(ROOT, "pilot/arc.tpl.html"), "utf8");
|
|
|
|
// split the app script (the last <script> block) out of the template
|
|
const i = tpl.lastIndexOf("<script>");
|
|
const j = tpl.lastIndexOf("</script>");
|
|
if (i < 0 || j < i) throw new Error("app script block not found");
|
|
let app = tpl.slice(i + 8, j)
|
|
.replace("__MAN_GITOPS__", man("gitops"))
|
|
.replace("__MAN_SUPPLY__", man("supply"))
|
|
.replace("__MAN_HELM__", man("helm"));
|
|
let body = tpl.slice(0, i) + tpl.slice(j + 9);
|
|
|
|
const heroes = {
|
|
__HERO_GITOPS__: "course-VI-gitops.webp",
|
|
__HERO_SUPPLY__: "course-VII-supply-chain.webp",
|
|
__HERO_HELM__: "course-VIII-helm-press.webp",
|
|
__HERO_APPENDIX__: "appendix-dependency-ledger.webp",
|
|
};
|
|
for (const [ph, file] of Object.entries(heroes)) body = body.replace(ph, "/assets/" + file);
|
|
if (body.includes("__HERO_") || app.includes("__MAN_")) throw new Error("unfilled placeholder");
|
|
|
|
const favicon = "data:image/svg+xml," + encodeURIComponent(
|
|
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" fill="#070b14"/><rect x="6" y="4" width="14" height="7" rx="1.5" fill="#3fbaf5"/><rect x="11" y="13" width="14" height="7" rx="1.5" fill="#7dd0f9"/><rect x="6" y="22" width="14" height="7" rx="1.5" fill="#e879f9"/></svg>`);
|
|
|
|
const html = `<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>The Exploded Cluster</title>
|
|
<meta name="description" content="Interactive teardown of the toolchain around Kubernetes and OpenShift: GitOps, the image supply chain, and Helm - every scene one generated hero, sliced and scrubbed.">
|
|
<meta name="theme-color" content="#070b14">
|
|
<link rel="icon" href="${favicon}">
|
|
</head>
|
|
<body>
|
|
${body}
|
|
<script src="/gsap.min.js"></script>
|
|
<script src="/st.min.js"></script>
|
|
<script src="/arc.js"></script>
|
|
</body>
|
|
</html>
|
|
`;
|
|
|
|
mkdirSync(join(DIST, "assets"), { recursive: true });
|
|
writeFileSync(join(DIST, "index.html"), html);
|
|
writeFileSync(join(DIST, "arc.js"), app);
|
|
copyFileSync(join(ROOT, "gsap.min.js"), join(DIST, "gsap.min.js"));
|
|
copyFileSync(join(ROOT, "st.min.js"), join(DIST, "st.min.js"));
|
|
for (const f of Object.values(heroes)) copyFileSync(join(ROOT, "assets/dist", f), join(DIST, "assets", f));
|
|
|
|
// CSP gate: no inline <script> may survive into the served HTML
|
|
const inline = (html.match(/<script(?![^>]*src=)[^>]*>/gi) || []).length;
|
|
if (inline) throw new Error("inline <script> in index.html - CSP would block it");
|
|
console.log(`site build -> ${DIST}: index.html ${(html.length / 1024).toFixed(0)}KB + arc.js + 2 bundles + ${Object.keys(heroes).length} heroes`);
|