Compare commits

...

21 Commits

Author SHA1 Message Date
alex8088
23d7bfc8ba release: v6.0.0 2026-10-15 00:01:19 +08:00
alex8088
2ca5e40433 release: v6.0.0-beta.8 2026-10-11 02:23:37 +08:00
alex8088
bffc6c95df perf(swcPlugin): skip redundant minification when using Oxc (#922) 2026-10-11 02:13:14 +08:00
alex8088
b0b33bdd5e fix(asset): use consistent file paths for asset cache lookups 2026-10-11 02:07:32 +08:00
alex8088
3ec79bbf65 perf: skip Babel loading in development and builds without bytecode 2026-10-11 02:01:18 +08:00
alex8088
19050aaec9 refactor: share and normalize build output resolution 2026-10-11 00:59:44 +08:00
alex8088
0655933704 refactor: inline bytecode build plugin name 2026-10-11 00:37:36 +08:00
alex8088
6fc103eb5b perf: short-circuit bytecode detection in builds 2026-10-10 23:00:29 +08:00
alex8088
6b0f85a2a7 chore: simplify bytecode compiler initialization 2026-10-10 02:03:25 +08:00
alex8088
fa199eb372 release: v6.0.0-beta.7 2026-10-08 02:30:56 +08:00
alex8088
b66840e2ec fix(bytecode): unable to obtain the bytecode build session in Node.js 24 (#928) 2026-10-08 02:30:44 +08:00
alex8088
1cbef3fb80 release: v6.0.0-beta.6 2026-10-07 13:41:25 +08:00
alex8088
36a0147a6d refactor!: invoke the bytecode compiler only once to compile all code 2026-10-07 13:36:10 +08:00
alex8088
4c4bdf5248 refactor: use pipe stdio for spawn Electron bytecode compliler and append stderr error 2026-10-01 22:44:18 +08:00
alex8088
395f0dae0c chore: improve jsdoc for bytecode options (#898) 2026-10-01 22:31:29 +08:00
alex8088
22b12bd102 release: v6.0.0-beta.5 2026-09-29 23:47:02 +08:00
alex8088
c104b2d15b fix(bytecode): set the regular expression's lastIndex to 0 to perform a global match for chunkAlias option 2026-09-29 23:45:48 +08:00
Beans
c047b58d5d
feat(bytecode): support RegExp for chunkAlias option (#885)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-09-29 23:25:21 +08:00
alex8088
55239df0ee fix(dev): improve Electron process signal handling 2026-09-29 23:23:03 +08:00
Iddo Gino
d8e7702dc5
fix(dev): forward termination signals to Electron child (#900)
Spawn the Electron binary directly in dev/preview mode but forward
SIGINT/SIGTERM/SIGHUP from the electron-vite parent to the child,
matching Electron's own CLI wrapper (node_modules/electron/cli.js).

Without this, Ctrl-C sends SIGINT to the foreground process group once:
the Node parent exits immediately (no handler) and the Electron child
gets only a single SIGINT. On macOS, an Electron app whose before-quit
handler calls preventDefault() and later calls app.quit() can stall in
AppKit's event loop in that path. Under a direct 'electron .' launch
the CLI shim forwards SIGINT as well, so Electron gets it twice and
exits cleanly. Matching that behavior here fixes the hang.

Fixes #899
2026-09-29 11:55:31 +08:00
Praveens-Pepper-AI
816c6dac61
fix(dev): exit non-zero when Electron is killed by a signal (#918)
`ChildProcess`'s `close` event fires as `(code, signal)` and `code` is null when the
child was terminated by a signal. `process.exit` ignores its second argument and
coerces null to 0, so passing the listener directly reports every signal death as a
successful exit.

Handle the two arguments explicitly so a signal death exits non-zero.
2026-09-29 10:33:35 +08:00
12 changed files with 332 additions and 228 deletions

View File

@ -1,45 +1,85 @@
### v6.0.0-beta.4 (_2026-09-28_) ### v6.0.0 (_2026-10-15_)
- fix(esmShim)!: use `this.parse` to find the last static import ([#906](https://github.com/alex8088/electron-vite/issues/906))
### v6.0.0-beta.3 (_2026-09-27_)
- fix(deps)!: downgraded @babel/* to v7 due to Node requirements
### v6.0.0-beta.2 (_2026-09-27_)
- feat(bytecode): support RegExp for chunkAlias option ([#885](https://github.com/alex8088/electron-vite/issues/885))
- refactor: share and normalize build output resolution
- refactor: inline bytecode build plugin name
- refactor!: invoke the bytecode compiler only once to compile all code
- refactor: use pipe stdio for spawn Electron bytecode compliler and append stderr error
- refactor: use rolldown instead of esbuild to bundle config file - refactor: use rolldown instead of esbuild to bundle config file
- refactor: compile bytecode using the Electron main process or renderer process rather then via the `ELECTRON_RUN_AS_NODE` - refactor: compile bytecode using the Electron main process or renderer process rather then via the `ELECTRON_RUN_AS_NODE`
- refactor!: simplify resolve config and isolate user config for sub-builds
- refactor: `?modulePath` sub-build output strategy
- perf(swcPlugin): skip redundant minification when using Oxc ([#922](https://github.com/alex8088/electron-vite/issues/922))
- perf: skip Babel loading in development and builds without bytecode
- perf: short-circuit bytecode detection in builds
- perf: setup rollupOption compatibility via config factory - perf: setup rollupOption compatibility via config factory
- perf: build compatibility target for Electron 42, 43, 44 - perf: build compatibility target for Electron 42, 43, 44
- fix(asset): use consistent file paths for asset cache lookups
- fix(bytecode): unable to obtain the bytecode build session in Node.js 24 ([#928](https://github.com/alex8088/electron-vite/issues/928))
- fix(dev): exit non-zero when Electron is killed by a signal ([#918](https://github.com/alex8088/electron-vite/issues/918))
- fix(dev): forward termination signals to Electron child ([#900](https://github.com/alex8088/electron-vite/issues/900))
- fix(dev): improve Electron process signal handling
- fix(bytecode): set the regular expression's `lastIndex` to 0 to perform a global match for chunkAlias option
- fix(esmShim)!: use `this.parse` to find the last static import ([#906](https://github.com/alex8088/electron-vite/issues/906))
- fix(deps)!: downgraded @babel/* to v7 due to Node requirements
- fix: remove useless preset config - fix: remove useless preset config
- fix(swcPlugin): also disable oxc on Vite 8 ([#920](https://github.com/alex8088/electron-vite/issues/920)) - fix(swcPlugin): also disable oxc on Vite 8 ([#920](https://github.com/alex8088/electron-vite/issues/920))
- fix: clone config without plugins [#902](https://github.com/alex8088/electron-vite/issues/902) - fix: clone config without plugins [#902](https://github.com/alex8088/electron-vite/issues/902)
- fix(deps)!: update magic-string to v1 - fix(deps)!: update magic-string to v1
- fix(deps)!: update @babel/* to v8 - fix(deps)!: update @babel/* to v8
- chore(deps): update all non-major dependencies
- chore(deps): update lint-staged to v17
### v6.0.0-beta.1 (_2026-04-12_)
- refactor!: simplify resolve config and isolate user config for sub-builds
- fix!: compatible with rollupOptions and rolldownOptions - fix!: compatible with rollupOptions and rolldownOptions
- fix: chunk filename is wrong in ES mode - fix: chunk filename is wrong in ES mode
### v6.0.0-beta.0 (_2026-04-09_)
- refactor: `?modulePath` sub-build output strategy
- fix(asset): isolate asset caches per environment - fix(asset): isolate asset caches per environment
- fix: use the environment api in plugins - fix: use the environment api in plugins
- fix: skip TTY terminal manipulations in CI environments - fix: skip TTY terminal manipulations in CI environments
- fix(deps)!: update vite to v8 ([#894](https://github.com/alex8088/electron-vite/issues/894)) - fix(deps)!: update vite to v8 ([#894](https://github.com/alex8088/electron-vite/issues/894))
- fix: Typescript compiler option 'outDir' must be located inside Rollup 'dir' option - fix: Typescript compiler option 'outDir' must be located inside Rollup 'dir' option
- fix(isolateEntries): handle non-TTY stdout in build reporter (#887) - fix(isolateEntries): handle non-TTY stdout in build reporter (#887)
- chore: simplify bytecode compiler initialization
- chore: improve jsdoc for bytecode options ([#898](https://github.com/alex8088/electron-vite/issues/898))
- chore(deps): update all non-major dependencies
- chore(deps): update lint-staged to v17
- chore(deps): update cac to v7 - chore(deps): update cac to v7
- chore(deps): update globals to v17 - chore(deps): update globals to v17
- chore(deps): update eslint and @eslint/js to v10 - chore(deps): update eslint and @eslint/js to v10
- chore(deps): update all non-major dependencies - chore(deps): update all non-major dependencies
### v6.0.0-beta.8 (_2026-10-11_)
See [v6.0.0-beta.8 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.8/CHANGELOG.md)
### v6.0.0-beta.7 (_2026-10-08_)
See [v6.0.0-beta.7 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.7/CHANGELOG.md)
### v6.0.0-beta.6 (_2026-10-07_)
See [v6.0.0-beta.6 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.6/CHANGELOG.md)
### v6.0.0-beta.5 (_2026-09-29_)
See [v6.0.0-beta.5 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.5/CHANGELOG.md)
### v6.0.0-beta.4 (_2026-09-28_)
See [v6.0.0-beta.4 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.4/CHANGELOG.md)
### v6.0.0-beta.3 (_2026-09-27_)
See [v6.0.0-beta.3 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.3/CHANGELOG.md)
### v6.0.0-beta.2 (_2026-09-27_)
See [v6.0.0-beta.2 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.2/CHANGELOG.md)
### v6.0.0-beta.1 (_2026-04-12_)
See [v6.0.0-beta.1 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.1/CHANGELOG.md)
### v6.0.0-beta.0 (_2026-04-09_)
See [v6.0.0-beta.0 changelog](https://github.com/alex8088/electron-vite/blob/v6.0.0-beta.0/CHANGELOG.md)
### v5.0.0 (_2025-12-07_) ### v5.0.0 (_2025-12-07_)
- feat(config): add `build.externalizeDeps` and `build.bytecode` config options to replace `externalizeDepsPlugin` and `bytecodePlugin` - feat(config): add `build.externalizeDeps` and `build.bytecode` config options to replace `externalizeDepsPlugin` and `bytecodePlugin`

View File

@ -11,6 +11,7 @@ const channel = 'electron-vite:bytecode:rpc'
const readyChannel = 'electron-vite:bytecode:ready' const readyChannel = 'electron-vite:bytecode:ready'
let win let win
let rendererRPC let rendererRPC
let rendererStarting
let stopping = false let stopping = false
function fail(error) { function fail(error) {
@ -74,17 +75,18 @@ function createRenderer() {
}) })
} }
// Register readiness immediately, but only resolve it after the selected backend is usable. const initialized = app.whenReady().then(() => true)
const initialized = app.whenReady().then(async () => {
if (process.env.ELECTRON_VITE_RENDERER === '1' || process.env.ELECTRON_VITE_RENDERER === 'true') {
await createRenderer()
parentRPC.register('compile', (...args) => rendererRPC.request('compile', ...args))
} else {
parentRPC.register('compile', compile)
}
return true
})
parentRPC.register('ready', () => initialized) parentRPC.register('ready', () => initialized)
parentRPC.register('compile', async (target, code) => {
await initialized
if (target === 'main') return compile(code)
if (target === 'preload') {
rendererStarting ||= createRenderer()
await rendererStarting
return rendererRPC.request('compile', code)
}
throw new Error(`Invalid bytecode target: ${target}`)
})
initialized.catch(fail) initialized.catch(fail)
process.on('message', message => { process.on('message', message => {

View File

@ -1,6 +1,6 @@
{ {
"name": "electron-vite", "name": "electron-vite",
"version": "6.0.0-beta.4", "version": "6.0.0",
"description": "Electron build tooling based on Vite", "description": "Electron build tooling based on Vite",
"type": "module", "type": "module",
"main": "./dist/index.js", "main": "./dist/index.js",

View File

@ -1,22 +1,40 @@
import { build as viteBuild } from 'vite' import { build as viteBuild, mergeConfig } from 'vite'
import { type InlineConfig, resolveConfig } from './config' import { type InlineConfig, type MainViteConfig, type PreloadViteConfig, resolveConfig } from './config'
import { BytecodeCompiler } from './bytecodeCompiler'
import { asyncFlatten } from './utils'
async function usesBytecode(config?: MainViteConfig | PreloadViteConfig): Promise<boolean> {
if (!config) return false
if (config.build?.bytecode) return true
const plugins = await asyncFlatten(config.plugins || [])
return plugins.some(plugin => plugin && typeof plugin === 'object' && plugin.name === 'vite:bytecode')
}
/** /**
* Bundles the electron app for production. * Bundles the electron app for production.
*/ */
export async function build(inlineConfig: InlineConfig = {}): Promise<void> { export async function build(inlineConfig: InlineConfig = {}): Promise<void> {
process.env.NODE_ENV_ELECTRON_VITE = 'production' process.env.NODE_ENV_ELECTRON_VITE = 'production'
const config = await resolveConfig(inlineConfig, 'build', 'production')
const bytecodeCompiler = new BytecodeCompiler()
const config = await resolveConfig(
mergeConfig(inlineConfig, {
plugins: [{ name: 'vite:electron-bytecode-build', api: { compiler: bytecodeCompiler } }]
}),
'build',
'production'
)
if (!config.config) { if (!config.config) {
return return
} }
// Build targets in order: main -> preload -> renderer const hasBytecode = (await usesBytecode(config.config.main)) || (await usesBytecode(config.config.preload))
const buildTargets = ['main', 'preload', 'renderer'] as const
for (const target of buildTargets) { const buildTargets = async (): Promise<void> => {
const viteConfig = config.config[target] // Build targets in order: main -> preload -> renderer
for (const target of ['main', 'preload', 'renderer'] as const) {
const viteConfig = config.config?.[target]
if (viteConfig) { if (viteConfig) {
// Disable watch mode in production builds // Disable watch mode in production builds
if (viteConfig.build?.watch) { if (viteConfig.build?.watch) {
@ -26,3 +44,13 @@ export async function build(inlineConfig: InlineConfig = {}): Promise<void> {
} }
} }
} }
if (!hasBytecode) return buildTargets()
try {
await bytecodeCompiler.start()
await buildTargets()
} finally {
await bytecodeCompiler.stop()
}
}

View File

@ -1,12 +1,11 @@
import { type ChildProcess, spawn } from 'node:child_process' import { type ChildProcess, spawn } from 'node:child_process'
import path from 'node:path'
import { createRequire } from 'node:module'
import { getElectronPath } from './electron'
export type ElectronCompilerOptions = { const require = createRequire(import.meta.url)
path: string const RPC_TIMEOUT = 30_000
args: string[] const SHUTDOWN_TIMEOUT = 3_000
env?: NodeJS.ProcessEnv
timeout?: number
shutdownTimeout?: number
}
export type BytecodeCompileResult = { export type BytecodeCompileResult = {
sourceLength: number sourceLength: number
@ -14,7 +13,9 @@ export type BytecodeCompileResult = {
cachedData: string cachedData: string
} }
type ElectronResponse = { export type BytecodeTarget = 'main' | 'preload'
type CompilerResponse = {
type: 'response' type: 'response'
invocationId: number invocationId: number
result?: unknown result?: unknown
@ -27,24 +28,17 @@ type PendingRequest = {
timer: ReturnType<typeof setTimeout> timer: ReturnType<typeof setTimeout>
} }
export class ElectronCompiler { export class BytecodeCompiler {
private ps?: ChildProcess private ps?: ChildProcess
private rpcMap = new Map<number, PendingRequest>() private rpcMap = new Map<number, PendingRequest>()
private invocationId = 0 private invocationId = 0
private starting?: Promise<void> private starting?: Promise<void>
private stopping?: Promise<void> private stopping?: Promise<void>
private ready = false private ready = false
private stderr = ''
constructor(readonly options: ElectronCompilerOptions) {
for (const timeout of [options.timeout, options.shutdownTimeout]) {
if (timeout !== undefined && (!Number.isFinite(timeout) || timeout <= 0)) {
throw new RangeError('ElectronCompiler timeouts must be positive finite numbers')
}
}
}
start(): Promise<void> { start(): Promise<void> {
if (this.stopping) return Promise.reject(new Error('ElectronCompiler is stopping')) if (this.stopping) return Promise.reject(new Error('BytecodeCompiler is stopping'))
if (this.starting) return this.starting if (this.starting) return this.starting
if (this.ready) return Promise.resolve() if (this.ready) return Promise.resolve()
if (this.ps) return Promise.reject(new Error('Previous Electron process has not closed; await stop() first')) if (this.ps) return Promise.reject(new Error('Previous Electron process has not closed; await stop() first'))
@ -55,15 +49,27 @@ export class ElectronCompiler {
} }
private async initialize(): Promise<void> { private async initialize(): Promise<void> {
const env: NodeJS.ProcessEnv = { ...process.env, ...this.options.env, ELECTRON_VITE_RPC: 'true' } const env: NodeJS.ProcessEnv = { ...process.env, ELECTRON_VITE_RPC: 'true' }
delete env.ELECTRON_RUN_AS_NODE delete env.ELECTRON_RUN_AS_NODE
const ps = spawn(this.options.path, this.options.args, { this.stderr = ''
stdio: ['ignore', 'inherit', 'inherit', 'ipc'], const compilerPath = path.join(
path.dirname(require.resolve('electron-vite/package.json')),
'bin',
'electron-bytecode-main.cjs'
)
const ps = spawn(getElectronPath(), [compilerPath], {
stdio: ['ignore', 'ignore', 'pipe', 'ipc'],
windowsHide: true, windowsHide: true,
env env
}) })
this.ps = ps this.ps = ps
ps.on('message', (message: ElectronResponse) => { if (ps.stderr) {
ps.stderr.setEncoding('utf8')
ps.stderr.on('data', (chunk: string) => {
if (this.ps === ps) this.stderr = (this.stderr + chunk).slice(-64 * 1024)
})
}
ps.on('message', (message: CompilerResponse) => {
if (this.ps !== ps || !message || message.type !== 'response') return if (this.ps !== ps || !message || message.type !== 'response') return
const pending = this.rpcMap.get(message.invocationId) const pending = this.rpcMap.get(message.invocationId)
if (!pending) return if (!pending) return
@ -101,12 +107,12 @@ export class ElectronCompiler {
} }
} }
compile(code: string): Promise<BytecodeCompileResult> { compile(target: BytecodeTarget, code: string): Promise<BytecodeCompileResult> {
return this.request<BytecodeCompileResult>('compile', code) return this.request<BytecodeCompileResult>('compile', target, code)
} }
async request<T = unknown>(cmd: string, ...args: unknown[]): Promise<T> { async request<T = unknown>(cmd: string, ...args: unknown[]): Promise<T> {
if (!this.ready || this.stopping) throw new Error('ElectronCompiler is not ready; await start() first') if (!this.ready || this.stopping) throw new Error('BytecodeCompiler is not ready; await start() first')
return this.sendRequest(cmd, args) as Promise<T> return this.sendRequest(cmd, args) as Promise<T>
} }
@ -120,9 +126,9 @@ export class ElectronCompiler {
if (!pending) return if (!pending) return
clearTimeout(pending.timer) clearTimeout(pending.timer)
this.rpcMap.delete(invocationId) this.rpcMap.delete(invocationId)
reject(error) reject(this.withStderr(error))
} }
const timer = setTimeout(() => fail(new Error('Electron RPC timed out: ' + cmd)), this.options.timeout ?? 30000) const timer = setTimeout(() => fail(new Error('Electron RPC timed out: ' + cmd)), RPC_TIMEOUT)
this.rpcMap.set(invocationId, { resolve, reject, timer }) this.rpcMap.set(invocationId, { resolve, reject, timer })
try { try {
ps.send({ type: 'request', invocationId, cmd, args }, error => { ps.send({ type: 'request', invocationId, cmd, args }, error => {
@ -136,6 +142,7 @@ export class ElectronCompiler {
private rejectPending(error: Error): void { private rejectPending(error: Error): void {
this.ready = false this.ready = false
if (this.rpcMap.size) this.withStderr(error)
for (const pending of this.rpcMap.values()) { for (const pending of this.rpcMap.values()) {
clearTimeout(pending.timer) clearTimeout(pending.timer)
pending.reject(error) pending.reject(error)
@ -143,13 +150,23 @@ export class ElectronCompiler {
this.rpcMap.clear() this.rpcMap.clear()
} }
private withStderr(error: Error): Error {
const stderr = this.stderr.trim()
if (stderr) {
const stack = error.stack
error.message += '\n' + stderr
if (stack) error.stack = stack + '\n' + stderr
}
return error
}
stop(): Promise<void> { stop(): Promise<void> {
if (this.stopping) return this.stopping if (this.stopping) return this.stopping
this.rejectPending(new Error('ElectronCompiler stopped')) this.rejectPending(new Error('BytecodeCompiler stopped'))
const ps = this.ps const ps = this.ps
if (!ps) return Promise.resolve() if (!ps) return Promise.resolve()
this.stopping = new Promise<void>(resolve => { this.stopping = new Promise<void>(resolve => {
const timer = setTimeout(() => ps.kill('SIGKILL'), this.options.shutdownTimeout ?? 3000) const timer = setTimeout(() => ps.kill('SIGKILL'), SHUTDOWN_TIMEOUT)
ps.once('close', () => { ps.once('close', () => {
clearTimeout(timer) clearTimeout(timer)
resolve() resolve()

View File

@ -68,7 +68,7 @@ interface BytecodeMixin {
* Options pass on to `bytecode` plugin in electron-vite. * Options pass on to `bytecode` plugin in electron-vite.
* https://electron-vite.org/guide/source-code-protection#options * https://electron-vite.org/guide/source-code-protection#options
* *
* Compile source code to v8 bytecode. * Compiles source code to v8 bytecode and works only in the `build` command.
*/ */
bytecode?: boolean | BytecodeOptions bytecode?: boolean | BytecodeOptions
} }

View File

@ -165,7 +165,36 @@ export function startElectron(root: string | undefined): ChildProcess {
const entry = process.env.ELECTRON_ENTRY || '.' const entry = process.env.ELECTRON_ENTRY || '.'
const ps = spawn(electronPath, [entry].concat(args), { stdio: 'inherit' }) const ps = spawn(electronPath, [entry].concat(args), { stdio: 'inherit' })
ps.on('close', process.exit) // `code` is null when the child was terminated by a signal, and `process.exit` ignores
// its second argument, so passing the listener directly would report every crash as a
// successful exit. Map a signal death to a non-zero status instead.
ps.on('close', (code, signal) => process.exit(code ?? (signal ? 1 : 0)))
setupElectronSignalHandlers(ps)
return ps return ps
} }
let electronPs: ChildProcess | undefined
let signalHandlersRegistered = false
function setupElectronSignalHandlers(child: ChildProcess): void {
electronPs = child
if (signalHandlersRegistered) return
const signals: NodeJS.Signals[] =
process.platform === 'win32' ? ['SIGINT', 'SIGTERM'] : ['SIGINT', 'SIGTERM', 'SIGHUP', 'SIGUSR2']
for (const signal of signals) {
process.on(signal, () => {
const child = electronPs
if (!child?.pid || child.exitCode !== null || child.signalCode !== null) return
// A previously sent signal does not mean the child has exited.
child.kill(signal)
})
}
signalHandlersRegistered = true
}

View File

@ -67,7 +67,7 @@ export default function assetPlugin(): Plugin {
referenceId = `__VITE_NODE_PUBLIC_ASSET__${hash}__` referenceId = `__VITE_NODE_PUBLIC_ASSET__${hash}__`
} else { } else {
const cache = assetCache.get(this.environment)! const cache = assetCache.get(this.environment)!
const cached = cache.get(id) const cached = cache.get(file)
if (cached) { if (cached) {
referenceId = cached referenceId = cached
} else { } else {

View File

@ -2,28 +2,29 @@ import path from 'node:path'
import { randomBytes } from 'node:crypto' import { randomBytes } from 'node:crypto'
import { createRequire } from 'node:module' import { createRequire } from 'node:module'
import colors from 'picocolors' import colors from 'picocolors'
import { type Plugin, type LibraryOptions, type Rolldown, normalizePath } from 'vite' import { type Plugin, type Rolldown, normalizePath } from 'vite'
import * as babel from '@babel/core' import type * as Babel from '@babel/core'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { getElectronPath } from '../electron' import { toRelativePath, resolveBuildOutputs } from '../utils'
import { toRelativePath } from '../utils' import type { BytecodeCompiler, BytecodeTarget } from '../bytecodeCompiler'
import { ElectronCompiler } from './electronCompiler'
// Inspired by https://github.com/bytenode/bytenode // Inspired by https://github.com/bytenode/bytenode
const _require = createRequire(import.meta.url) const require = createRequire(import.meta.url)
let babel: typeof Babel | undefined
function getBytecodeCompilerPath(): string { function getBytecodeCompiler(plugins: readonly Plugin[]): BytecodeCompiler | undefined {
return path.join(path.dirname(_require.resolve('electron-vite/package.json')), 'bin', 'electron-bytecode-main.cjs') const plugin = plugins.find(plugin => plugin.name === 'vite:electron-bytecode-build')
return (plugin?.api as { compiler?: BytecodeCompiler } | undefined)?.compiler
} }
// Versioned envelope: magic/version (8), UTF-16 source length (4), module ID (16), V8 cache. // Versioned envelope: magic/version (8), UTF-16 source length (4), module ID (16), V8 cache.
// Keep our metadata separate from the private V8 cache header. // Keep our metadata separate from the private V8 cache header.
async function compileToBytecode(compiler: ElectronCompiler, code: string): Promise<Buffer> { async function compileToBytecode(compiler: BytecodeCompiler, target: BytecodeTarget, code: string): Promise<Buffer> {
const id = randomBytes(16) const id = randomBytes(16)
// Reserve enough source space for an untruncated, per-artifact placeholder identity. // Reserve enough source space for an untruncated, per-artifact placeholder identity.
const source = code + '\n/*' + id.toString('hex') + '*/' const source = code + '\n/*' + id.toString('hex') + '*/'
const result = await compiler.compile(source) const result = await compiler.compile(target, source)
const header = Buffer.alloc(28) const header = Buffer.alloc(28)
header.write('EVBC0001', 0, 'ascii') header.write('EVBC0001', 0, 'ascii')
header.writeUInt32LE(result.sourceLength, 8) header.writeUInt32LE(result.sourceLength, 8)
@ -73,7 +74,7 @@ const bytecodeModuleLoaderCode = [
const bytecodeChunkExtensionRE = /.(jsc|cjsc)$/ const bytecodeChunkExtensionRE = /.(jsc|cjsc)$/
export interface BytecodeOptions { export interface BytecodeOptions {
chunkAlias?: string | string[] chunkAlias?: string | string[] | RegExp
transformArrowFunctions?: boolean transformArrowFunctions?: boolean
removeBundleJS?: boolean removeBundleJS?: boolean
protectedStrings?: string[] protectedStrings?: string[]
@ -90,21 +91,24 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
} }
const { chunkAlias = [], transformArrowFunctions = true, removeBundleJS = true, protectedStrings = [] } = options const { chunkAlias = [], transformArrowFunctions = true, removeBundleJS = true, protectedStrings = [] } = options
const _chunkAlias = Array.isArray(chunkAlias) ? chunkAlias : [chunkAlias] const _chunkAlias = chunkAlias instanceof RegExp ? chunkAlias : Array.isArray(chunkAlias) ? chunkAlias : [chunkAlias]
const transformAllChunks = _chunkAlias.length === 0
const isBytecodeChunk = (chunkName: string): boolean => { const isBytecodeChunk = (chunkName: string): boolean => {
return transformAllChunks || _chunkAlias.some(alias => alias === chunkName) if (_chunkAlias instanceof RegExp) {
_chunkAlias.lastIndex = 0
return _chunkAlias.test(chunkName)
}
return _chunkAlias.length === 0 || _chunkAlias.some(alias => alias === chunkName)
} }
const plugins: babel.PluginItem[] = [] const plugins: Babel.PluginItem[] = []
if (transformArrowFunctions) { if (transformArrowFunctions) {
plugins.push('@babel/plugin-transform-arrow-functions') plugins.push('@babel/plugin-transform-arrow-functions')
} }
if (protectedStrings.length > 0) { if (protectedStrings.length > 0) {
plugins.push([protectStringsPlugin as babel.PluginTarget, { protectedStrings: new Set(protectedStrings) }]) plugins.push([protectStringsPlugin as Babel.PluginTarget, { protectedStrings: new Set(protectedStrings) }])
} }
const shouldTransformBytecodeChunk = plugins.length !== 0 const shouldTransformBytecodeChunk = plugins.length !== 0
@ -113,6 +117,7 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
code: string, code: string,
sourceMaps: boolean = false sourceMaps: boolean = false
): { code: string; map?: Rolldown.SourceMapInput } | null => { ): { code: string; map?: Rolldown.SourceMapInput } | null => {
babel ??= require('@babel/core') as typeof Babel
const re = babel.transformSync(code, { plugins, sourceMaps }) const re = babel.transformSync(code, { plugins, sourceMaps })
return re ? { code: re.code || '', map: re.map as Rolldown.SourceMapInput } : null return re ? { code: re.code || '', map: re.map as Rolldown.SourceMapInput } : null
} }
@ -122,12 +127,14 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
let supported = false let supported = false
let isPreload = false let isPreload = false
let bytecodeCompiler: BytecodeCompiler | undefined
return { return {
name: 'vite:bytecode', name: 'vite:bytecode',
apply: 'build', apply: 'build',
enforce: 'post', enforce: 'post',
configResolved(config): void { configResolved(config): void {
bytecodeCompiler = getBytecodeCompiler(config.plugins)
if (supported) { if (supported) {
return return
} }
@ -138,10 +145,9 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
return return
} }
const build = config.build const build = config.build
const resolvedOutputs = resolveBuildOutputs(build.rolldownOptions.output, build.lib) const outputs = resolveBuildOutputs(build.rolldownOptions.output, build.lib)
if (resolvedOutputs) {
const outputs = Array.isArray(resolvedOutputs) ? resolvedOutputs : [resolvedOutputs]
const output = outputs[0] const output = outputs[0]
if (output) {
if (output.format === 'es') { if (output.format === 'es') {
config.logger.warn( config.logger.warn(
colors.yellow( colors.yellow(
@ -186,13 +192,9 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
const bundles = Object.keys(output) const bundles = Object.keys(output)
const compiler = new ElectronCompiler({ if (!bytecodeCompiler) {
path: getElectronPath(), throw new Error('Bytecode compilation requires an electron-vite build')
args: [getBytecodeCompilerPath()], }
env: { ELECTRON_VITE_RENDERER: isPreload ? '1' : '0' }
})
try {
await compiler.start()
for (const name of bundles) { for (const name of bundles) {
const chunk = output[name] const chunk = output[name]
if (chunk.type === 'chunk') { if (chunk.type === 'chunk') {
@ -213,7 +215,7 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
} }
} }
if (bytecodeChunks.includes(name)) { if (bytecodeChunks.includes(name)) {
const bytecodeBuffer = await compileToBytecode(compiler, _code) const bytecodeBuffer = await compileToBytecode(bytecodeCompiler, isPreload ? 'preload' : 'main', _code)
this.emitFile({ this.emitFile({
type: 'asset', type: 'asset',
fileName: name + 'c', fileName: name + 'c',
@ -263,9 +265,6 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
} }
} }
} }
} finally {
await compiler.stop()
}
if (bytecodeChunkCount && !_chunks.some(ass => ass.type === 'asset' && ass.fileName === bytecodeModuleLoader)) { if (bytecodeChunkCount && !_chunks.some(ass => ass.type === 'asset' && ass.fileName === bytecodeModuleLoader)) {
this.emitFile({ this.emitFile({
@ -285,25 +284,14 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
} }
} }
function resolveBuildOutputs( interface ProtectStringsPluginState extends Babel.PluginPass {
outputs: Rolldown.OutputOptions | Rolldown.OutputOptions[] | undefined,
libOptions: LibraryOptions | false
): Rolldown.OutputOptions | Rolldown.OutputOptions[] | undefined {
if (libOptions && !Array.isArray(outputs)) {
const libFormats = libOptions.formats || []
return libFormats.map(format => ({ ...outputs, format }))
}
return outputs
}
interface ProtectStringsPluginState extends babel.PluginPass {
opts: { protectedStrings: Set<string> } opts: { protectedStrings: Set<string> }
} }
function protectStringsPlugin(api: typeof babel & babel.ConfigAPI): babel.PluginObj<ProtectStringsPluginState> { function protectStringsPlugin(api: typeof Babel & Babel.ConfigAPI): Babel.PluginObj<ProtectStringsPluginState> {
const { types: t } = api const { types: t } = api
function createFromCharCodeFunction(value: string): babel.types.CallExpression { function createFromCharCodeFunction(value: string): Babel.types.CallExpression {
const charCodes = Array.from(value).map(s => s.charCodeAt(0)) const charCodes = Array.from(value).map(s => s.charCodeAt(0))
const charCodeLiterals = charCodes.map(code => t.numericLiteral(code)) const charCodeLiterals = charCodes.map(code => t.numericLiteral(code))

View File

@ -2,9 +2,9 @@ import path from 'node:path'
import fs from 'node:fs' import fs from 'node:fs'
import { builtinModules } from 'node:module' import { builtinModules } from 'node:module'
import colors from 'picocolors' import colors from 'picocolors'
import { type Plugin, type LibraryOptions, type Rolldown, type UserConfig, mergeConfig, normalizePath } from 'vite' import { type Plugin, type UserConfig, mergeConfig, normalizePath } from 'vite'
import { getElectronNodeTarget, getElectronChromeTarget, supportESM } from '../electron' import { getElectronNodeTarget, getElectronChromeTarget, supportESM } from '../electron'
import { loadPackageData } from '../utils' import { loadPackageData, resolveBuildOutputs } from '../utils'
export interface ElectronPluginOptions { export interface ElectronPluginOptions {
root?: string root?: string
@ -38,17 +38,6 @@ function processEnvDefine(): Record<string, string> {
} }
} }
function resolveBuildOutputs(
outputs: Rolldown.OutputOptions | Rolldown.OutputOptions[] | undefined,
libOptions: LibraryOptions | false
): Rolldown.OutputOptions | Rolldown.OutputOptions[] | undefined {
if (libOptions && !Array.isArray(outputs)) {
const libFormats = libOptions.formats || []
return libFormats.map(format => ({ ...outputs, format }))
}
return outputs
}
export function electronMainConfigPresetPlugin(options?: ElectronPluginOptions): Plugin { export function electronMainConfigPresetPlugin(options?: ElectronPluginOptions): Plugin {
return { return {
name: 'vite:electron-main-config-preset', name: 'vite:electron-main-config-preset',
@ -148,16 +137,15 @@ export function electronMainConfigValidatorPlugin(): Plugin {
) )
} }
const resolvedOutputs = resolveBuildOutputs(rolldownOptions.output, libOptions) const outputs = resolveBuildOutputs(rolldownOptions.output, libOptions)
if (resolvedOutputs) { if (outputs.length > 0) {
const outputs = Array.isArray(resolvedOutputs) ? resolvedOutputs : [resolvedOutputs]
if (outputs.length > 1) { if (outputs.length > 1) {
throw new Error('The electron vite main config does not support multiple outputs.') throw new Error('The electron vite main config does not support multiple outputs.')
} else { } else {
const outpout = outputs[0] const output = outputs[0]
if (['es', 'cjs'].includes(outpout.format || '')) { if (['es', 'cjs'].includes(output.format || '')) {
if (outpout.format === 'es' && !supportESM()) { if (output.format === 'es' && !supportESM()) {
throw new Error( throw new Error(
'The electron vite main config output format does not support "es", ' + 'The electron vite main config output format does not support "es", ' +
'you can upgrade electron to the latest version or switch to "cjs" format.' 'you can upgrade electron to the latest version or switch to "cjs" format.'
@ -232,12 +220,9 @@ export function electronPreloadConfigPresetPlugin(options?: ElectronPluginOption
const buildConfig = mergeConfig(defaultConfig.build, build) const buildConfig = mergeConfig(defaultConfig.build, build)
config.build = buildConfig config.build = buildConfig
const resolvedOutputs = resolveBuildOutputs(config.build.rolldownOptions!.output, config.build.lib || false) const outputs = resolveBuildOutputs(config.build.rolldownOptions!.output, config.build.lib)
if (resolvedOutputs) { if (outputs.some(({ format }) => format === 'es')) {
const outputs = Array.isArray(resolvedOutputs) ? resolvedOutputs : [resolvedOutputs]
if (outputs.find(({ format }) => format === 'es')) {
if (Array.isArray(config.build.rolldownOptions!.output)) { if (Array.isArray(config.build.rolldownOptions!.output)) {
config.build.rolldownOptions!.output.forEach(output => { config.build.rolldownOptions!.output.forEach(output => {
if (output.format === 'es') { if (output.format === 'es') {
@ -252,7 +237,6 @@ export function electronPreloadConfigPresetPlugin(options?: ElectronPluginOption
: path.posix.join(build.assetsDir || defaultConfig.build.assetsDir, '[name]-[hash].mjs') : path.posix.join(build.assetsDir || defaultConfig.build.assetsDir, '[name]-[hash].mjs')
} }
} }
}
config.define = config.define || {} config.define = config.define || {}
config.define = { ...processEnvDefine(), ...config.define } config.define = { ...processEnvDefine(), ...config.define }
@ -302,16 +286,15 @@ export function electronPreloadConfigValidatorPlugin(): Plugin {
) )
} }
const resolvedOutputs = resolveBuildOutputs(rolldownOptions.output, libOptions) const outputs = resolveBuildOutputs(rolldownOptions.output, libOptions)
if (resolvedOutputs) { if (outputs.length > 0) {
const outputs = Array.isArray(resolvedOutputs) ? resolvedOutputs : [resolvedOutputs]
if (outputs.length > 1) { if (outputs.length > 1) {
throw new Error('The electron vite preload config does not support multiple outputs.') throw new Error('The electron vite preload config does not support multiple outputs.')
} else { } else {
const outpout = outputs[0] const output = outputs[0]
if (['es', 'cjs'].includes(outpout.format || '')) { if (['es', 'cjs'].includes(output.format || '')) {
if (outpout.format === 'es' && !supportESM()) { if (output.format === 'es' && !supportESM()) {
throw new Error( throw new Error(
'The electron vite preload config output format does not support "es", ' + 'The electron vite preload config output format does not support "es", ' +
'you can upgrade electron to the latest version or switch to "cjs" format.' 'you can upgrade electron to the latest version or switch to "cjs" format.'

View File

@ -105,7 +105,7 @@ export function swcPlugin(options: SwcOptions = {}): Plugin {
} }
}, },
async renderChunk(code, chunk): Promise<null | { code: string; map: SourceMap }> { async renderChunk(code, chunk): Promise<null | { code: string; map: SourceMap }> {
if (!minify || minify === 'terser') { if (!minify || minify === 'terser' || minify === 'oxc') {
return null return null
} }
const result = await transformWithSWC(code, chunk.fileName, { const result = await transformWithSWC(code, chunk.fileName, {

View File

@ -3,7 +3,7 @@ import path from 'node:path'
import fs from 'node:fs' import fs from 'node:fs'
import { createHash } from 'node:crypto' import { createHash } from 'node:crypto'
import { createRequire } from 'node:module' import { createRequire } from 'node:module'
import { loadEnv as viteLoadEnv } from 'vite' import { type LibraryOptions, type Rolldown, loadEnv as viteLoadEnv } from 'vite'
export function isObject(value: unknown): value is Record<string, unknown> { export function isObject(value: unknown): value is Record<string, unknown> {
return Object.prototype.toString.call(value) === '[object Object]' return Object.prototype.toString.call(value) === '[object Object]'
@ -32,6 +32,23 @@ export function toRelativePath(filename: string, importer: string): string {
return relPath.startsWith('.') ? relPath : `./${relPath}` return relPath.startsWith('.') ? relPath : `./${relPath}`
} }
/**
* Resolve configured build outputs into an array.
*/
export function resolveBuildOutputs(
outputs: Rolldown.OutputOptions | Rolldown.OutputOptions[] | undefined,
libOptions: LibraryOptions | false | undefined
): Rolldown.OutputOptions[] {
if (Array.isArray(outputs)) {
return outputs
}
if (libOptions) {
const libFormats = libOptions.formats || []
return libFormats.map(format => ({ ...outputs, format }))
}
return outputs ? [outputs] : []
}
/** /**
* Load `.env` files within the `envDir` (default: `process.cwd()`) . * Load `.env` files within the `envDir` (default: `process.cwd()`) .
* By default, only env variables prefixed with `VITE_`, `MAIN_VITE_`, `PRELOAD_VITE_` and * By default, only env variables prefixed with `VITE_`, `MAIN_VITE_`, `PRELOAD_VITE_` and