···25252626import { loadPyodide } from 'pyodide';
27272828+/**
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+2846async function initPyodide() {
4747+ const base = getRendererBaseUrl();
4848+2949 // indexURL tells pyodide where to load pyodide-lock.json and binary wheels.
3030- // The publicDir (src/renderer/utils/webworker/src/) is served at the web root,
3131- // so /pyodide/ maps to src/renderer/utils/webworker/src/pyodide/.
3232- const pyodide = await loadPyodide({ indexURL: '/pyodide/' });
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 });
33533454 // Load binary packages from locally served .whl files.
3555 await pyodide.loadPackage(['numpy', 'scipy', 'matplotlib', 'pandas']);
···3757 // Install MNE and its pure-Python deps from pre-downloaded wheels.
3858 let manifest = {};
3959 try {
4040- const res = await fetch(new URL('/packages/manifest.json', self.location.href).href);
6060+ const res = await fetch(new URL('packages/manifest.json', base).href);
4161 if (res.ok) {
4262 manifest = await res.json();
4363 } else {
···4868 }
49695070 const wheelUrls = Object.values(manifest).map(
5151- (entry) => new URL(`/packages/${entry.filename}`, self.location.href).href
7171+ (entry) => new URL(`packages/${entry.filename}`, base).href
5272 );
53735474 if (wheelUrls.length > 0) {
···6686const pyodideReadyPromise = initPyodide();
67876888self.onmessage = async (event) => {
6969- const pyodide = await pyodideReadyPromise;
8989+ // Propagate init failures back to the main thread rather than hanging silently.
9090+ let pyodide;
9191+ try {
9292+ pyodide = await pyodideReadyPromise;
9393+ } catch (error) {
9494+ self.postMessage({ error: `Pyodide init failed: ${error.message}` });
9595+ return;
9696+ }
70977198 const { data, ...context } = event.data;
7299