fix(watcher): preserve isolation in electron-vite builds (#8162)

This commit is contained in:
OrcaWin
2026-07-10 15:10:15 -07:00
committed by GitHub
parent 473313fca6
commit b93a7eece1
3 changed files with 78 additions and 18 deletions
@@ -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'
)
)
})
})
+37
View File
@@ -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
)
}
+1 -18
View File
@@ -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<number, SubscriptionRecord>()
const pendingUnsubscribes = new Map<number, () => 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),