/* ============================================================ build.mjs — zero-dependency bundler + inliner. Walks the ES-module graph from src/main.js, wraps each module in a tiny require() shim (constrained to the named-import / named-export subset this project uses), inlines the CSS, and writes a single self-contained, file://-openable bubble_chamber.html. Run: node build.mjs ============================================================ */ import { readFileSync, writeFileSync } from 'node:fs'; import path from 'node:path'; const ROOT = path.dirname(new URL(import.meta.url).pathname); const ENTRY = 'src/main.js'; const IMPORT_RE = /^\s*import\s*\{([^}]*)\}\s*from\s*['"]([^'"]+)['"]\s*;?\s*$/; const EXPORT_RE = /^\s*export\s+(function|const|let|var|class)\s+([A-Za-z0-9_$]+)/; const modules = new Map(); // id -> { code } const order = []; function resolve(fromId, spec) { const dir = path.posix.dirname(fromId); let id = path.posix.normalize(path.posix.join(dir, spec)); if (!id.endsWith('.js')) id += '.js'; return id; } function load(id) { if (modules.has(id)) return; const src = readFileSync(path.join(ROOT, id), 'utf8'); const exports = []; const deps = []; const lines = src.split('\n').map((line) => { const imp = line.match(IMPORT_RE); if (imp) { const names = imp[1].split(',').map(s => s.trim()).filter(Boolean); const depId = resolve(id, imp[2]); deps.push(depId); return `const { ${names.join(', ')} } = __require(${JSON.stringify(depId)});`; } const exp = line.match(EXPORT_RE); if (exp) { exports.push(exp[2]); return line.replace(/^\s*export\s+/, ''); } return line; }); let code = lines.join('\n'); if (exports.length) code += `\nObject.assign(exports, { ${exports.join(', ')} });\n`; modules.set(id, { code }); for (const d of deps) load(d); // depth-first; registry handles cycles/order order.push(id); } load(ENTRY); let bundle = `(function () {\n`; bundle += ` const __cache = {};\n const __reg = {};\n`; bundle += ` function __require(id) {\n if (__cache[id]) return __cache[id].exports;\n` + ` const module = { exports: {} }; __cache[id] = module;\n` + ` __reg[id](module, module.exports, __require);\n return module.exports;\n }\n`; for (const id of [...modules.keys()]) { bundle += ` __reg[${JSON.stringify(id)}] = function (module, exports, __require) {\n`; bundle += modules.get(id).code + '\n'; bundle += ` };\n`; } bundle += ` __require(${JSON.stringify(ENTRY)});\n})();\n`; // inline into index.html let html = readFileSync(path.join(ROOT, 'index.html'), 'utf8'); const css = readFileSync(path.join(ROOT, 'src/style.css'), 'utf8'); html = html.replace( //, `` ); html = html.replace( /` ); html = html.replace('Parametric Generator · v3', 'Parametric Generator · v3 (built)'); writeFileSync(path.join(ROOT, 'bubble_chamber.html'), html); console.log(`built bubble_chamber.html — ${modules.size} modules, ${(html.length / 1024).toFixed(1)} KB`);