brain-university-api / scripts /build_frontend.mjs
jang0294's picture
Upload folder using huggingface_hub
4a8ceaa verified
Raw
History Blame Contribute Delete
4.99 kB
// Frontend precompile — docs/HARDENING.md Phase 5 (prod deploy path).
//
// Compiles every design/*.jsx and design/screens/*.jsx (plus the inline
// <script type="text/babel"> app shell inside design/Brain University.html)
// to plain .js under design/.compiled/, preserving the window-global
// architecture (classic scripts, no modules). Emits
// design/.compiled/manifest.json mapping each compiled file to a content
// hash, which the HTML's inline loader uses both as the prod/dev switch and
// as a cache-busting ?v= token.
//
// This is ADDITIVE: the zero-build dev mode (in-browser Babel over the raw
// .jsx files) keeps working unchanged whenever design/.compiled/ is absent.
// design/.compiled/ is a deploy artifact — never commit it (see .gitignore).
//
// Usage: npm ci && node scripts/build_frontend.mjs
// Deploy: netlify.toml [build] command runs this after gen-config.mjs.
import { createHash } from "node:crypto";
import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import vm from "node:vm";
import babel from "@babel/core";
const here = dirname(fileURLToPath(import.meta.url));
const root = join(here, "..");
const designDir = join(root, "design");
const outDir = join(designDir, ".compiled");
const htmlPath = join(designDir, "Brain University.html");
// Babel options mirroring in-browser @babel/standalone's text/babel handling:
// the react preset only (classic runtime -> React.createElement globals),
// classic-script semantics, no module transform, modern JS left untouched.
const BABEL_OPTS = {
presets: [["@babel/preset-react", {}]],
sourceType: "script",
babelrc: false,
configFile: false,
compact: false,
sourceMaps: false,
};
function hashOf(code) {
return createHash("sha256").update(code).digest("hex").slice(0, 16);
}
function syntaxCheck(code, name) {
// Same check as `node --check`: throws on any syntax error.
new vm.Script(code, { filename: name });
}
async function compileOne(source, sourceName, outRelPath, manifest) {
const result = await babel.transformAsync(source, {
...BABEL_OPTS,
filename: sourceName,
});
const code = result.code + "\n";
syntaxCheck(code, outRelPath);
const outPath = join(outDir, outRelPath);
mkdirSync(dirname(outPath), { recursive: true });
writeFileSync(outPath, code);
manifest[outRelPath] = hashOf(code);
console.log(`[build_frontend] ${sourceName} -> .compiled/${outRelPath}`);
}
function jsxFiles(dir, prefix) {
return readdirSync(dir, { withFileTypes: true })
.filter((e) => e.isFile() && e.name.endsWith(".jsx"))
.map((e) => ({ abs: join(dir, e.name), rel: prefix + e.name }))
.sort((a, b) => (a.rel < b.rel ? -1 : 1));
}
// Extract <script type="text/babel"> tags from the HTML: inline bodies get
// compiled to _inline_{n}.js (document order); src= refs are collected so we
// can fail loudly if the HTML references a .jsx the glob didn't cover.
function scanHtml(html) {
const inline = [];
const externalSrcs = [];
const re = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
let m;
while ((m = re.exec(html)) !== null) {
const attrs = m[1];
if (!/type\s*=\s*["']text\/babel["']/i.test(attrs)) continue;
const src = attrs.match(/\bsrc\s*=\s*["']([^"']+)["']/i);
if (src) externalSrcs.push(src[1].split("?")[0]);
else inline.push(m[2]);
}
return { inline, externalSrcs };
}
async function main() {
rmSync(outDir, { recursive: true, force: true });
mkdirSync(outDir, { recursive: true });
const manifest = {};
const files = [
...jsxFiles(designDir, ""),
...jsxFiles(join(designDir, "screens"), "screens/"),
];
if (files.length === 0) throw new Error("no .jsx files found under design/");
for (const f of files) {
const source = readFileSync(f.abs, "utf8");
await compileOne(source, f.rel, f.rel.replace(/\.jsx$/, ".js"), manifest);
}
const html = readFileSync(htmlPath, "utf8");
const { inline, externalSrcs } = scanHtml(html);
if (inline.length === 0) {
throw new Error("no inline <script type=\"text/babel\"> app shell found in the HTML");
}
for (let i = 0; i < inline.length; i++) {
await compileOne(inline[i], `Brain University.html <inline #${i}>`, `_inline_${i}.js`, manifest);
}
// Referential integrity: every text/babel src in the HTML must have been
// compiled, or the prod page would 404 on it.
const missing = externalSrcs
.map((s) => s.replace(/\.jsx$/, ".js"))
.filter((k) => !(k in manifest));
if (missing.length > 0) {
throw new Error(`HTML references uncompiled jsx: ${missing.join(", ")}`);
}
writeFileSync(join(outDir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
console.log(`[build_frontend] wrote manifest.json (${Object.keys(manifest).length} entries)`);
}
main().catch((err) => {
console.error("[build_frontend] FAILED:", err.message);
process.exit(1);
});