···11/**
22- * Pyodide Web Worker — ES module worker following the pattern from
33- * https://gitlab.com/castedo/pyodide-worker-example
22+ * Pyodide Web Worker — local node_modules implementation.
43 *
54 * Loading strategy
65 * ----------------
77- * 1. `import { loadPyodide } from "pyodide"` — Vite resolves this to
88- * node_modules/pyodide/pyodide.mjs and serves it from /@fs/… in dev mode,
99- * completely bypassing any publicDir transform issues.
1010- *
1111- * 2. `indexURL: '/pyodide/'` — tells pyodide where to find pyodide-lock.json
1212- * and binary package wheels (.whl). These are served from publicDir:
1313- * src/renderer/utils/webworker/src/pyodide/
1414- * which is populated by:
1515- * • InstallPyodide.mjs (copies pyodide-lock.json + runtime from npm)
1616- * • InstallMNE.mjs (downloads binary wheels from Pyodide CDN)
1717- *
1818- * 3. Binary packages (numpy / scipy / matplotlib / pandas) — loaded via
1919- * pyodide.loadPackage(), resolved from local /pyodide/ files.
66+ * Use Vite's `?url` suffix on 'pyodide/pyodide.mjs' to get the resolved file URL
77+ * at build/dev time (/@fs/... in dev, an asset URL in prod), then dynamically
88+ * import from that URL. This bypasses Vite's SPA fallback and lets pyodide.mjs
99+ * resolve all sibling assets (pyodide.asm.wasm, pyodide-lock.json, etc.) via
1010+ * import.meta.url — no CDN required.
2011 *
2121- * 4. MNE + pure-Python deps — installed via micropip from pre-downloaded
2222- * wheels in /packages/, listed in /packages/manifest.json.
2323- * Populated by InstallMNE.mjs Part 2 (PyPI).
1212+ * Production builds use the files copied to publicDir by InstallPyodide.mjs.
2413 */
25142626-import { loadPyodide } from 'pyodide';
2727-2828-/**
2929- * Derive the renderer root URL from the worker's own location.
3030- *
3131- * Dev (Vite HTTP server): self.location.href is an http:// URL — the root is
3232- * just the origin, so root-relative paths work as normal.
3333- *
3434- * Production (Electron file://): Vite bundles workers into assets/, so the
3535- * renderer root is one directory above the worker file.
3636- */
3737-function getRendererBaseUrl() {
3838- const loc = self.location.href;
3939- if (loc.startsWith('http')) {
4040- return new URL('/', loc).href;
4141- }
4242- // file:// — go up from assets/webworker-[hash].js to the renderer root
4343- return new URL('../', loc).href;
4444-}
4545-4646-async function initPyodide() {
4747- const base = getRendererBaseUrl();
4848-4949- // indexURL tells pyodide where to load pyodide-lock.json and binary wheels.
5050- // Resolved from the renderer root so it works under both HTTP (dev) and
5151- // file:// (Electron production).
5252- const pyodide = await loadPyodide({ indexURL: new URL('pyodide/', base).href });
5353-5454- // Load binary packages from locally served .whl files.
5555- await pyodide.loadPackage(['numpy', 'scipy', 'matplotlib', 'pandas']);
1515+// ?url tells Vite to resolve the path and return a URL string rather than bundling
1616+// the module. In dev mode this is a /@fs/ URL (bypasses SPA fallback); in prod it
1717+// is an asset URL. We then dynamically import from that URL so pyodide.mjs can
1818+// resolve all its sibling assets (pyodide.asm.wasm, etc.) via import.meta.url.
1919+import pyodideMjsUrl from 'pyodide/pyodide.mjs?url';
56205757- // Install MNE and its pure-Python deps from pre-downloaded wheels.
5858- let manifest = {};
5959- try {
6060- const res = await fetch(new URL('packages/manifest.json', base).href);
6161- if (res.ok) {
6262- manifest = await res.json();
6363- } else {
6464- console.warn('[pyodide worker] manifest.json not found — MNE unavailable');
6565- }
6666- } catch (err) {
6767- console.warn('[pyodide worker] Could not fetch manifest.json:', err);
6868- }
6969-7070- const wheelUrls = Object.values(manifest).map(
7171- (entry) => new URL(`packages/${entry.filename}`, base).href
7272- );
7373-7474- if (wheelUrls.length > 0) {
7575- await pyodide.loadPackage('micropip');
7676- const micropip = pyodide.pyimport('micropip');
7777- await micropip.install(wheelUrls);
7878- } else {
7979- console.warn('[pyodide worker] No MNE wheels in manifest — skipping micropip install');
8080- }
8181-8282- return pyodide;
8383-}
8484-8585-// Start loading immediately so it is ready when the first message arrives.
8686-const pyodideReadyPromise = initPyodide();
2121+const pyodideReadyPromise = (async () => {
2222+ const { loadPyodide } = await import(/* @vite-ignore */ pyodideMjsUrl);
2323+ return loadPyodide();
2424+})();
87258826self.onmessage = async (event) => {
8927 // Propagate init failures back to the main thread rather than hanging silently.
+1-1
vite.config.ts
···8080 exclude: ['pyodide'],
8181 },
8282 worker: {
8383- // ES module workers are required for `import { loadPyodide } from "pyodide"`.
8383+ // ES module workers are required for the CDN import in webworker.js.
8484 format: 'es',
8585 },
8686 build: {