diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ab016e..5f05d16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Carry a script's top-level `const`, `let` and `class` declarations across the wombat block, so other scripts on the page can still see them (#329) + ## [5.4.1] - 2026-07-31 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index 06a731d..cc8f341 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,6 +34,13 @@ dependencies = [ "piexif==1.1.3", # this dep is a nightmare in terms of release management, better pinned just like in optimize-images anyway "idna>=2.5,<4.0", "xxhash>=2.0,<4.0", + # Parsing JavaScript well enough to know which names a script declares at its + # top level (see rewriting/js_ast.py). tree-sitter rather than a pure-Python + # parser because the input is whatever the live web served: esprima is ES2017 + # and refuses optional chaining, class fields and `for await`, all ordinary in + # shipped code, and a parse failure here silently restores the bug this fixes. + "tree-sitter>=0.23,<1.0", + "tree-sitter-javascript>=0.23,<1.0", "types-xxhash>=2.0,<4.0", ] dynamic = ["authors", "classifiers", "keywords", "license", "version", "urls"] diff --git a/src/zimscraperlib/rewriting/js.py b/src/zimscraperlib/rewriting/js.py index 2743fde..171fb4e 100644 --- a/src/zimscraperlib/rewriting/js.py +++ b/src/zimscraperlib/rewriting/js.py @@ -21,6 +21,7 @@ from collections.abc import Callable, Iterable from typing import Any, Literal +from zimscraperlib.rewriting.js_ast import parse_top_level from zimscraperlib.rewriting.rx_replacer import ( RxRewriter, TransformationAction, @@ -348,13 +349,97 @@ def rewrite(self, text: str | bytes, opts: dict[str, Any] | None = None) -> str: if opts.get("inline", False): new_text = new_text.replace("\n", " ") - # This is not totally correctly handling globals, - # see https://github.com/openzim/python-scraperlib/issues/329 if wrap_globals: - new_text = self.first_buff + new_text + self.last_buff + new_text = self._wrap(new_text, GLOBAL_OVERRIDES) + if opts.get("inline", False): + new_text = new_text.replace("\n", " ") return new_text + def _wrap(self, new_text: str, overrides: list[str]) -> str: + """Put the script inside the wombat block, and put its globals back. + + The block is a scope, so `const`, `let` and `class` declared at the top + level of the script stop being reachable from any other script on the + page — which is how a page that declares its data in one