An easy-to-use platform for EEG experimentation in the classroom
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

simplify pyodide in the app

+48 -110
+22 -52
src/main/index.ts
··· 5 5 * All Node.js / filesystem / shell operations the renderer needs 6 6 * are handled here via ipcMain handlers and exposed via the preload. 7 7 */ 8 - import { app, BrowserWindow, ipcMain, dialog, shell, session } from 'electron'; 8 + import { app, BrowserWindow, ipcMain, dialog, shell, session, protocol, net } from 'electron'; 9 9 import path from 'path'; 10 10 import fs from 'fs'; 11 - import http from 'http'; 11 + import { pathToFileURL } from 'url'; 12 12 import os from 'os'; 13 13 import Papa from 'papaparse'; 14 14 import mkdirp from 'mkdirp'; ··· 24 24 'true' 25 25 ); 26 26 27 - // Port for the local pyodide asset server (serves whl files to web workers, 28 - // bypassing Vite's dev server which returns HTML for all fetch() requests). 29 - const PYODIDE_ASSET_PORT = 17173; 30 - 31 - const PYODIDE_CONTENT_TYPES: Record<string, string> = { 32 - '.json': 'application/json', 33 - '.whl': 'application/zip', 34 - '.zip': 'application/zip', 35 - '.wasm': 'application/wasm', 36 - '.js': 'application/javascript', 37 - '.mjs': 'application/javascript', 38 - }; 39 - 40 - function startPyodideAssetServer(rootDir: string): void { 41 - const server = http.createServer((req, res) => { 42 - res.setHeader('Access-Control-Allow-Origin', '*'); 43 - res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); 44 - if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; } 45 - 46 - const urlPath = (req.url || '').split('?')[0]; 47 - const filePath = path.join(rootDir, urlPath); 48 - const ext = path.extname(filePath).toLowerCase(); 49 - 50 - fs.stat(filePath, (statErr, stat) => { 51 - if (statErr || !stat.isFile()) { 52 - res.writeHead(404, { 'Content-Type': 'text/plain' }); 53 - res.end(`Not found: ${urlPath}`); 54 - return; 55 - } 56 - res.setHeader('Content-Type', PYODIDE_CONTENT_TYPES[ext] || 'application/octet-stream'); 57 - res.setHeader('Content-Length', stat.size); 58 - res.setHeader('Cache-Control', 'no-cache'); 59 - res.writeHead(200); 60 - fs.createReadStream(filePath).pipe(res); 61 - }); 62 - }); 63 - 64 - server.listen(PYODIDE_ASSET_PORT, '127.0.0.1', () => { 65 - console.log(`[main] Pyodide asset server: http://127.0.0.1:${PYODIDE_ASSET_PORT}`); 66 - }); 67 - 68 - server.on('error', (err: NodeJS.ErrnoException) => { 69 - console.error('[main] Pyodide asset server error:', err.message); 70 - }); 71 - } 27 + // Register pyodide:// as a privileged custom scheme so web workers can 28 + // fetch() package .whl files from it. Must be called before app.whenReady(). 29 + protocol.registerSchemesAsPrivileged([ 30 + { 31 + scheme: 'pyodide', 32 + privileges: { 33 + standard: true, // treat like http for URL parsing / resolution 34 + secure: true, // counts as a secure origin (needed for WASM, SAB) 35 + supportFetchAPI: true, // allow fetch() from renderer and worker contexts 36 + corsEnabled: true, // no CORS errors when Pyodide fetches its own assets 37 + }, 38 + }, 39 + ]); 72 40 73 41 export default class AppUpdater { 74 42 constructor() { ··· 513 481 }); 514 482 515 483 app.whenReady().then(async () => { 516 - // Serve pyodide assets (whl files, runtime files) via a local HTTP server so 517 - // web workers can fetch() them without hitting Vite's SPA fallback, which 518 - // returns HTML for ALL fetch() requests regardless of path. 519 - // Port 17173 is hardcoded and matched in webworker.js. 484 + // Serve pyodide:// assets (whl files, manifest.json, etc.) directly from the 485 + // filesystem via Electron's protocol API — no network socket required. 520 486 // In dev: files are in src/renderer/utils/webworker/src/ 521 487 // In prod: files are in resources/webworker/src/ (via extraResources) 522 488 const pyodideRoot = is.dev 523 489 ? path.join(app.getAppPath(), 'src/renderer/utils/webworker/src') 524 490 : path.join(process.resourcesPath, 'webworker/src'); 525 491 526 - startPyodideAssetServer(pyodideRoot); 492 + protocol.handle('pyodide', (request) => { 493 + const { pathname } = new URL(request.url); 494 + const filePath = path.join(pyodideRoot, pathname); 495 + return net.fetch(pathToFileURL(filePath).href); 496 + }); 527 497 528 498 // Enable F12 devtools shortcut and Ctrl+R reload in dev, disable in prod 529 499 app.on('browser-window-created', (_, window) => {
+1 -1
src/renderer/index.html
··· 4 4 <meta charset="utf-8" /> 5 5 <meta 6 6 http-equiv="Content-Security-Policy" 7 - content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob:; connect-src 'self' ws: wss: webpack: http://127.0.0.1:17173; font-src 'self' data: https://fonts.gstatic.com; worker-src blob: 'self';" 7 + content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: blob:; connect-src 'self' ws: wss: webpack: pyodide:; font-src 'self' data: https://fonts.gstatic.com; worker-src blob: 'self';" 8 8 /> 9 9 <title>BrainWaves</title> 10 10 </head>
+25 -17
src/renderer/utils/webworker/webworker.js
··· 12 12 * can "fetch" it from memory. 13 13 * 14 14 * Package whl files (numpy, scipy, etc.) live in 15 - * src/renderer/utils/webworker/src/pyodide/ and are served by a tiny Node.js 16 - * HTTP server on port 17173 started in the Electron main process. This bypasses 17 - * Vite's dev server, which returns HTML (SPA fallback) for ALL fetch() requests 18 - * from web workers, including /@fs/ and publicDir paths. 15 + * src/renderer/utils/webworker/src/pyodide/ and are served via a custom 16 + * Electron protocol scheme (pyodide://) registered in the main process. 17 + * This requires no network socket and works in both dev and production. 19 18 * 20 - * MNE and its pure-Python deps are installed via micropip from local .whl 21 - * files served by the same pyodide-asset:// protocol under /packages/. 19 + * MNE and its pure-Python deps: JS fetches each .whl via pyodide://, writes 20 + * the bytes into Pyodide's emscripten FS (/tmp/), then micropip installs from 21 + * emfs:///tmp/ — micropip only accepts http/https/emfs URLs, not custom schemes. 22 22 */ 23 23 24 24 // ?url → Vite resolves to /@fs/... in dev; asset URL in prod. ··· 26 26 import pyodideMjsUrl from 'pyodide/pyodide.mjs?url'; 27 27 import lockFileRaw from 'pyodide/pyodide-lock.json?raw'; 28 28 29 - // A tiny Node.js HTTP server on port 17173 (started in the Electron main 30 - // process) serves pyodide assets from src/renderer/utils/webworker/src/. 31 - // This bypasses Vite's dev server, which returns index.html (SPA fallback) 32 - // for ALL fetch() requests from web workers, including /@fs/ and publicDir paths. 33 - const PYODIDE_ASSET_BASE = 'http://127.0.0.1:17173'; 29 + // Custom Electron protocol scheme registered in src/main/index.ts. 30 + // Serves files from src/renderer/utils/webworker/src/ (dev) or 31 + // resources/webworker/src/ (prod) without opening a network socket. 32 + const PYODIDE_ASSET_BASE = 'pyodide://host'; 34 33 35 34 const pyodideReadyPromise = (async () => { 36 35 const { loadPyodide } = await import(/* @vite-ignore */ pyodideMjsUrl); ··· 67 66 await pyodide.loadPackage('micropip', { checkIntegrity: false }); 68 67 const micropip = pyodide.pyimport('micropip'); 69 68 70 - // MNE + pure-Python deps are served from /packages/ via pyodide-asset://. 71 - const manifestUrl = `${PYODIDE_ASSET_BASE}/packages/manifest.json`; 72 - const manifest = await fetch(manifestUrl).then((r) => r.json()); 73 - const whlUrls = Object.values(manifest).map( 74 - ({ filename }) => `${PYODIDE_ASSET_BASE}/packages/${filename}` 69 + // MNE + pure-Python deps: micropip only accepts http://, https://, emfs://, 70 + // and relative paths — it rejects the pyodide:// custom scheme. 71 + // Workaround: JS-fetch each .whl via the protocol handler (which supports it), 72 + // write the bytes into Pyodide's emscripten virtual FS, then install via emfs://. 73 + const manifest = await fetch(`${PYODIDE_ASSET_BASE}/packages/manifest.json`) 74 + .then((r) => r.json()); 75 + 76 + for (const { filename } of Object.values(manifest)) { 77 + const buffer = await fetch(`${PYODIDE_ASSET_BASE}/packages/${filename}`) 78 + .then((r) => r.arrayBuffer()); 79 + pyodide.FS.writeFile(`/tmp/${filename}`, new Uint8Array(buffer)); 80 + } 81 + 82 + await micropip.install( 83 + Object.values(manifest).map(({ filename }) => `emfs:///tmp/${filename}`) 75 84 ); 76 - await micropip.install(whlUrls); 77 85 78 86 return pyodide; 79 87 })();
-40
vite.config.ts
··· 1 1 import { defineConfig } from 'electron-vite'; 2 2 import react from '@vitejs/plugin-react'; 3 3 import path from 'path'; 4 - import fs from 'node:fs'; 5 4 import { createRequire } from 'module'; 6 5 const _require = createRequire(import.meta.url); 7 6 ··· 49 48 // /pyodide/pyodide.mjs, /pyodide/pyodide.asm.js, /packages/*.whl, etc. 50 49 publicDir: path.resolve(__dirname, 'src/renderer/utils/webworker/src'), 51 50 plugins: [ 52 - // Serve pyodide runtime and package .whl files directly from the filesystem 53 - // before Vite's SPA fallback can intercept them. publicDir alone is not 54 - // reliable — Vite's historyApiFallback returns index.html for fetch() 55 - // requests to these paths in dev mode. 56 - { 57 - name: 'serve-pyodide-assets', 58 - configureServer(server) { 59 - const staticDir = path.resolve( 60 - __dirname, 61 - 'src/renderer/utils/webworker/src' 62 - ); 63 - const contentTypes: Record<string, string> = { 64 - '.json': 'application/json', 65 - '.whl': 'application/zip', 66 - '.zip': 'application/zip', 67 - '.wasm': 'application/wasm', 68 - '.js': 'application/javascript', 69 - '.mjs': 'application/javascript', 70 - }; 71 - server.middlewares.use((req, res, next) => { 72 - const url = req.url ?? ''; 73 - if (url.startsWith('/pyodide/') || url.startsWith('/packages/')) { 74 - console.log('[serve-pyodide-assets] intercepted:', url); 75 - const filePath = path.join(staticDir, url.split('?')[0]); 76 - if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) { 77 - const ext = path.extname(filePath).toLowerCase(); 78 - res.setHeader( 79 - 'Content-Type', 80 - contentTypes[ext] ?? 'application/octet-stream' 81 - ); 82 - res.setHeader('Cache-Control', 'no-cache'); 83 - fs.createReadStream(filePath).pipe(res); 84 - return; 85 - } 86 - } 87 - next(); 88 - }); 89 - }, 90 - }, 91 51 react({ 92 52 jsxRuntime: 'classic', // React 16 does not ship react/jsx-runtime 93 53 babel: {