#!/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