You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/pyodide-in-electron-vite.md
+59-32Lines changed: 59 additions & 32 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -10,43 +10,51 @@ Vite's dev server runs a `historyApiFallback` middleware that returns `index.htm
10
10
11
11
This is not an obvious failure — Pyodide may partially initialise and then hang or throw cryptic errors when it tries to load packages.
12
12
13
-
### Solution: Serve Pyodide Assets Out-of-Band
13
+
### Solution: Electron Custom Protocol Scheme (`pyodide://`)
14
14
15
-
We use two complementary mechanisms:
15
+
We register a privileged custom Electron protocol scheme that serves Pyodide assets directly from the filesystem — no network socket, no Vite interception, works identically in dev and production.
16
16
17
-
**1. Custom Vite middleware (dev only)**
17
+
**Registration in `src/main/index.ts` — must happen before `app.whenReady()`:**
18
18
19
-
In `vite.config.ts`, a plugin intercepts requests to `/pyodide/` and `/packages/` before the SPA fallback runs and streams the files directly from `src/renderer/utils/webworker/src/`:
19
+
```ts
20
+
protocol.registerSchemesAsPrivileged([{
21
+
scheme: 'pyodide',
22
+
privileges: {
23
+
standard: true, // treat like http for URL parsing / resolution
24
+
secure: true, // counts as a secure origin (needed for WASM, SAB)
25
+
supportFetchAPI: true, // allow fetch() from renderer and worker contexts
26
+
corsEnabled: true, // no CORS errors when Pyodide fetches its own assets
27
+
},
28
+
}]);
29
+
```
30
+
31
+
**Handler registered in `app.whenReady()`:**
20
32
21
33
```ts
22
-
server.middlewares.use((req, res, next) => {
23
-
const url =req.url??'';
24
-
if (url.startsWith('/pyodide/') ||url.startsWith('/packages/')) {
**2. Electron local HTTP server on port 17173 (dev + prod)**
45
+
The web worker uses `pyodide://host` as its asset base:
37
46
38
-
Web workers cannot use Vite's dev server at all — `fetch()` from a worker always hits the SPA fallback. The main process (`src/main/index.ts`) starts a plain Node.js `http` server at `http://127.0.0.1:17173` that serves `src/renderer/utils/webworker/src/` (dev) or `resources/webworker/src/` (prod).
47
+
```js
48
+
constPYODIDE_ASSET_BASE='pyodide://host';
49
+
```
39
50
40
-
The web worker (`webworker.js`) uses this as its `PYODIDE_ASSET_BASE`:
51
+
**CSP in `src/renderer/index.html`** includes `pyodide:` in `connect-src`:
41
52
42
-
```js
43
-
constPYODIDE_ASSET_BASE='http://127.0.0.1:17173';
53
+
```html
54
+
connect-src 'self' ws: wss: webpack: pyodide:
44
55
```
45
56
46
-
Port 17173 is hardcoded in three places that must stay in sync:
> **Previous approach (removed):** We previously ran a plain Node.js HTTP server on port 17173 in the main process and a custom Vite middleware that intercepted `/pyodide/` and `/packages/` requests. Both were replaced by the `pyodide://` protocol — cleaner, no open port, no hardcoded port numbers to keep in sync, and no dev-only code path.
50
58
51
59
---
52
60
@@ -73,7 +81,7 @@ worker: {
73
81
},
74
82
```
75
83
76
-
And workers must be created with `type: 'module'`:
|`indexURL`| Where Pyodide looks for its **runtime** files (WASM, stdlib). Already resolved from `node_modules` via `import.meta.url`. Do not override. |
106
-
|`packageBaseUrl`| Where `loadPackage()` fetches **package `.whl` files**. Set this to `http://127.0.0.1:17173/pyodide/`. |
114
+
|`packageBaseUrl`| Where `loadPackage()` fetches **package `.whl` files**. Set this to `pyodide://host/pyodide/`. |
`micropip` is a Python object loaded via `pyodide.pyimport()`. Passing a JavaScript array directly works fine in Pyodide 0.29.x — micropip handles the `JsProxy` conversion internally:
135
+
`micropip.install()` only accepts `http://`, `https://`, and `emfs://` URLs — it rejects custom schemes like `pyodide://`. This means we cannot install pure-Python `.whl` files (MNE and its deps) directly from the protocol.
136
+
137
+
**Workaround: fetch via JS → write to emscripten FS → install via `emfs://`**
> Note: older guidance suggested wrapping with `pyodide.toPy(whlUrls)` — this is not necessary as of 0.29.x.
155
+
The `fetch()` calls here use the `pyodide://` protocol directly (which Electron's protocol handler supports) and write the bytes into Pyodide's virtual emscripten filesystem. `micropip` then installs from `emfs://` paths, which it does accept.
156
+
157
+
> Note: In Pyodide 0.29.x, passing a JS array directly to `micropip.install()` works — no `pyodide.toPy()` wrapper needed.
130
158
131
159
---
132
160
@@ -304,8 +332,7 @@ The resulting `ArrayBuffer` is passed through the IPC chain (`preload → main`)
304
332
| Web worker entry point |`src/renderer/utils/webworker/webworker.js`|
305
333
| JS wrappers for Python calls |`src/renderer/utils/webworker/index.ts`|
0 commit comments