fix(mobile): patch pyodide.js at runtime to bypass Chrome 61 import() syntax
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.
This commit is contained in:
@@ -6,7 +6,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { AppState, PermissionsAndroid, Platform, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
|
||||
import { insertActivity, isSourcePathImported, getSetting } from '@/db/queries';
|
||||
import { PyodideWebView } from '@/extraction/PyodideWebView';
|
||||
import { extractFile } from '@/extraction/extractActivity';
|
||||
import { extractFile, waitForEngine } from '@/extraction/extractActivity';
|
||||
import { useTheme } from '@/ThemeContext';
|
||||
|
||||
const FIT_EXTENSIONS = ['.fit', '.fit.gz'];
|
||||
@@ -54,7 +54,10 @@ export default function ImportScreen() {
|
||||
const path = await getSetting(db, 'auto_import_path');
|
||||
if (!path) return;
|
||||
const instanceUrl = await getSetting(db, 'instance_url');
|
||||
if (!instanceUrl) return; // silently skip — engine can't be downloaded without an instance
|
||||
if (!instanceUrl) return;
|
||||
|
||||
// Wait for the extraction engine — but don't block forever on auto-scan.
|
||||
try { await waitForEngine(120_000); } catch { return; }
|
||||
|
||||
const newFiles = await discoverNewFiles(db, path);
|
||||
if (newFiles.length === 0) return;
|
||||
@@ -77,6 +80,14 @@ export default function ImportScreen() {
|
||||
return;
|
||||
}
|
||||
|
||||
setState({ status: 'loading', msg: 'Preparing extraction engine…', current: 0, total: 0 });
|
||||
try {
|
||||
await waitForEngine();
|
||||
} catch (e: unknown) {
|
||||
setState({ status: 'error', message: e instanceof Error ? e.message : String(e) });
|
||||
return;
|
||||
}
|
||||
|
||||
setState({ status: 'loading', msg: 'Scanning…', current: 0, total: 0 });
|
||||
const newFiles = await discoverNewFiles(db, path);
|
||||
if (newFiles.length === 0) {
|
||||
|
||||
@@ -2,7 +2,9 @@ import { StyleSheet } from 'react-native';
|
||||
import WebView from 'react-native-webview';
|
||||
import { handleWebViewMessage, pyodideRef } from './extractActivity';
|
||||
|
||||
const CDN = 'https://cdn.jsdelivr.net/pyodide/v0.26.4/full/';
|
||||
// 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 = [
|
||||
@@ -74,10 +76,39 @@ var initError = null;
|
||||
(async function init() {
|
||||
try {
|
||||
_post({ type: 'progress', msg: 'Loading Python runtime…' });
|
||||
await new Promise(function(res, rej) {
|
||||
|
||||
// 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 = _CDN + 'pyodide.js';
|
||||
s.onload = res; s.onerror = rej;
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
@@ -22,6 +22,24 @@ const pending = new Map<string, Pending>();
|
||||
let reqCounter = 0;
|
||||
let isExtracting = false;
|
||||
|
||||
// Engine readiness — tracked so callers can wait before batching files.
|
||||
let _engineReady = false;
|
||||
let _engineError: string | null = null;
|
||||
const _engineResolvers: Array<() => void> = [];
|
||||
const _engineRejecters: Array<(e: Error) => void> = [];
|
||||
|
||||
export function waitForEngine(timeoutMs = 300_000): Promise<void> {
|
||||
if (_engineReady) return Promise.resolve();
|
||||
if (_engineError) return Promise.reject(new Error(_engineError));
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
reject(new Error('Extraction engine timed out — check network and Bincio instance URL'));
|
||||
}, timeoutMs);
|
||||
_engineResolvers.push(() => { clearTimeout(timer); resolve(); });
|
||||
_engineRejecters.push((e) => { clearTimeout(timer); reject(e); });
|
||||
});
|
||||
}
|
||||
|
||||
export function handleWebViewMessage(e: WebViewMessageEvent): void {
|
||||
let msg: Record<string, unknown>;
|
||||
try { msg = JSON.parse(e.nativeEvent.data); } catch { return; }
|
||||
@@ -30,6 +48,14 @@ export function handleWebViewMessage(e: WebViewMessageEvent): void {
|
||||
const p = reqId ? pending.get(reqId) : undefined;
|
||||
|
||||
switch (msg.type) {
|
||||
case 'pyodide_ready':
|
||||
_engineReady = true;
|
||||
_engineResolvers.splice(0).forEach(fn => fn());
|
||||
break;
|
||||
case 'init_error':
|
||||
_engineError = msg.message as string;
|
||||
_engineRejecters.splice(0).forEach(fn => fn(new Error(_engineError!)));
|
||||
break;
|
||||
case 'result':
|
||||
if (p) {
|
||||
pending.delete(reqId!);
|
||||
|
||||
Reference in New Issue
Block a user