From bf913abc2efcfb6379296007db46e6f0e3dc1738 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Wed, 8 Jul 2026 22:40:51 -0500 Subject: [PATCH] fix(desktop): stop using tsx to boot Electron main in dev Electron 40 ships Node 24.15, where tsx's ESM load hook returns null and crashes with ERR_INVALID_RETURN_PROPERTY_VALUE. Bundle main+preload via esbuild for `npm run dev` and always load the JS preload from dist/. --- apps/desktop/electron/main.ts | 9 ++++----- apps/desktop/package.json | 6 +++--- apps/desktop/scripts/bundle-electron-main.mjs | 19 ++++++++++++------- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/apps/desktop/electron/main.ts b/apps/desktop/electron/main.ts index f569bf2dd45..b8c2d03da38 100644 --- a/apps/desktop/electron/main.ts +++ b/apps/desktop/electron/main.ts @@ -144,11 +144,10 @@ const IS_WSL = isWslEnvironment() const DARWIN_MAJOR = IS_MAC ? Number.parseInt(os.release(), 10) || 0 : 0 const APP_ROOT = app.getAppPath() -// Preload script path: in dev we load the .ts source directly (tsx handles -// the transform); in prod we load the bundled .js from dist/. -const PRELOAD_PATH = IS_PACKAGED - ? path.join(APP_ROOT, 'dist', 'electron-preload.js') - : path.join(import.meta.dirname, 'preload.ts') +// Preload must be plain JS — Electron's sandbox can't run .ts, and tsx's +// ESM loader is broken on Electron 40's Node (ERR_INVALID_RETURN_PROPERTY_VALUE). +// Dev (`npm run dev`) and prod both load the esbuild output from dist/. +const PRELOAD_PATH = path.join(APP_ROOT, 'dist', 'electron-preload.js') function hiddenWindowsChildOptions(options: any = {}): ExecFileSyncOptionsWithStringEncoding { if (!IS_WINDOWS || Object.prototype.hasOwnProperty.call(options, 'windowsHide')) { diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 9bad2161cb0..fe91d2991c0 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -14,9 +14,9 @@ "dev": "concurrently -k \"npm:dev:renderer\" \"npm:dev:electron\"", "dev:fake-boot": "cross-env HERMES_DESKTOP_BOOT_FAKE=1 HERMES_DESKTOP_BOOT_FAKE_STEP_MS=650 npm run dev", "dev:renderer": "node scripts/assert-root-install.mjs && vite --host 127.0.0.1 --port 5174", - "dev:electron": "wait-on http://127.0.0.1:5174 && cross-env XCURSOR_SIZE=24 HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 NODE_OPTIONS=tsx electron electron/main.ts", - "profile:main": "wait-on http://127.0.0.1:5174 && cross-env XCURSOR_SIZE=24 HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 NODE_OPTIONS=tsx electron --inspect=9229 electron/main.ts", - "profile:main:cpu": "wait-on http://127.0.0.1:5174 && cross-env XCURSOR_SIZE=24 NODE_OPTIONS=--cpu-prof NODE_OPTIONS=tsx HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron electron/main.ts", + "dev:electron": "wait-on http://127.0.0.1:5174 && node scripts/bundle-electron-main.mjs --dev && cross-env XCURSOR_SIZE=24 HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron .", + "profile:main": "wait-on http://127.0.0.1:5174 && node scripts/bundle-electron-main.mjs --dev && cross-env XCURSOR_SIZE=24 HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron --inspect=9229 .", + "profile:main:cpu": "wait-on http://127.0.0.1:5174 && node scripts/bundle-electron-main.mjs --dev && cross-env XCURSOR_SIZE=24 NODE_OPTIONS=--cpu-prof HERMES_DESKTOP_DEV_SERVER=http://127.0.0.1:5174 electron .", "start": "npm run build && electron .", "build": "node scripts/assert-root-install.mjs && node scripts/write-build-stamp.mjs && tsc -b && vite build && node scripts/bundle-electron-main.mjs && node scripts/stage-native-deps.mjs", "postbuild": "node scripts/assert-dist-built.mjs", diff --git a/apps/desktop/scripts/bundle-electron-main.mjs b/apps/desktop/scripts/bundle-electron-main.mjs index f950f4908a2..316d2ce1b18 100644 --- a/apps/desktop/scripts/bundle-electron-main.mjs +++ b/apps/desktop/scripts/bundle-electron-main.mjs @@ -25,9 +25,14 @@ const preloadEntry = resolve(root, 'electron/preload.ts') const preloadOut = resolve(distDir, 'electron-preload.js') const external = ['electron', 'node-pty', 'fs'] - const define = { - 'process.env.HERMES_DESKTOP_IS_PACKAGED': JSON.stringify(true) - } +// Production bundles bake packaged=true so unpackaged `electron .` still +// behaves like a packaged build. Dev bundles (`--dev`) leave the env alone +// so HERMES_DESKTOP_DEV_SERVER / source-tree resolution keep working. +const isDev = process.argv.includes('--dev') +const define = isDev + ? {} + : { 'process.env.HERMES_DESKTOP_IS_PACKAGED': JSON.stringify(true) } + // Bundle main.ts → dist/electron-main.mjs await build({ entryPoints: [mainEntry], @@ -37,15 +42,15 @@ await build({ target: 'node20', outfile: mainOut, external, - banner: { + banner: { js: "import { createRequire } from 'module'; const require = createRequire(import.meta.url);", }, define, logLevel: 'info', }) -console.log(`bundled ${mainOut}`) +console.log(`bundled ${mainOut}${isDev ? ' (dev)' : ''}`) -// Bundle preload.ts → dist/electron-preload.cjs +// Bundle preload.ts → dist/electron-preload.js await build({ entryPoints: [preloadEntry], bundle: true, @@ -57,4 +62,4 @@ await build({ define, logLevel: 'info', }) -console.log(`bundled ${preloadOut}`) +console.log(`bundled ${preloadOut}${isDev ? ' (dev)' : ''}`)