#!/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]*\s*\n', re.S)
TW_STYLE_RE = re.compile(r'', re.S)
TW_CDN_RE = re.compile(r'[ \t]*\s*\n', re.S)
LUCIDE_CDN_RE = re.compile(r'[ \t]*\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 \n" + js + "\n" + 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 \n" + css + "\n" + 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