1410be7427
Chrome 61 (Karoo WebView) cannot parse dynamic import() — a SyntaxError at parse time prevents loadPyodide from ever being defined. Fix: fetch pyodide.js as text, replace every import( with a __loadScript( shim that uses <script> tag injection, then inject via Blob URL. The Blob script is never pre-scanned for module syntax so the patch is invisible to the parser. Also: expose waitForEngine() from extractActivity so callers can await engine readiness before batching files — manual scan now shows "Preparing extraction engine…" instead of flooding with N individual failures.
224 lines
8.4 KiB
TypeScript
224 lines
8.4 KiB
TypeScript
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/';
|
||
|
||
// Python snippets embedded as JSON strings to avoid any JS/TS escaping issues.
|
||
const PY_INSTALL_PACKAGES = [
|
||
'import micropip',
|
||
'await micropip.install(["fitdecode", "gpxpy"])',
|
||
].join('\n');
|
||
|
||
// emfs:// is Pyodide's Emscripten-FS URL scheme — the only reliable way to
|
||
// install a wheel from bytes without an http/https URL (blob: URLs are not
|
||
// recognised by micropip and cause an InvalidRequirement parse error).
|
||
// _wheel_path is set as a Pyodide global before this runs.
|
||
const PY_INSTALL_WHEEL = [
|
||
'import micropip',
|
||
'await micropip.install("emfs://" + _wheel_path, deps=False)',
|
||
].join('\n');
|
||
|
||
const PY_EXTRACT = [
|
||
'import json, shutil',
|
||
'from pathlib import Path',
|
||
'from bincio.extract.parsers.factory import parse_file',
|
||
'from bincio.extract.metrics import compute',
|
||
'from bincio.extract.writer import make_activity_id, write_activity',
|
||
'',
|
||
'outdir = Path("/tmp/bincio_out")',
|
||
'if outdir.exists(): shutil.rmtree(outdir)',
|
||
'outdir.mkdir()',
|
||
'',
|
||
'activity = parse_file(Path("/tmp/" + _filename))',
|
||
'metrics = compute(activity)',
|
||
'write_activity(activity, metrics, outdir, privacy="public", rdp_epsilon=0.0001)',
|
||
'act_id = make_activity_id(activity)',
|
||
'',
|
||
'detail_path = outdir / "activities" / (act_id + ".json")',
|
||
'ts_path = outdir / "activities" / (act_id + ".timeseries.json")',
|
||
'geojson_path = outdir / "activities" / (act_id + ".geojson")',
|
||
'',
|
||
'# write_activity in the installed wheel silently skips timeseries — write it directly.',
|
||
'if not ts_path.exists():',
|
||
' from bincio.extract.timeseries import build_timeseries as _bts',
|
||
' _ts = _bts(activity.points, activity.started_at, "public")',
|
||
' if _ts.get("t"):',
|
||
' ts_path.write_text(json.dumps(_ts))',
|
||
'',
|
||
'json.dumps({',
|
||
' "id": act_id,',
|
||
' "detail": json.loads(detail_path.read_text()),',
|
||
' "timeseries": json.loads(ts_path.read_text()) if ts_path.exists() else None,',
|
||
' "geojson": json.loads(geojson_path.read_text()) if geojson_path.exists() else None,',
|
||
'})',
|
||
].join('\n');
|
||
|
||
// JSON.stringify gives us safely-quoted JS string literals for embedding in HTML.
|
||
const PYODIDE_HTML = `<!DOCTYPE html>
|
||
<html><head><meta charset="utf-8"></head>
|
||
<body>
|
||
<script>
|
||
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)};
|
||
|
||
function _post(m) { window.ReactNativeWebView.postMessage(JSON.stringify(m)); }
|
||
|
||
var pyodide = null;
|
||
var packagesReady = false;
|
||
var wheelReady = false;
|
||
var initError = null;
|
||
|
||
(async function init() {
|
||
try {
|
||
_post({ type: 'progress', msg: 'Loading Python runtime…' });
|
||
|
||
// Chrome <63 (e.g. Karoo WebView 61) cannot parse the dynamic import()
|
||
// syntax that Pyodide uses to load pyodide.asm.js. The parser rejects
|
||
// the whole file before it runs, so loadPyodide is never defined.
|
||
//
|
||
// Fix: fetch pyodide.js as text, replace every "import(" with a
|
||
// script-tag–based loader that Chrome 61 can execute, then inject the
|
||
// patched code via a Blob URL. The Blob script is never parsed by the
|
||
// module-aware pre-scanner, so the import keyword is invisible to it.
|
||
window.__loadScript = function(url) {
|
||
return new Promise(function(res, rej) {
|
||
var s = document.createElement('script');
|
||
s.src = url;
|
||
s.onload = res;
|
||
s.onerror = function() { rej(new Error('Failed to load ' + url)); };
|
||
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();
|
||
// Replace all dynamic import() calls. Node.js-only paths (importing
|
||
// "path", "fs", etc.) are guarded by IN_NODE and never run in WebView.
|
||
pyCode = pyCode.replace(/\bimport\(/g, '__loadScript(');
|
||
|
||
await new Promise(function(res, rej) {
|
||
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')); };
|
||
document.head.appendChild(s);
|
||
});
|
||
|
||
pyodide = await loadPyodide({ indexURL: _CDN });
|
||
|
||
_post({ type: 'progress', msg: 'Loading packages…' });
|
||
await pyodide.loadPackage(['lxml', 'pyyaml', 'micropip']);
|
||
|
||
_post({ type: 'progress', msg: 'Installing fitdecode, gpxpy…' });
|
||
await pyodide.runPythonAsync(_PY_INSTALL_PACKAGES);
|
||
|
||
packagesReady = true;
|
||
_post({ type: 'pyodide_ready' });
|
||
} catch(e) {
|
||
initError = String(e);
|
||
_post({ type: 'init_error', message: initError });
|
||
}
|
||
})();
|
||
|
||
window._bincioExtract = async function(params) {
|
||
var reqId = params.reqId;
|
||
var filename = params.filename;
|
||
var base64 = params.base64;
|
||
var wheelBase64 = params.wheelBase64; // pre-fetched by React Native (avoids ATS/HTTP issues)
|
||
var wheelFilename = params.wheelFilename; // e.g. "bincio-0.1.0-py3-none-any.whl"
|
||
|
||
function post(m) { _post(Object.assign({}, m, { reqId: reqId })); }
|
||
|
||
try {
|
||
// Wait for base packages if still loading
|
||
if (!packagesReady && !initError) {
|
||
await new Promise(function(res, rej) {
|
||
var n = 0;
|
||
var id = setInterval(function() {
|
||
if (packagesReady) { clearInterval(id); res(undefined); }
|
||
else if (initError) { clearInterval(id); rej(new Error(initError)); }
|
||
else if (++n > 300) { clearInterval(id); rej(new Error('Pyodide init timed out')); }
|
||
}, 200);
|
||
});
|
||
}
|
||
if (initError) throw new Error(initError);
|
||
|
||
// Install bincio wheel on first extraction.
|
||
// Wheel bytes arrive pre-fetched from React Native (avoids ATS/HTTP issues).
|
||
// Write to Pyodide's Emscripten FS so micropip can install via emfs:// URL
|
||
// (blob: URLs are not recognised by micropip — they cause an InvalidRequirement error).
|
||
if (!wheelReady) {
|
||
post({ type: 'progress', msg: 'Loading Bincio…' });
|
||
var wheelBytes = Uint8Array.from(atob(wheelBase64), function(c) { return c.charCodeAt(0); });
|
||
var wheelPath = '/tmp/' + wheelFilename;
|
||
pyodide.FS.writeFile(wheelPath, wheelBytes);
|
||
pyodide.globals.set('_wheel_path', wheelPath);
|
||
await pyodide.runPythonAsync(_PY_INSTALL_WHEEL);
|
||
wheelReady = true;
|
||
}
|
||
|
||
post({ type: 'progress', msg: 'Extracting…' });
|
||
|
||
// Decode base64 file bytes and write to Pyodide's virtual filesystem
|
||
var bytes = Uint8Array.from(atob(base64), function(c) { return c.charCodeAt(0); });
|
||
pyodide.FS.writeFile('/tmp/' + filename, bytes);
|
||
|
||
// SHA-256 of original file bytes (replaces the stub source_hash)
|
||
var hashBuf = await crypto.subtle.digest('SHA-256', bytes.buffer);
|
||
var sourceHash = Array.from(new Uint8Array(hashBuf))
|
||
.map(function(b) { return b.toString(16).padStart(2, '0'); })
|
||
.join('');
|
||
|
||
// Run the bincio extraction pipeline
|
||
pyodide.globals.set('_filename', filename);
|
||
var resultJson = await pyodide.runPythonAsync(_PY_EXTRACT);
|
||
var result = JSON.parse(resultJson);
|
||
|
||
_post({
|
||
type: 'result',
|
||
reqId: reqId,
|
||
id: result.id,
|
||
detail: result.detail,
|
||
timeseries: result.timeseries,
|
||
geojson: result.geojson,
|
||
sourceHash: sourceHash,
|
||
});
|
||
} catch(e) {
|
||
_post({ type: 'error', reqId: reqId, message: e.message || String(e) });
|
||
}
|
||
};
|
||
</script>
|
||
</body></html>`;
|
||
|
||
export function PyodideWebView() {
|
||
return (
|
||
<WebView
|
||
ref={pyodideRef}
|
||
source={{ html: PYODIDE_HTML, baseUrl: 'https://localhost' }}
|
||
style={styles.hidden}
|
||
onMessage={handleWebViewMessage}
|
||
javaScriptEnabled
|
||
originWhitelist={['*']}
|
||
/>
|
||
);
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
// Off-screen but still rendered — display:none / opacity:0 can suppress JS on some platforms.
|
||
hidden: {
|
||
position: 'absolute',
|
||
top: -2000,
|
||
left: 0,
|
||
width: 1,
|
||
height: 1,
|
||
},
|
||
});
|