Files
gstack/lib/diagram-render/scripts/build.ts
T
Garry Tanandcapy-ai[bot] a1487458d3 v1.91.241.0 fix(deps): bump dompurify, MCP SDK, zod, body-parser, fast-uri, ip-address to OSV-fixed releases (#3462) (#3463)
* fix(deps): bump dompurify, @modelcontextprotocol/sdk, zod, body-parser, fast-uri and ip-address to their OSV-fixed releases (#3462)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* build(diagram-render): empty excalidraw's public Firebase config literal in the bundle; declare BUILD_INFO.json's test lane

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test(diagram-render): name the bundle, BUILD_INFO and build script by repo path so the pre-gate sees their lane

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore(pregate): drop the BUILD_INFO.json lane declaration; the drift test names the file now

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes

Testing specialist: the drift test asserts the bundled DOMPurify is the override and the bundle carries no Google API key; deps-smoke refuses ip-address below 10.7.4 and checks every bumped package against its OSV-fixed floor in both lockfiles. Adversarial: build.ts fails when any AIza key survives the strip. Outside voice and red team: the CHANGELOG hash and scope lines match the shipped tree.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(diagram-render): document the Firebase-literal strip and the new drift checks

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-26 10:44:36 +00:00

118 lines
4.3 KiB
TypeScript

/**
* Build dist/diagram-render.html — the single-file offline render page.
*
* One command updates everything: `bun run build` (in this directory) or
* `bun run build:diagram-render` (repo root). To bump a dependency: edit the
* exact pin in package.json, `bun install`, rebuild, commit src + dist +
* BUILD_INFO.json together. The drift test (test/diagram-render-drift.test.ts)
* fails CI when dist and BUILD_INFO disagree.
*
* Page assembly notes (learned in the spike, do not "simplify" away):
* - The script MUST be `type="module"` — mermaid's bundle contains
* import.meta, which throws in a classic script.
* - `</scri` sequences inside the minified JS MUST be escaped to `<\/scri`,
* or the inline <script> terminates early ("Unexpected end of input").
* - A <base href> with an absolute URL is required: the page lives at
* about:blank (page.setContent), where relative URL construction throws.
*/
import { createHash } from "node:crypto";
import path from "node:path";
const ROOT = path.resolve(import.meta.dir, "..");
const ENTRY = path.join(ROOT, "src", "entry.ts");
const DIST_DIR = path.join(ROOT, "dist");
const DIST_HTML = path.join(DIST_DIR, "diagram-render.html");
const BUILD_INFO = path.join(DIST_DIR, "BUILD_INFO.json");
const pkg = await Bun.file(path.join(ROOT, "package.json")).json();
const deps: Record<string, string> = pkg.dependencies;
const result = await Bun.build({
entrypoints: [ENTRY],
target: "browser",
minify: true,
define: {
__BUNDLE_INFO_DEPS__: JSON.stringify(deps),
"process.env.NODE_ENV": '"production"',
},
});
if (!result.success) {
for (const log of result.logs) console.error(log);
process.exit(1);
}
const js = await result.outputs[0].text();
// @excalidraw/excalidraw's npm bundle carries excalidraw.com's public Firebase
// client config (an `AIza…` key) in an env literal nothing in the package
// reads. The offline page never opens a collab session, and a key-shaped
// literal in a committed file fails the pre-gate secret scan on every rebuild,
// so the literal is emptied. Exactly one match is expected: a bump that changes
// the shape must fail here rather than put the key back silently.
const FIREBASE_CONFIG = /VITE_APP_FIREBASE_CONFIG:'\{[^']*\}'/g;
const firebaseHits = js.match(FIREBASE_CONFIG)?.length ?? 0;
if (firebaseHits !== 1) {
console.error(`expected exactly one VITE_APP_FIREBASE_CONFIG literal in the excalidraw bundle, found ${firebaseHits}; update the strip rule in scripts/build.ts`);
process.exit(1);
}
const strippedJs = js.replace(FIREBASE_CONFIG, "VITE_APP_FIREBASE_CONFIG:'{}'");
if (/AIza[0-9A-Za-z_-]{35}/.test(strippedJs)) {
console.error("a Google API key literal survived the strip; update the strip rule in scripts/build.ts");
process.exit(1);
}
// Escape inline-script terminators (see header note).
const inlineJs = strippedJs.replaceAll("</scri", "<\\/scri");
const head = `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<base href="https://gstack-render.localhost/">
<title>gstack diagram-render</title>
<style>
body { font-family: Helvetica, "Liberation Sans", Arial, sans-serif; margin: 0; }
</style>
<script>
window.__errors = [];
window.onerror = function (msg, src, line, col, err) {
window.__errors.push(String(msg) + " @" + line + ":" + col);
};
window.addEventListener("unhandledrejection", function (e) {
window.__errors.push("unhandledrejection: " + String(e.reason).slice(0, 500));
});
</script>
</head>
<body>
<div id="status">loading</div>
<script type="module">
`;
const tail = `
</script>
</body>
</html>
`;
const html = head + inlineJs + tail;
await Bun.write(DIST_HTML, html);
const sha256 = createHash("sha256").update(html).digest("hex");
// Source fingerprint: lets the drift test catch "edited src, forgot to
// rebuild dist" WITHOUT needing node_modules for a full rebuild (the deep
// rebuild check only runs where deps are installed).
const srcSha256 = createHash("sha256")
.update(await Bun.file(ENTRY).text())
.update(await Bun.file(import.meta.path).text())
.digest("hex");
const info = {
name: "gstack-diagram-render",
sha256,
srcSha256,
bytes: Buffer.byteLength(html),
bunVersion: Bun.version,
deps,
};
await Bun.write(BUILD_INFO, JSON.stringify(info, null, 2) + "\n");
console.log(`built ${path.relative(process.cwd(), DIST_HTML)} (${(info.bytes / 1024 / 1024).toFixed(2)} MB)`);
console.log(`sha256 ${sha256}`);