#!/usr/bin/env python3 """Precompile the frontend so first boot doesn't ship compilers to the client. `index.html` stays the editable source (it still works opened standalone via the in-browser Babel + Tailwind fallbacks). This script reads it and writes `index.build.html` with: * the `', re.S) BABEL_CDN_RE = re.compile(r'[ \t]*]*@babel/standalone[^>]*>\s*\n', re.S) TW_STYLE_RE = re.compile(r'', re.S) TW_CDN_RE = re.compile(r'[ \t]*]*@tailwindcss/browser[^>]*>\s*\n', re.S) LUCIDE_CDN_RE = re.compile(r'[ \t]*]*/lucide@[^>]*>\s*\n', re.S) MK_NAME_RE = re.compile(r'mk\("([A-Za-z0-9]+)"\)') # --------------------------------------------------------------------------- # # JSX -> JS (esbuild) # --------------------------------------------------------------------------- # def _esbuild_cmd(): if shutil.which("esbuild"): return ["esbuild"] if shutil.which("npx"): return ["npx", "--yes", f"esbuild@{ESBUILD_VERSION}"] sys.exit("error: need `esbuild` or `npx` on PATH to precompile the JSX.") def transpile_jsx(jsx: str) -> str: with tempfile.TemporaryDirectory() as td: inp, outp = os.path.join(td, "app.jsx"), os.path.join(td, "app.js") with open(inp, "w", encoding="utf-8") as fh: fh.write(jsx) # Classic JSX runtime against the global React UMD (no auto-import), # matching the current `data-presets="react"` behaviour. cmd = _esbuild_cmd() + [ inp, "--jsx-factory=React.createElement", "--jsx-fragment=React.Fragment", "--target=es2020", "--charset=utf8", f"--outfile={outp}", ] res = subprocess.run(cmd, capture_output=True, text=True) if res.returncode != 0: sys.stderr.write(res.stderr) sys.exit("error: esbuild failed to transpile the JSX (see above).") with open(outp, "r", encoding="utf-8") as fh: return fh.read() # --------------------------------------------------------------------------- # # Tailwind config-in-CSS -> static stylesheet (Tailwind v4 CLI) # --------------------------------------------------------------------------- # def compile_tailwind(tw_block: str) -> str: """Compile the in-page Tailwind block to a static, minified stylesheet. `@source` points at index.html so the CLI scans the real className strings (all Tailwind classes here are literal tokens — there is no runtime class concatenation — so the static scan has full coverage).""" if not shutil.which("npm"): sys.exit("error: need `npm` on PATH to precompile Tailwind.") os.makedirs(TW_BUILD_DIR, exist_ok=True) if not os.path.isdir(os.path.join(TW_BUILD_DIR, "node_modules", "tailwindcss")): print(f"[tailwind] installing tailwindcss@{TAILWIND_VERSION} (one-time) …") res = subprocess.run( ["npm", "install", "--silent", "--no-audit", "--no-fund", f"tailwindcss@{TAILWIND_VERSION}", f"@tailwindcss/cli@{TAILWIND_VERSION}"], cwd=TW_BUILD_DIR, capture_output=True, text=True) if res.returncode != 0: sys.stderr.write(res.stderr) sys.exit("error: failed to install Tailwind (see above).") inp = os.path.join(TW_BUILD_DIR, "input.css") outp = os.path.join(TW_BUILD_DIR, "app.css") with open(inp, "w", encoding="utf-8") as fh: fh.write(f'@import "tailwindcss";\n@source "{SRC}";\n' + tw_block) res = subprocess.run( [os.path.join(TW_BUILD_DIR, "node_modules", ".bin", "tailwindcss"), "-i", inp, "-o", outp, "--minify"], capture_output=True, text=True) if res.returncode != 0: sys.stderr.write(res.stderr) sys.exit("error: Tailwind CLI failed (see above).") with open(outp, "r", encoding="utf-8") as fh: css = fh.read() if len(css) < 5000: sys.exit(f"error: compiled Tailwind CSS is suspiciously small ({len(css)} B) — aborting.") return css # --------------------------------------------------------------------------- # # lucide: ship only the referenced icons instead of the full ~600 KB library # --------------------------------------------------------------------------- # def compile_lucide(names: list[str]) -> str: """Return an inline ") # --------------------------------------------------------------------------- # def main() -> None: with open(SRC, "r", encoding="utf-8") as fh: html = fh.read() # ---- JSX block -> plain JS ---- m = BABEL_BLOCK_RE.search(html) if not m: sys.exit('error: no " + html[m.end():] html, n_babel = BABEL_CDN_RE.subn("", html) # ---- Tailwind block -> inlined static CSS ---- tw = TW_STYLE_RE.search(html) if not tw: sys.exit('error: no " + html[tw.end():] html, n_tw = TW_CDN_RE.subn("", html) # ---- lucide: full library CDN -> inline subset of referenced icons ---- names = sorted(set(MK_NAME_RE.findall(html))) if not names: sys.exit("error: no mk(\"…\") icon references found in index.html") inline_icons = compile_lucide(names) html, n_lucide = LUCIDE_CDN_RE.subn(inline_icons + "\n", html) if n_lucide != 1: sys.exit(f"error: expected exactly 1 lucide CDN