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