Compare commits

...

15 Commits

11 changed files with 292 additions and 228 deletions

View File

@ -1,53 +1,85 @@
### v6.0.0-beta.5 (_2026-09-29_) ### v6.0.0 (_2026-10-15_)
- feat(bytecode): support RegExp for chunkAlias option ([#885](https://github.com/alex8088/electron-vite/issues/885)) - 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: 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: 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): 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): forward termination signals to Electron child ([#900](https://github.com/alex8088/electron-vite/issues/900))
- fix(dev): improve Electron process signal handling - 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(bytecode): set the regular expression's `lastIndex` to 0 to perform a global match for chunkAlias option
### v6.0.0-beta.4 (_2026-09-28_)
- fix(esmShim)!: use `this.parse` to find the last static import ([#906](https://github.com/alex8088/electron-vite/issues/906)) - 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 - fix(deps)!: downgraded @babel/* to v7 due to Node requirements
### v6.0.0-beta.2 (_2026-09-27_)
- 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`
- perf: setup rollupOption compatibility via config factory
- perf: build compatibility target for Electron 42, 43, 44
- 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.5", "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) {
@ -25,4 +43,14 @@ export async function build(inlineConfig: InlineConfig = {}): Promise<void> {
await viteBuild(viteConfig) await viteBuild(viteConfig)
} }
} }
}
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

@ -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)
@ -100,14 +101,14 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
return _chunkAlias.length === 0 || _chunkAlias.some(alias => alias === 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
@ -116,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
} }
@ -125,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
} }
@ -141,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(
@ -189,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') {
@ -216,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',
@ -266,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({
@ -288,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