1414
1515This code is based on https://github.com/webrecorder/wabac.js/blob/main/src/rewrite/jsrewriter.ts
1616Last backport of upstream changes is from wabac.js commit:
17- Feb 20 , 2026 - 25061cb53ff113d5cff28f2f1354819f6c41034b
17+ Jul 30 , 2026 - 0564e36993f4044f17119e71dd7b2892512ee59c
1818"""
1919
2020import re
2121from collections .abc import Callable , Iterable
22- from typing import Any
22+ from typing import Any , Literal
2323
2424from zimscraperlib .rewriting .rx_replacer import (
2525 RxRewriter ,
3333from zimscraperlib .rewriting .url_rewriting import ArticleUrlRewriter , ZimPath
3434
3535# The regex used to rewrite `import ...` in module code.
36+ IMPORT_RX = re .compile (
37+ r"""^\s*?import\s*?[{"'*]""" ,
38+ )
39+ EXPORT_RX = re .compile (
40+ r"""\s*?export\s*?({([\s\w,$\n]+?)}[\s;]*|default|class)\s+""" , re .MULTILINE
41+ )
3642IMPORT_EXPORT_MATCH_RX = re .compile (
3743 r"""(^|;)\s*?(?:im|ex)port(?:['"\s]*(?:[\w*${}\s,]+from\s*)?['"\s]?['"\s])(?:.*?)['"\s]""" ,
3844)
5662 "opener" ,
5763]
5864
59- GLOBALS_RX = re .compile (
65+ WORKER_GLOBAL_OVERRIDES = ["globalThis" , "self" , "location" ]
66+
67+ GLOBALS_CONCAT_STR = (
6068 r"("
6169 + "|" .join ([r"(?:^|[^$.])\b" + x + r"\b(?:$|[^$])" for x in GLOBAL_OVERRIDES ])
6270 + ")"
6371)
6472
73+ GLOBALS_RX = re .compile (GLOBALS_CONCAT_STR )
74+
6575# This will replace `this` in code. The `_____WB$wombat$check$this$function_____`
6676# will "see" with wombat and may return a "wrapper" around `this`
6777this_rw = "_____WB$wombat$check$this$function_____(this)"
@@ -84,7 +94,7 @@ def remove_args_if_strict(
8494 return target
8595
8696
87- def add_suffix_non_prop (suffix : str ) -> TransformationAction :
97+ def add_suffix (suffix : str ) -> TransformationAction :
8898 """
8999 Create a rewrite_function which add a `suffix` to the match str.
90100 The suffix is added only if the match is not preceded by `.` or `$`.
@@ -108,7 +118,7 @@ def replace_this() -> TransformationAction:
108118 return replace ("this" , this_rw )
109119
110120
111- def replace_this_non_prop () -> TransformationAction :
121+ def replace_this_prop () -> TransformationAction :
112122 """
113123 Create a rewrite_function replacing "this" by `this_rw`.
114124
@@ -117,12 +127,12 @@ def replace_this_non_prop() -> TransformationAction:
117127
118128 def f (m_object : re .Match [str ], _opts : dict [str , Any ] | None ) -> str :
119129 offset = m_object .start ()
120- prev = m_object .string [offset - 1 ] if offset > 0 else ""
121- if prev == "\n " :
130+ first_char = m_object .string [offset - 1 ] if offset > 0 else ""
131+ if first_char == "\n " :
122132 # This detection of new line is probably buggy, plus it is hard to get the
123133 # intent of this, see https://github.com/openzim/warc2zim/issues/410
124134 return m_object [0 ].replace ("this" , ";" + this_rw )
125- if prev not in ".$" :
135+ if first_char not in ".$" :
126136 return m_object [0 ].replace ("this" , this_rw )
127137 return m_object [0 ]
128138
@@ -186,10 +196,14 @@ def create_js_rules() -> list[TransformationRule]:
186196 (re .compile (r"var\s+self" ), replace ("var" , "let" )),
187197 # rewriting `.postMessage` -> `__WB_pmw(self).postMessage`
188198 (re .compile (r"\.postMessage\b\(" ), add_prefix (".__WB_pmw(self)" )),
199+ # Avoid doing the below rewrite for `let/const` assignments,
200+ # which will break the scoping
201+ # See: https://github.com/webrecorder/wabac.js/issues/336
202+ (re .compile (r"(?:let|const)\s+location\s*=" ), m2str (lambda x : x )),
189203 # rewriting `location = ` to custom expression `(...).href =` assignement
190204 (
191205 re .compile (r"(?:^|[^$.+*/%^-])\s?\blocation\b\s*[=]\s*(?![\s\d=>])" ),
192- add_suffix_non_prop (check_loc ),
206+ add_suffix (check_loc ),
193207 ),
194208 # rewriting `return this`
195209 (re .compile (r"\breturn\s+this\b\s*(?![\s\w.$])" ), replace_this ()),
@@ -199,7 +213,7 @@ def create_js_rules() -> list[TransformationRule]:
199213 re .compile (
200214 rf"[^$.]\s?\bthis\b(?=(?:\.(?:{ '|' .join (GLOBAL_OVERRIDES )} )\b))"
201215 ),
202- replace_this_non_prop (),
216+ replace_this_prop (),
203217 ),
204218 # rewrite `= this` or `, this`
205219 (re .compile (r"[=,]\s*\bthis\b\s*(?![\s\w:.$])" ), replace_this ()),
@@ -239,7 +253,7 @@ def __init__(
239253 ):
240254 super ().__init__ (None )
241255 self .first_buff = self ._init_local_declaration (GLOBAL_OVERRIDES )
242- self .last_buff = "\n }"
256+ self .last_buff = "\n \n }"
243257 self .url_rewriter = url_rewriter
244258 self .notify_js_module = notify_js_module
245259 self .base_href = base_href
@@ -277,29 +291,20 @@ def _get_module_decl(self, local_decls: Iterable[str]) -> str:
277291 f"""import {{ { ", " .join (local_decls )} }} from "{ wb_module_decl_url } ";\n """
278292 )
279293
280- def _detect_strict_mode (self , text : str ) -> bool :
294+ def _detect_module_or_strict (self , text : str ) -> Literal [ "strict" , "module" , "lax" ] :
281295 """
282- Detect if the JavaScript code is in strict mode.
283-
284- Returns True if the code contains:
285- - "use strict"; directive
286- - import statements
287- - export statements
288- - class declarations
296+ Detect if the JavaScript code mode.
289297 """
290- # Check for "use strict"; directive
291- if '"use strict";' in text or "'use strict';" in text :
292- return True
298+ if "import" in text and IMPORT_RX .search (text ):
299+ return "module"
293300
294- # Check for import or export statements
295- if re .search (r"(?:^|\s)(?:im|ex)port\s+" , text ):
296- return True
301+ if '"use strict";' in text :
302+ return "strict"
297303
298- # Check for class declaration
299- if re .search (r"\bclass\s+" , text ):
300- return True
304+ if "export" in text and EXPORT_RX .search (text ):
305+ return "module"
301306
302- return False
307+ return "lax"
303308
304309 def rewrite (self , text : str | bytes , opts : dict [str , Any ] | None = None ) -> str :
305310 """
@@ -310,18 +315,24 @@ def rewrite(self, text: str | bytes, opts: dict[str, Any] | None = None) -> str:
310315
311316 opts = opts or {}
312317
313- is_module = opts .get ("isModule" , False )
314-
315- # Detect and set strict mode
316- # Modules are always strict mode
317- if is_module :
318+ if "isModule" not in opts :
319+ match self ._detect_module_or_strict (text ):
320+ case "module" :
321+ opts ["isModule" ] = True
322+ opts ["isStrict" ] = True
323+ case "strict" :
324+ opts ["isModule" ] = False
325+ opts ["isStrict" ] = True
326+ case _:
327+ pass
328+
329+ elif opts ["isModule" ]:
318330 opts ["isStrict" ] = True
319- elif "isStrict" not in opts : # pragma: no branch
320- # Detect strict mode from the code itself
321- opts ["isStrict" ] = self ._detect_strict_mode (text )
322331
323332 rules = REWRITE_JS_RULES [:]
324333
334+ is_module = opts .get ("isModule" , False )
335+
325336 if is_module :
326337 rules .append (self ._get_esm_import_rule ())
327338
@@ -332,12 +343,16 @@ def rewrite(self, text: str | bytes, opts: dict[str, Any] | None = None) -> str:
332343 if is_module :
333344 return self ._get_module_decl (GLOBAL_OVERRIDES ) + new_text
334345
335- if GLOBALS_RX .search (text ):
336- new_text = self .first_buff + new_text + self .last_buff
346+ wrap_globals = GLOBALS_RX .search (text ) is not None
337347
338348 if opts .get ("inline" , False ):
339349 new_text = new_text .replace ("\n " , " " )
340350
351+ # This is not totally correctly handling globals,
352+ # see https://github.com/openzim/python-scraperlib/issues/329
353+ if wrap_globals :
354+ new_text = self .first_buff + new_text + self .last_buff
355+
341356 return new_text
342357
343358 def _get_esm_import_rule (self ) -> TransformationRule :
0 commit comments