#!/usr/bin/env python3 """ extract-js.py — Extract inline `, and writes a manifest for cache-busting. Behaviour --------- - Skips `` immediately before the external script tag, and rewrites the JS body to read from `window.NEURON_CFG.` so the external file is fully static and runtime values are still injected at render time. - Pipeline per file: terser (compress + mangle, reserved globals preserved) → javascript-obfuscator (string-array, base64, hex names). Idempotency ----------- - Running twice is a no-op: blocks already rewritten to ` # i.e. quotes are written as \". We unescape on the way out, re-escape on # the way in. # We match a *plain* opening . # Cases we deliberately don't match: # - (external loader) # - (external loader, even with body) # - (structured data) SCRIPT_BLOCK_RE = re.compile( r"", re.DOTALL, ) # An interpolation point inside a JS body: `'" + ident + "'` (single-quoted # string in JS containing an El concat). We capture the bare identifier. INTERP_RE = re.compile(r"""'"\s*\+\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\+\s*"'""") def is_skip_block(body: str) -> bool: """True if the block is too small or non-JS to be worth extracting.""" stripped = body.strip() if len(stripped) < MIN_INLINE_BYTES: return True return False def el_unescape(s: str) -> str: r"""Mirror the El lexer's string-escape rules (foundation/el/bootstrap.py): \n -> LF, \t -> TAB, \r -> CR, \" -> ", \\ -> \, \ -> X for any X. The catch-all means \' inside an El string yields a bare apostrophe; if we don't replicate that here, an extracted block like `onclick=\"window.location.href=\\\'/contact\\\'\"` parses with stray backslashes that terser then rejects as bad escape sequences.""" out = [] i = 0 n = len(s) while i < n: c = s[i] if c == "\\" and i + 1 < n: nxt = s[i + 1] if nxt == "n": out.append("\n") elif nxt == "t": out.append("\t") elif nxt == "r": out.append("\r") elif nxt == '"': out.append('"') elif nxt == "\\": out.append("\\") else: # Catch-all: unrecognised escape collapses to the second char, # exactly as the El lexer does. out.append(nxt) i += 2 continue out.append(c) i += 1 return "".join(out) def el_escape_attr(s: str) -> str: """Escape a string for use inside an El "..." literal. We only need to escape the double quote — backslash is already legal in URLs and we don't emit any.""" return s.replace("\\", "\\\\").replace('"', '\\"') def sha12(content: str) -> str: return hashlib.sha1(content.encode("utf-8")).hexdigest()[:12] def run(cmd: List[str], **kwargs) -> subprocess.CompletedProcess: proc = subprocess.run(cmd, check=False, capture_output=True, text=True, **kwargs) if proc.returncode != 0: sys.stderr.write( f"\n[extract-js] command failed: {' '.join(cmd[:2])} ...\n" f" exit={proc.returncode}\n" f" stdout: {proc.stdout[:500]}\n" f" stderr: {proc.stderr[:2000]}\n" ) raise subprocess.CalledProcessError( proc.returncode, cmd, proc.stdout, proc.stderr ) return proc def minify_and_obfuscate(js: str, hash_id: str) -> str: """Run js through terser then javascript-obfuscator. Returns the final obfuscated source.""" raw_path = ASSET_DIR / f".{hash_id}.raw.js" min_path = ASSET_DIR / f".{hash_id}.min.js" out_path = ASSET_DIR / f"{hash_id}.js" def _cleanup_scratch() -> None: raw_path.unlink(missing_ok=True) min_path.unlink(missing_ok=True) raw_path.write_text(js, encoding="utf-8") reserved_arg = ",".join(RESERVED_GLOBALS) # terser terser_cmd = TERSER.split() + [ str(raw_path), "--compress", "passes=2,drop_console=true,drop_debugger=true", "--mangle", f"reserved=[{reserved_arg}]", "--output", str(min_path), ] try: run(terser_cmd) except Exception: _cleanup_scratch() raise # javascript-obfuscator obf_cmd = OBFUSCATOR.split() + [ str(min_path), "--output", str(out_path), "--compact", "true", "--simplify", "true", "--string-array", "true", "--string-array-encoding", "base64", "--string-array-threshold", "0.75", "--identifier-names-generator", "hexadecimal", "--rename-globals", "false", "--self-defending", "false", "--reserved-names", ",".join(RESERVED_GLOBALS), ] try: run(obf_cmd) except Exception: _cleanup_scratch() raise # Tidy up scratch files; keep only the final .js _cleanup_scratch() return out_path.read_text(encoding="utf-8") def find_script_blocks(text: str) -> List[tuple[int, int, str]]: """Return (start, end, body) for every plain block. `start`/`end` are file offsets covering the entire match (the tags too).""" out: List[tuple[int, int, str]] = [] for m in SCRIPT_BLOCK_RE.finditer(text): out.append((m.start(), m.end(), m.group(1))) return out def process_block(raw_body_escaped: str) -> Optional[tuple[str, str, List[str]]]: """Process a single " ) parts.append(shim) # External script tag, defer so it runs after parse but before # DOMContentLoaded — that's compatible with `onclick=` handlers # because they only fire on user interaction (post-load). parts.append( f'' ) return hash_id, "".join(parts), seen EXISTING_REF_RE = re.compile( r'' ) def collect_existing_refs(text: str) -> List[str]: """Find /assets/js/.js references already inlined into this El file from a previous run. Returns hash IDs in document order.""" return [m.group(1) for m in EXISTING_REF_RE.finditer(text)] def process_file(path: Path) -> tuple[int, int, List[dict]]: """Rewrite a single .el file, replacing extractable