/* ============================================================
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`);