diff --git a/mobile/extraction/PyodideWebView.tsx b/mobile/extraction/PyodideWebView.tsx index d69d7c9..d531b21 100644 --- a/mobile/extraction/PyodideWebView.tsx +++ b/mobile/extraction/PyodideWebView.tsx @@ -2,9 +2,10 @@ import { StyleSheet } from 'react-native'; import WebView from 'react-native-webview'; import { handleWebViewMessage, pyodideRef } from './extractActivity'; -// v0.18.1: newest version whose pyodide.js is ES2017-compatible (no ??, no ?.) -// Chrome 61 (Karoo WebView) throws SyntaxError on 0.19+ which uses nullish-coalescing. -const CDN = 'https://cdn.jsdelivr.net/pyodide/v0.18.1/full/'; +const CDN = 'https://cdn.jsdelivr.net/pyodide/v0.26.4/full/'; +// v0.18.1: last version whose JS wrapper avoids ??, ?., and other syntax +// unavailable on Chrome <80 (e.g. Karoo WebView 61). Used in the compat path. +const CDN_COMPAT = 'https://cdn.jsdelivr.net/pyodide/v0.18.1/full/'; // Python snippets embedded as JSON strings to avoid any JS/TS escaping issues. const PY_INSTALL_PACKAGES = [ @@ -64,7 +65,8 @@ const PYODIDE_HTML = ` var _PY_INSTALL_PACKAGES = ${JSON.stringify(PY_INSTALL_PACKAGES)}; var _PY_INSTALL_WHEEL = ${JSON.stringify(PY_INSTALL_WHEEL)}; var _PY_EXTRACT = ${JSON.stringify(PY_EXTRACT)}; -var _CDN = ${JSON.stringify(CDN)}; +var _CDN = ${JSON.stringify(CDN)}; +var _CDN_COMPAT = ${JSON.stringify(CDN_COMPAT)}; function _post(m) { window.ReactNativeWebView.postMessage(JSON.stringify(m)); } @@ -77,17 +79,23 @@ var initError = null; try { _post({ type: 'progress', msg: 'Loading Python runtime…' }); - // Chrome <63 cannot parse dynamic import() — a parse-time SyntaxError - // prevents loadPyodide from ever being defined. Detect this once and - // use a patched loader only for those old WebViews. - var chromeVer = (navigator.userAgent.match(/Chrome\\/([0-9]+)/) || [])[1]; - var needsPatch = chromeVer && parseInt(chromeVer) < 63; + // Chrome <80 is missing features that modern Pyodide uses in its JS wrapper: + // Chrome <71: no globalThis → factory throws ReferenceError immediately + // Chrome <63: no dynamic import() / for-await-of → parse/runtime failure + // Detection: read Chrome version from UA; absent means non-Chrome (assume modern). + var _chromeVer = (navigator.userAgent.match(/Chrome\\/([0-9]+)/) || [])[1]; + var _needsPatch = _chromeVer && parseInt(_chromeVer) < 80; - if (needsPatch) { - // Fetch pyodide.js as text and replace every "import(" with a - // script-tag shim before injecting via Blob URL. Using split/join - // avoids regex escape sequences, which template literals corrupt. - // Node.js import("path") calls are guarded by IN_NODE and never run. + if (_needsPatch) { + // Use v0.18.1 — its JS wrapper avoids ??, ?., and other Chrome-80+ syntax. + // Then apply three text patches before injecting via Blob URL (Blob scripts + // bypass the browser's module pre-scanner, so patched keywords are invisible). + // + // Patches (split/join avoids regex escapes, which template literals corrupt): + // 1. globalThis polyfill prepended — Chrome <71 lacks globalThis entirely + // 2. import( → __loadScript( — Chrome <63 cannot parse dynamic import + // 3. for await( → for( — Chrome <63 lacks async iteration; + // the only affected fn (getFsHandles/NativeFS) is never called by us window.__loadScript = function(url) { return new Promise(function(res, rej) { var s = document.createElement('script'); @@ -97,19 +105,22 @@ var initError = null; document.head.appendChild(s); }); }; - var pyResp = await fetch(_CDN + 'pyodide.js'); - if (!pyResp.ok) throw new Error('Could not fetch pyodide.js (' + pyResp.status + ')'); - var pyCode = await pyResp.text(); - pyCode = pyCode.split('import(').join('__loadScript('); + var _pyResp = await fetch(_CDN_COMPAT + 'pyodide.js'); + if (!_pyResp.ok) throw new Error('Could not fetch pyodide.js (' + _pyResp.status + ')'); + var _pyCode = await _pyResp.text(); + _pyCode = 'var globalThis=typeof globalThis!=="undefined"?globalThis:self;\n' + _pyCode; + _pyCode = _pyCode.split('import(').join('__loadScript('); + _pyCode = _pyCode.split('for await(').join('for('); await new Promise(function(res, rej) { - var blob = new Blob([pyCode], { type: 'application/javascript' }); + var blob = new Blob([_pyCode], { type: 'application/javascript' }); var blobUrl = URL.createObjectURL(blob); var s = document.createElement('script'); s.src = blobUrl; s.onload = function() { URL.revokeObjectURL(blobUrl); res(); }; - s.onerror = function() { URL.revokeObjectURL(blobUrl); rej(new Error('Failed to execute patched pyodide.js')); }; + s.onerror = function() { URL.revokeObjectURL(blobUrl); rej(new Error('Failed to inject patched pyodide.js')); }; document.head.appendChild(s); }); + pyodide = await loadPyodide({ indexURL: _CDN_COMPAT }); } else { await new Promise(function(res, rej) { var s = document.createElement('script'); @@ -117,10 +128,9 @@ var initError = null; s.onload = res; s.onerror = rej; document.head.appendChild(s); }); + pyodide = await loadPyodide({ indexURL: _CDN }); } - pyodide = await loadPyodide({ indexURL: _CDN }); - _post({ type: 'progress', msg: 'Loading packages…' }); await pyodide.loadPackage(['lxml', 'pyyaml', 'micropip']);