#!/usr/bin/env node
/**
* bundle.js — Flatten an HTML app and all its local deps into a single
* self-contained .html artifact, ready to open offline.
*
* What it does:
* - Inlines every local block so the browser doesn't close it early.
function escForScript(src) { return src.replace(/<\/script/gi, '<\\/script'); }
function escForStyle(src) { return src.replace(/<\/style/gi, '<\\/style'); }
// Pre-compile JSX → JS. Falls back to raw source if babel isn't installed.
function compileJSX(src, filename) {
if (!babel || !babelPresetReact) return { code: src, compiled: false };
try {
const result = babel.transformSync(src, {
presets: [babelPresetReact],
filename,
sourceMaps: false,
compact: false,
});
return { code: result.code, compiled: true };
} catch (e) {
console.warn(` ⚠ JSX compile error in ${filename}: ${e.message}`);
return { code: src, compiled: false };
}
}
// ---- Inline local stylesheets -----------------------------------------------
async function inlineStylesheets(html) {
const comments = commentSpans(html);
const re = /]*?)\brel\s*=\s*["']stylesheet["']([^>]*?)>/gi;
const matches = [...html.matchAll(re)];
let out = '', cursor = 0;
for (const m of matches) {
if (insideComment(m.index, comments)) {
out += html.slice(cursor, m.index + m[0].length);
cursor = m.index + m[0].length;
continue;
}
out += html.slice(cursor, m.index);
const hrefM = (m[1] + ' ' + m[2]).match(/\bhref\s*=\s*["']([^"']+)["']/i);
const href = hrefM?.[1];
const body = href && !isRemote(href) ? readLocal(href) : null;
if (body) { console.log(` ✓ stylesheet ${href}`); out += ``; }
else out += m[0];
cursor = m.index + m[0].length;
}
return out + html.slice(cursor);
}
// ---- Inline / swap / remove `; }
else out += full;
}
} else {
// Local file.
let body = readLocal(src);
if (body == null) { out += full; }
else {
let label = src;
if (isBabel) {
const r = compileJSX(body, src);
body = r.code;
label = r.compiled ? `${src} (JSX→JS)` : src;
}
console.log(` ✓ script ${label}`);
// Strip type="text/babel", integrity, crossorigin — content has changed.
const attrs = allAttrs
.replace(/\btype\s*=\s*["']text\/babel["']/gi, '')
.replace(/\bintegrity\s*=\s*["'][^"']*["']/gi, '')
.replace(/\bcrossorigin\s*=\s*["'][^"']*["']/gi, '')
.replace(/\s+/g, ' ').trim();
out += ``;
}
}
cursor = m.index + full.length;
}
return out + html.slice(cursor);
}
// ---- Compile remaining inline blocks ---
function compileInlineBabel(html) {
const comments = commentSpans(html);
const re = /(