Initial
This commit is contained in:
85
build.mjs
Normal file
85
build.mjs
Normal file
@@ -0,0 +1,85 @@
|
||||
/* ============================================================
|
||||
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(
|
||||
/<link rel="stylesheet" href="src\/style\.css">/,
|
||||
`<style>\n${css}\n</style>`
|
||||
);
|
||||
html = html.replace(
|
||||
/<script type="module" src="src\/main\.js"><\/script>/,
|
||||
`<script>\n${bundle}</script>`
|
||||
);
|
||||
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`);
|
||||
Reference in New Issue
Block a user