From b93a7eece1b52204f05be95003da37d7368c6ea5 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Fri, 10 Jul 2026 15:10:15 -0700 Subject: [PATCH] fix(watcher): preserve isolation in electron-vite builds (#8162) --- .../ipc/parcel-watcher-entry-path.test.ts | 40 +++++++++++++++++++ src/main/ipc/parcel-watcher-entry-path.ts | 37 +++++++++++++++++ src/main/ipc/parcel-watcher-process.ts | 19 +-------- 3 files changed, 78 insertions(+), 18 deletions(-) create mode 100644 src/main/ipc/parcel-watcher-entry-path.test.ts create mode 100644 src/main/ipc/parcel-watcher-entry-path.ts diff --git a/src/main/ipc/parcel-watcher-entry-path.test.ts b/src/main/ipc/parcel-watcher-entry-path.test.ts new file mode 100644 index 00000000000..f8b59becc78 --- /dev/null +++ b/src/main/ipc/parcel-watcher-entry-path.test.ts @@ -0,0 +1,40 @@ +import path from 'node:path' +import { describe, expect, it } from 'vitest' +import { resolveWatcherProcessEntryPath } from './parcel-watcher-entry-path' + +describe('resolveWatcherProcessEntryPath', () => { + it('uses an adjacent entry when electron-vite appPath is already out/main', () => { + const builtMainPath = path.join(process.cwd(), 'out', 'main') + const adjacentEntry = path.join(builtMainPath, 'parcel-watcher-process-entry.js') + + expect( + resolveWatcherProcessEntryPath( + builtMainPath, + false, + (candidate) => candidate === adjacentEntry + ) + ).toBe(adjacentEntry) + }) + + it('uses the nested build entry when appPath is the project root', () => { + expect(resolveWatcherProcessEntryPath(process.cwd(), false, () => false)).toBe( + path.join(process.cwd(), 'out', 'main', 'parcel-watcher-process-entry.js') + ) + }) + + it('uses the unpacked nested entry for packaged apps', () => { + const appPath = path.join('C:', 'Orca', 'resources', 'app.asar') + + expect(resolveWatcherProcessEntryPath(appPath, true, () => true)).toBe( + path.join( + 'C:', + 'Orca', + 'resources', + 'app.asar.unpacked', + 'out', + 'main', + 'parcel-watcher-process-entry.js' + ) + ) + }) +}) diff --git a/src/main/ipc/parcel-watcher-entry-path.ts b/src/main/ipc/parcel-watcher-entry-path.ts new file mode 100644 index 00000000000..90133146715 --- /dev/null +++ b/src/main/ipc/parcel-watcher-entry-path.ts @@ -0,0 +1,37 @@ +import { existsSync } from 'node:fs' +import { join } from 'node:path' + +type ElectronAppPath = { getAppPath(): string; isPackaged: boolean } + +function loadElectronApp(): ElectronAppPath | null { + try { + return require('electron').app ?? null + } catch { + return null + } +} + +export function resolveWatcherProcessEntryPath( + appPath: string, + isPackaged: boolean, + pathExists: (candidate: string) => boolean = existsSync +): string { + // Why: ELECTRON_RUN_AS_NODE bypasses Electron's asar integration, so the + // packaged entry must be forked from app.asar.unpacked. + const basePath = isPackaged ? appPath.replace('app.asar', 'app.asar.unpacked') : appPath + const adjacentBuildEntry = join(basePath, 'parcel-watcher-process-entry.js') + // Why: electron-vite's unpackaged appPath is already out/main. Appending + // out/main again silently disables crash isolation in dev and E2E builds. + if (!isPackaged && pathExists(adjacentBuildEntry)) { + return adjacentBuildEntry + } + return join(basePath, 'out', 'main', 'parcel-watcher-process-entry.js') +} + +export function getWatcherProcessEntryPath(): string { + const app = loadElectronApp() + return resolveWatcherProcessEntryPath( + app?.getAppPath() ?? process.cwd(), + app?.isPackaged === true + ) +} diff --git a/src/main/ipc/parcel-watcher-process.ts b/src/main/ipc/parcel-watcher-process.ts index 58ad6097011..72e2ccf58ce 100644 --- a/src/main/ipc/parcel-watcher-process.ts +++ b/src/main/ipc/parcel-watcher-process.ts @@ -6,8 +6,8 @@ // they can refresh state that changed during the gap. import { fork, type ChildProcess } from 'node:child_process' import { existsSync } from 'node:fs' -import { join } from 'node:path' import type * as ParcelWatcher from '@parcel/watcher' +import { getWatcherProcessEntryPath } from './parcel-watcher-entry-path' import type { HostToWatcherMessage, WatcherProcessEvent, @@ -42,23 +42,6 @@ let loggedInProcessFallback = false const records = new Map() const pendingUnsubscribes = new Map void>() -function loadElectronApp(): { getAppPath(): string; isPackaged: boolean } | null { - try { - return require('electron').app ?? null - } catch { - return null - } -} - -function getWatcherProcessEntryPath(): string { - const app = loadElectronApp() - const appPath = app?.getAppPath() ?? process.cwd() - // Why: ELECTRON_RUN_AS_NODE bypasses Electron's asar integration, so the - // packaged entry must be forked from app.asar.unpacked. - const basePath = app?.isPackaged ? appPath.replace('app.asar', 'app.asar.unpacked') : appPath - return join(basePath, 'out', 'main', 'parcel-watcher-process-entry.js') -} - function shouldRunInProcess(entryPath: string): boolean { // Why: vitest suites mock '@parcel/watcher' at the module level; a forked // child would bypass those mocks (and could execute a stale build output),