#!/usr/bin/env python3 """Flatten and minify the site's CSS imports without changing their order. The page HTML loads css/main.css. Keeping this file as a generated bundle removes the browser's serial @import chain while the individual files under css/ remain the editable source of truth. """ import argparse import posixpath import re import subprocess import sys import tempfile from pathlib import Path ROOT = Path(__file__).resolve().parent.parent OUTPUT = ROOT / "css" / "main.css" SOURCE = ROOT / "css" / "source.css" IMPORT_RE = re.compile(r"@import\s+['\"]([^'\"]+\.css)(?:\?v=[0-9a-f]{8})?['\"]\s*;", re.I) URL_RE = re.compile(r"url\(\s*(['\"]?)(\.\.?/[^)'\"\s]+)\1\s*\)", re.I) LIGHTNINGCSS = "lightningcss-cli@1.33.0" def rebase_urls(css: str, source: str) -> str: """Make relative url() references resolve from css/main.css after bundling.""" source_dir = posixpath.dirname(source) def replace(match: re.Match) -> str: quote, value = match.groups() target = posixpath.normpath(posixpath.join(source_dir, value)) from_bundle = posixpath.relpath(target, "css") return f"url({quote}{from_bundle}{quote})" return URL_RE.sub(replace, css) def build(output: Path) -> None: source_text = SOURCE.read_text(encoding="utf-8") imports = IMPORT_RE.findall(source_text) if not imports: raise RuntimeError("css/source.css has no CSS imports to bundle") chunks = [] for import_path in imports: rel = posixpath.normpath(posixpath.join("css", import_path)) path = ROOT / rel if not path.is_file(): raise FileNotFoundError(f"Missing CSS source: {rel}") chunks.append(rebase_urls(path.read_text(encoding="utf-8"), rel)) input_path = ROOT / "css" / ".main.build.css" input_path.write_text("\n".join(chunks), encoding="utf-8", newline="\n") try: subprocess.run( ["npx", "--yes", LIGHTNINGCSS, str(input_path), "--minify", "--output-file", str(output)], cwd=ROOT, check=True, ) finally: input_path.unlink(missing_ok=True) def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--check", action="store_true", help="fail if css/main.css is stale") args = parser.parse_args() if not SOURCE.is_file(): raise FileNotFoundError("Missing CSS source aggregator: css/source.css") if args.check: with tempfile.NamedTemporaryFile(prefix="msos-main-", suffix=".css", dir=ROOT / "css", delete=False) as tmp: temp_output = Path(tmp.name) try: build(temp_output) if temp_output.read_bytes() != OUTPUT.read_bytes(): print("css/main.css is stale; run tools/build_css_bundle.py", file=sys.stderr) return 1 finally: temp_output.unlink(missing_ok=True) print(f"CSS bundle is current ({OUTPUT.stat().st_size:,} bytes)") return 0 build(OUTPUT) print(f"Wrote css/main.css ({OUTPUT.stat().st_size:,} bytes)") return 0 if __name__ == "__main__": try: raise SystemExit(main()) except Exception as exc: print(f"CSS bundle failed: {exc}", file=sys.stderr) raise SystemExit(1)