Skip to content

Commit a6e7323

Browse files
yi-geclaude
andcommitted
fix: stabilize Vite 8/Rolldown beta and merge pending fixes
Fork release as @desirecore/electron-vite (v6.0.0-beta.2). Fixes a batch of Vite 8 / Rolldown regressions and Electron 42 support, and merges several long-pending upstream fixes. - fix(esmShim): AST-based CJS shim injection at offset 0, no longer corrupts large Vite 8/Rolldown ESM bundles (alex8088#906) - fix(swc): also set `oxc: false` to disable Vite 8 default transform (alex8088#916) - fix(config): do not throw on non-plain objects when isolating config (alex8088#902) - fix(renderer): re-root when HTML entry is outside root, avoid illegal `../` emit paths under Rolldown (alex8088#915) - fix(bytecode): compile via vm.compileFunction in the consuming process type, fixing cachedDataRejected on Electron 42 / V8 14.8 (alex8088#911, alex8088#912) - fix(bytecode): remove non-deterministic compile race (alex8088#910) - fix(bytecode): correct renderer preset plugin name; locate the compiler script relative to this module so it survives package renames - fix(modulePath): inject modulePathPlugin into the sub-build for nested `?modulePath` and re-exports (alex8088#860, alex8088#862) - fix(dev): forward signals to the Electron child and escalate to SIGKILL on restart, fixing zombie processes (alex8088#899, alex8088#734, alex8088#353) - feat: support Electron 42 targets and lazy electron install (alex8088#904) - feat(bytecode): allow RegExp for chunkAlias; protect string concat (alex8088#885) - feat(cli): add --noElectronStart (alex8088#578) and --electronPackage (alex8088#867) - feat: export workerPlugin (alex8088#480) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 31965d2 commit a6e7323

16 files changed

Lines changed: 536 additions & 144 deletions

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,25 @@
1+
### v6.0.0-beta.2 (_2026-06-27_)
2+
3+
> Fork release published as `@desirecore/electron-vite` (CLI command stays `electron-vite`). Stabilizes the Vite 8 / Rolldown beta and merges a batch of long-pending upstream fixes. Issue links reference the upstream `alex8088/electron-vite` tracker.
4+
5+
- fix(esmShim): inject the CommonJS shim via AST detection at offset 0 to stop corrupting large Vite 8/Rolldown ESM bundles ([#906](https://github.com/alex8088/electron-vite/issues/906))
6+
- fix(swc): also set `oxc: false` so `swcPlugin` disables Vite 8's default Oxc transform ([#916](https://github.com/alex8088/electron-vite/issues/916))
7+
- fix(config): no longer throw on non-plain objects (plugin/class instances) when isolating sub-build config ([#902](https://github.com/alex8088/electron-vite/issues/902))
8+
- fix(renderer): re-root renderer build when the HTML entry lives outside `root`, avoiding illegal `../` emit paths under Rolldown ([#915](https://github.com/alex8088/electron-vite/issues/915))
9+
- fix(bytecode): compile via `vm.compileFunction` inside the consuming process type to fix `cachedDataRejected` crashes on Electron 42 / V8 14.8 ([#911](https://github.com/alex8088/electron-vite/issues/911), [#912](https://github.com/alex8088/electron-vite/pull/912))
10+
- fix(bytecode): remove a non-deterministic compile race that intermittently produced bad bundles ([#910](https://github.com/alex8088/electron-vite/issues/910))
11+
- fix(bytecode): correct the renderer preset plugin name so the renderer guard actually applies
12+
- fix(modulePath): inject `modulePathPlugin` into the sub-build so nested `?modulePath` and re-exports resolve ([#860](https://github.com/alex8088/electron-vite/issues/860), [#862](https://github.com/alex8088/electron-vite/issues/862))
13+
- fix(dev): forward `SIGINT`/`SIGTERM`/`SIGHUP` to the Electron child and escalate to `SIGKILL` on restart, fixing zombie Electron processes ([#899](https://github.com/alex8088/electron-vite/issues/899), [#734](https://github.com/alex8088/electron-vite/issues/734), [#353](https://github.com/alex8088/electron-vite/issues/353))
14+
- feat: support Electron 42 build targets and lazily run `electron/install.js` when the binary is missing on Electron ≥ 42 ([#904](https://github.com/alex8088/electron-vite/issues/904))
15+
- feat(bytecode): allow `RegExp` (and mixed arrays) for the `chunkAlias` option ([#885](https://github.com/alex8088/electron-vite/pull/885))
16+
- feat(bytecode): protect static string-concatenation expressions
17+
- feat(cli): add `--noElectronStart` to run dev servers without launching Electron ([#578](https://github.com/alex8088/electron-vite/pull/578))
18+
- feat(cli): add `--electronPackage <name>` to support a custom Electron package name ([#867](https://github.com/alex8088/electron-vite/pull/867))
19+
- feat: export `workerPlugin` ([#480](https://github.com/alex8088/electron-vite/pull/480))
20+
21+
> Note: with `build.bytecode`, compilation now launches a real Electron process. On Linux without a display, run under `xvfb-run`.
22+
123
### v6.0.0-beta.0 (_2026-04-12_)
224

325
- refactor!: simplify resolve config and isolate user config for sub-builds

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@
4141

4242
### Install
4343

44+
> This is a community fork published as [`@desirecore/electron-vite`](https://www.npmjs.com/package/@desirecore/electron-vite). The CLI command stays `electron-vite`.
45+
4446
```sh
45-
npm i electron-vite -D
47+
npm i @desirecore/electron-vite -D
4648
```
4749

4850
### Development & Build

bin/electron-bytecode-preload.cjs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Bytecode compiler preload. Runs in a renderer process (sandbox:false) so the produced
2+
// V8 code cache matches the isolate (snapshot + flag hash) of the app's runtime preload,
3+
// which the browser/main process cache would not on Electron 42+ (V8 14.8). The compile
4+
// result (or error) is reported back over IPC. See bin/electron-bytecode.cjs.
5+
const fs = require('fs')
6+
const vm = require('vm')
7+
const v8 = require('v8')
8+
const { ipcRenderer } = require('electron')
9+
10+
v8.setFlagsFromString('--no-lazy')
11+
v8.setFlagsFromString('--no-flush-bytecode')
12+
13+
const COMPILE_PARAMS = ['exports', 'require', 'module', '__filename', '__dirname']
14+
15+
let error = ''
16+
try {
17+
const code = fs.readFileSync(process.env.ELECTRON_VITE_BYTECODE_IN, 'utf-8')
18+
const fn = vm.compileFunction(code, COMPILE_PARAMS, { produceCachedData: true })
19+
fs.writeFileSync(process.env.ELECTRON_VITE_BYTECODE_OUT, fn.cachedData)
20+
} catch (err) {
21+
error = String(err && err.stack ? err.stack : err)
22+
}
23+
24+
ipcRenderer.send('electron-vite:bytecode-done', error)

bin/electron-bytecode.cjs

Lines changed: 64 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,74 @@
1+
const { app } = require('electron')
12
const vm = require('vm')
23
const v8 = require('v8')
3-
const wrap = require('module').wrap
4+
const fs = require('fs')
5+
const path = require('path')
46

57
v8.setFlagsFromString('--no-lazy')
68
v8.setFlagsFromString('--no-flush-bytecode')
79

8-
let code = ''
10+
// Compile a chunk to a V8 code cache inside the SAME process type that will load it.
11+
// On Electron 42+ (V8 14.8) the cache is bound to a per-process-type snapshot/isolate
12+
// checksum (header @16) and a flag hash, so a cache produced in the wrong process type is
13+
// rejected (and force-patching the header corrupts execution). The compiler is therefore
14+
// launched as a real Electron app (never ELECTRON_RUN_AS_NODE, whose isolate matches
15+
// neither the main nor the renderer process):
16+
// - main / node chunks -> compiled here, in the browser (main) process.
17+
// - preload chunks -> ELECTRON_VITE_BYTECODE_RENDERER=1: compiled in a renderer
18+
// isolate by the sandbox:false preload of a hidden window.
19+
// Input/output go through temp files (a GUI process has no usable stdio pipe).
20+
const COMPILE_PARAMS = ['exports', 'require', 'module', '__filename', '__dirname']
21+
const inFile = process.env.ELECTRON_VITE_BYTECODE_IN
22+
const outFile = process.env.ELECTRON_VITE_BYTECODE_OUT
923

10-
process.stdin.setEncoding('utf-8')
24+
const fail = error => {
25+
process.stderr.write(`${error && error.stack ? error.stack : error}\n`)
26+
process.exitCode = 1
27+
}
1128

12-
process.stdin.on('readable', () => {
13-
const data = process.stdin.read()
14-
if (data !== null) {
15-
code += data
16-
}
17-
})
29+
// Headless: no GPU, no visible window.
30+
app.disableHardwareAcceleration()
1831

19-
process.stdin.on('end', () => {
20-
try {
21-
if (typeof code !== 'string') {
22-
throw new Error(`javascript code must be string. ${typeof code} was given.`)
32+
if (process.env.ELECTRON_VITE_BYTECODE_RENDERER) {
33+
const { BrowserWindow, ipcMain } = require('electron')
34+
app.whenReady().then(() => {
35+
// Guard against a renderer that never reports back, so the build can't hang forever.
36+
const timer = setTimeout(() => {
37+
fail(new Error('bytecode compilation timed out'))
38+
app.quit()
39+
}, 60000)
40+
ipcMain.once('electron-vite:bytecode-done', (_event, error) => {
41+
clearTimeout(timer)
42+
if (error) {
43+
fail(error)
44+
}
45+
app.quit()
46+
})
47+
try {
48+
const win = new BrowserWindow({
49+
show: false,
50+
webPreferences: {
51+
preload: path.join(__dirname, 'electron-bytecode-preload.cjs'),
52+
sandbox: false,
53+
contextIsolation: true
54+
}
55+
})
56+
win.loadURL('data:text/html,<!doctype html><html></html>')
57+
} catch (error) {
58+
clearTimeout(timer)
59+
fail(error)
60+
app.quit()
2361
}
24-
25-
const script = new vm.Script(wrap(code), { produceCachedData: true })
26-
const bytecodeBuffer = script.createCachedData()
27-
28-
process.stdout.write(bytecodeBuffer)
29-
} catch (error) {
30-
console.error(error)
31-
}
32-
})
62+
})
63+
} else {
64+
app.whenReady().then(() => {
65+
try {
66+
const code = fs.readFileSync(inFile, 'utf-8')
67+
const fn = vm.compileFunction(code, COMPILE_PARAMS, { produceCachedData: true })
68+
fs.writeFileSync(outFile, fn.cachedData)
69+
} catch (error) {
70+
fail(error)
71+
}
72+
app.quit()
73+
})
74+
}

package.json

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"name": "electron-vite",
3-
"version": "6.0.0-beta.1",
2+
"name": "@desirecore/electron-vite",
3+
"version": "6.0.0-beta.2",
44
"description": "Electron build tooling based on Vite",
55
"type": "module",
66
"main": "./dist/index.js",
@@ -28,12 +28,15 @@
2828
"license": "MIT",
2929
"repository": {
3030
"type": "git",
31-
"url": "git+https://github.com/alex8088/electron-vite.git"
31+
"url": "git+https://github.com/desirecore/electron-vite.git"
3232
},
3333
"bugs": {
34-
"url": "https://github.com/alex8088/electron-vite/issues"
34+
"url": "https://github.com/desirecore/electron-vite/issues"
35+
},
36+
"homepage": "https://github.com/desirecore/electron-vite",
37+
"publishConfig": {
38+
"access": "public"
3539
},
36-
"homepage": "https://electron-vite.org",
3740
"keywords": [
3841
"electron",
3942
"vite",

src/cli.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ interface GlobalCLIOptions {
2626
watch?: boolean
2727
outDir?: string
2828
entry?: string
29+
electronPackage?: string
2930
}
3031

3132
interface DevCLIOptions {
@@ -34,6 +35,7 @@ interface DevCLIOptions {
3435
remoteDebuggingPort?: string
3536
noSandbox?: boolean
3637
rendererOnly?: boolean
38+
noElectronStart?: boolean
3739
}
3840

3941
interface PreviewCLIOptions {
@@ -68,6 +70,7 @@ cli
6870
.option('--sourcemap', `[boolean] output source maps for debug (default: false)`)
6971
.option('--outDir <dir>', `[string] output directory (default: out)`)
7072
.option('--entry <file>', `[string] specify electron entry file`)
73+
.option('--electronPackage <name>', `[string] specify electron package name (default: electron)`)
7174

7275
// dev
7376
cli
@@ -80,6 +83,7 @@ cli
8083
.option('--remoteDebuggingPort <port>', `[string] port for remote debugging`)
8184
.option('--noSandbox', `[boolean] forces renderer process to run un-sandboxed`)
8285
.option('--rendererOnly', `[boolean] only dev server for the renderer`)
86+
.option('--noElectronStart', `[boolean] run dev servers without starting the electron app`)
8387
.action(async (root: string, options: DevCLIOptions & GlobalCLIOptions) => {
8488
if (options.remoteDebuggingPort) {
8589
process.env.REMOTE_DEBUGGING_PORT = options.remoteDebuggingPort
@@ -105,11 +109,18 @@ cli
105109
process.env.ELECTRON_ENTRY = options.entry
106110
}
107111

112+
if (options.electronPackage) {
113+
process.env.ELECTRON_PKG_NAME = options.electronPackage
114+
}
115+
108116
const { createServer } = await import('./server')
109117
const inlineConfig = createInlineConfig(root, options)
110118

111119
try {
112-
await createServer(inlineConfig, { rendererOnly: options.rendererOnly })
120+
await createServer(inlineConfig, {
121+
rendererOnly: options.rendererOnly,
122+
noElectronStart: options.noElectronStart
123+
})
113124
} catch (e) {
114125
const error = e as Error
115126
createLogger(options.logLevel).error(
@@ -129,6 +140,10 @@ cli.command('build [root]', 'build for production').action(async (root: string,
129140
process.env.ELECTRON_ENTRY = options.entry
130141
}
131142

143+
if (options.electronPackage) {
144+
process.env.ELECTRON_PKG_NAME = options.electronPackage
145+
}
146+
132147
try {
133148
await build(inlineConfig)
134149
} catch (e) {
@@ -155,6 +170,10 @@ cli
155170
process.env.ELECTRON_ENTRY = options.entry
156171
}
157172

173+
if (options.electronPackage) {
174+
process.env.ELECTRON_PKG_NAME = options.electronPackage
175+
}
176+
158177
if (options['--']) {
159178
process.env.ELECTRON_CLI_ARGS = JSON.stringify(options['--'])
160179
}

src/electron.ts

Lines changed: 72 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import path from 'node:path'
22
import fs from 'node:fs'
33
import { createRequire } from 'node:module'
4-
import { type ChildProcess, spawn } from 'node:child_process'
4+
import { type ChildProcess, spawn, spawnSync } from 'node:child_process'
5+
import colors from 'picocolors'
6+
import { createLogger } from 'vite'
57
import { loadPackageData } from './utils'
68

79
const _require = createRequire(import.meta.url)
810

11+
// The Electron package name, defaults to 'electron'. A custom build (e.g.
12+
// '@overwolf/ow-electron') can be selected via the ELECTRON_PKG_NAME env var,
13+
// which is set from the `--electronPackage` CLI option.
14+
const getElectronPackageName = (): string => process.env.ELECTRON_PKG_NAME || 'electron'
15+
916
const ensureElectronEntryFile = (root = process.cwd()): void => {
1017
if (process.env.ELECTRON_ENTRY) return
1118
const pkg = loadPackageData()
@@ -26,7 +33,7 @@ const ensureElectronEntryFile = (root = process.cwd()): void => {
2633
const getElectronMajorVer = (): string => {
2734
let majorVer = process.env.ELECTRON_MAJOR_VER || ''
2835
if (!majorVer) {
29-
const pkg = _require.resolve('electron/package.json')
36+
const pkg = _require.resolve(`${getElectronPackageName()}/package.json`)
3037
if (fs.existsSync(pkg)) {
3138
const version = _require(pkg).version
3239
majorVer = version.split('.')[0]
@@ -46,20 +53,41 @@ export function supportImportMetaPaths(): boolean {
4653
return parseInt(majorVer) >= 30
4754
}
4855

56+
// Electron 42+ no longer downloads its binary via a `postinstall` script
57+
// (supply-chain hardening). Instead the binary is fetched on first launch by
58+
// running the package's own `install.js`, mirroring what `npx electron` does.
59+
function installElectronBinary(electronModulePath: string): void {
60+
const installScript = path.join(electronModulePath, 'install.js')
61+
if (!fs.existsSync(installScript)) {
62+
throw new Error(`Electron install script not found at "${installScript}", please reinstall the electron package`)
63+
}
64+
createLogger().info(colors.green('Electron binary not found, downloading...'))
65+
const result = spawnSync(process.execPath, [installScript], { stdio: 'inherit', cwd: electronModulePath })
66+
if (result.status !== 0) {
67+
throw new Error('Failed to download the Electron binary')
68+
}
69+
}
70+
4971
export function getElectronPath(): string {
5072
let electronExecPath = process.env.ELECTRON_EXEC_PATH || ''
5173
if (!electronExecPath) {
52-
const electronModulePath = path.dirname(_require.resolve('electron'))
74+
const electronPkgName = getElectronPackageName()
75+
const electronModulePath = path.dirname(_require.resolve(electronPkgName))
5376
const pathFile = path.join(electronModulePath, 'path.txt')
54-
let executablePath
55-
if (fs.existsSync(pathFile)) {
56-
executablePath = fs.readFileSync(pathFile, 'utf-8')
77+
const readExecutablePath = (): string | undefined =>
78+
fs.existsSync(pathFile) ? fs.readFileSync(pathFile, 'utf-8') : undefined
79+
let executablePath = readExecutablePath()
80+
if (!executablePath && parseInt(getElectronMajorVer()) >= 42) {
81+
installElectronBinary(electronModulePath)
82+
executablePath = readExecutablePath()
5783
}
5884
if (executablePath) {
5985
electronExecPath = path.join(electronModulePath, 'dist', executablePath)
6086
process.env.ELECTRON_EXEC_PATH = electronExecPath
6187
} else {
62-
throw new Error('Electron uninstall')
88+
throw new Error(
89+
`The Electron package "${electronPkgName}" is not installed correctly, please reinstall it and try again`
90+
)
6391
}
6492
}
6593
return electronExecPath
@@ -69,6 +97,7 @@ export function getElectronNodeTarget(): string {
6997
const electronVer = getElectronMajorVer()
7098

7199
const nodeVer = {
100+
'42': '24.15',
72101
'41': '24.14',
73102
'40': '24.14',
74103
'39': '22.20',
@@ -102,6 +131,7 @@ export function getElectronChromeTarget(): string {
102131
const electronVer = getElectronMajorVer()
103132

104133
const chromeVer = {
134+
'42': '148',
105135
'41': '146',
106136
'40': '144',
107137
'39': '142',
@@ -159,7 +189,41 @@ export function startElectron(root: string | undefined): ChildProcess {
159189
const entry = process.env.ELECTRON_ENTRY || '.'
160190

161191
const ps = spawn(electronPath, [entry].concat(args), { stdio: 'inherit' })
162-
ps.on('close', process.exit)
192+
193+
currentElectronPs = ps
194+
installSignalForwarding()
195+
196+
ps.on('close', (code, signal) => {
197+
// A hot reload replaces the child via a new startElectron call. Only the
198+
// active child should terminate the parent; otherwise restarting would
199+
// bring down the dev server.
200+
if (currentElectronPs === ps) {
201+
process.exit(code ?? (signal ? 1 : 0))
202+
}
203+
})
163204

164205
return ps
165206
}
207+
208+
// The currently running Electron child. Updated on every (re)start so a single
209+
// set of signal handlers always targets the live process.
210+
let currentElectronPs: ChildProcess | undefined
211+
let signalHandlersInstalled = false
212+
213+
// Forward termination signals to the Electron child instead of letting the
214+
// parent die immediately, matching electron's own CLI wrapper (electron/cli.js).
215+
// This lets the app run its quit handlers and prevents orphaned/zombie Electron
216+
// processes on Ctrl-C or when a parent task runner stops us. Handlers are
217+
// installed once to avoid leaking across hot-reload restarts.
218+
function installSignalForwarding(): void {
219+
if (signalHandlersInstalled) return
220+
signalHandlersInstalled = true
221+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP'] as const) {
222+
process.on(signal, () => {
223+
const ps = currentElectronPs
224+
if (ps && ps.exitCode === null && !ps.killed) {
225+
ps.kill(signal)
226+
}
227+
})
228+
}
229+
}

0 commit comments

Comments
 (0)