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/.
This commit is contained in:
Brooklyn Nicholson 2026-07-08 22:40:51 -05:00
parent 513dba42e6
commit bf913abc2e
3 changed files with 19 additions and 15 deletions

View file

@ -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')) {

View file

@ -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",

View file

@ -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)' : ''}`)