#!/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 += `\n${escForScript(body)}\n`; } } cursor = m.index + full.length; } return out + html.slice(cursor); } // ---- Compile remaining inline blocks --- function compileInlineBabel(html) { const comments = commentSpans(html); const re = /(]*\btype\s*=\s*["']text\/babel["'][^>]*>)([\s\S]*?)(<\/script>)/gi; return html.replace(re, (full, open, body, close, offset) => { if (insideComment(offset, comments)) return full; const { code, compiled } = compileJSX(body, 'inline.jsx'); if (!compiled) return full; const attrs = open .replace(/^$/, '') .replace(/\btype\s*=\s*["']text\/babel["']/gi, '') .replace(/\s+/g, ' ').trim(); console.log(' ✓ inline babel block compiled'); return `\n${escForScript(code)}\n${close}`; }); } // ---- Main ------------------------------------------------------------------- (async () => { if (babel && babelPresetReact) console.log('JSX: pre-compile enabled (@babel/core)'); else console.log('JSX: no @babel/core — run npm install to enable'); console.log(`\nReading ${inputPath}…`); let html = fs.readFileSync(inputPath, 'utf8'); console.log('\nStylesheets:'); html = await inlineStylesheets(html); console.log('\nScripts:'); html = await inlineScripts(html); if (babel) { console.log('\nInline Babel blocks:'); html = compileInlineBabel(html); } fs.writeFileSync(outputPath, html); const kb = (fs.statSync(outputPath).size / 1024).toFixed(0); console.log(`\n✔ wrote ${outputPath} (${kb} KB)`); })();