Compare commits

..

No commits in common. "master" and "v5.0.0-beta.1" have entirely different histories.

25 changed files with 2110 additions and 2605 deletions

View File

@ -1,140 +1,32 @@
### v6.0.0 (_2026-10-15_) ### v5.0.0-beta.1 (_2025-10-29_)
- 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): 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(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(deps)!: update magic-string to v1
- fix(deps)!: update @babel/* to v8
- fix!: compatible with rollupOptions and rolldownOptions
- fix: chunk filename is wrong in ES mode
- fix(asset): isolate asset caches per environment
- fix: use the environment api in plugins
- fix: skip TTY terminal manipulations in CI environments
- 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(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 globals to v17
- chore(deps): update eslint and @eslint/js to v10
- 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_)
- feat(config): add `build.externalizeDeps` and `build.bytecode` config options to replace `externalizeDepsPlugin` and `bytecodePlugin`
- feat: reporter plugin for isolated builds
- feat: enhanced string protection - feat: enhanced string protection
- feat: add `isolatedEntries` option for `preload` and `renderer` to build entries as standalone bundles [#154](https://github.com/alex8088/electron-vite/issues/154) - feat: add `isolatedEntries` option for `preload` and `renderer` to build entries as standalone bundles [#154](https://github.com/alex8088/electron-vite/issues/154)
- refactor(config): move the `isolateEntries` options to the `build` option
- refactor: deprecated `externalizeDepsPlugin` and `bytecodePlugin`
- refactor(config)!: remove function resolution for nested config fields
- refactor(asset): remove redundant path normalization - refactor(asset): remove redundant path normalization
- refactor: split electron plugin into preset and validator plugins - refactor: split electron plugin into preset and validator plugins
- refactor(config)!: restructure Electron Vite config interfaces - refactor(config)!: restructure Electron Vite config interfaces
- refactor(build): simplify build logic - refactor(build): simplify build logic
- refactor: replace `JSON.parse/stringify` with manual deep clone - refactor: replace `JSON.parse/stringify` with manual deep clone
- perf: build compatibility target for Electron 39
- fix(modulePath): prevent duplicate asset emission
- fix(modulePath): support watch mode
- chore: improve logging message clarity and consistency
### v5.0.0-beta.0 (_2025-10-19_)
- refactor(bytecodePlugin): improved bytecode bundle generation and made a new string protection plugin - refactor(bytecodePlugin): improved bytecode bundle generation and made a new string protection plugin
- refactor(modulePath): better support for tree-shaking and code-splitting - refactor(modulePath): better support for tree-shaking and code-splitting
- refactor: remove Electron 18, 19, 20, 21 build compatilibity target - refactor: remove Electron 18, 19, 20, 21 build compatilibity target
- perf(buildReport): exclude node_modules from watch list
- perf(isolateEntries): transform log
- perf(isolateEntries): optimize entries transformation
- perf: build compatibility target for Electron 39
- perf(plugin): more efficient module filtering via regular expressions - perf(plugin): more efficient module filtering via regular expressions
- perf(plugin): no need to cache `sourcemap` option - perf(plugin): no need to cache `sourcemap` option
- perf(plugin): lazily initialize `MagicString` and remove the redundant pre-check - perf(plugin): lazily initialize `MagicString` and remove the redundant pre-check
- perf(bytecodePlugin): better way to count bytecode chunks - perf(bytecodePlugin): better way to count bytecode chunks
- perf(plugin): enhance path resolution using `import.meta.dirname` for ES modules - perf(plugin): enhance path resolution using `import.meta.dirname` for ES modules
- fix(modulePath): rewrite the build input instead of merging
- fix(asset): normalize imported public asset chunk path
- fix: avoid duplicate chunk emission
- fix(modulePath): prevent duplicate asset emission
- fix(modulePath): support watch mode
- chore: fix jsdoc
- chore: improve logging message clarity and consistency
- chore(deps): update all non-major dependencies - chore(deps): update all non-major dependencies
- chore: update eslint config - chore: update eslint config
- chore: replace `tseslint.config` with `defineConfig` - chore: replace `tseslint.config` with `defineConfig`
- chore: remove redundant external id - chore: remove redundant external id
- chore: rename the file `esm` to `esmShim` - chore: rename the file `esm` to `esmShim`
- docs: update
### v5.0.0-beta.3 (_2025-11-01_)
See [v5.0.0-beta.3 changelog](https://github.com/alex8088/electron-vite/blob/v5.0.0-beta.3/CHANGELOG.md)
### v5.0.0-beta.2 (_2025-10-30_)
See [v5.0.0-beta.2 changelog](https://github.com/alex8088/electron-vite/blob/v5.0.0-beta.2/CHANGELOG.md)
### v5.0.0-beta.1 (_2025-10-29_)
See [v5.0.0-beta.1 changelog](https://github.com/alex8088/electron-vite/blob/v5.0.0-beta.1/CHANGELOG.md)
### v5.0.0-beta.0 (_2025-10-19_)
See [v5.0.0-beta.0 changelog](https://github.com/alex8088/electron-vite/blob/v5.0.0-beta.0/CHANGELOG.md)
### v4.0.1 (_2025-09-21_) ### v4.0.1 (_2025-09-21_)

View File

@ -28,13 +28,13 @@
## Features ## Features
- ⚡️ [Vite](https://vitejs.dev) powered and use the same way. - ⚡️ [Vite](https://vitejs.dev) powered and use the same way.
- 🛠 Pre-configure with sensible defaults optimized for Electron. - 🛠 Pre-configured for Electron, don't worry about configuration.
- 💡 Optimize asset handling for Electron main process. - 💡 Optimize asset handling (Node.js addons, WebAssembly, Worker Thread, etc).
- 🚀 Fast HMR & hot reloading. - 🚀 Fast HMR for renderer processes.
- 🔥 Isolated build for multi-entry application development. - 🔥 Hot reloading for main process and preload scripts.
- ✨ Simplify multi-threading development. - 🔌 Easy to debug in IDEs like VSCode or WebStorm.
- 🔒 Compile code to v8 bytecode to protect source code. - 🔒 Compile to v8 bytecode to protect source code.
- 🔌 Easy to debug in IDEs such as VSCode or WebStorm. - 🏷️ Support for TypeScript decorators.
- 📦 Out-of-the-box support for TypeScript, Vue, React, Svelte, SolidJS and more. - 📦 Out-of-the-box support for TypeScript, Vue, React, Svelte, SolidJS and more.
## Usage ## Usage
@ -83,7 +83,7 @@ export default {
Clone the [electron-vite-boilerplate](https://github.com/alex8088/electron-vite-boilerplate) or use the [create-electron](https://github.com/alex8088/quick-start/tree/master/packages/create-electron) tool to scaffold your project. Clone the [electron-vite-boilerplate](https://github.com/alex8088/electron-vite-boilerplate) or use the [create-electron](https://github.com/alex8088/quick-start/tree/master/packages/create-electron) tool to scaffold your project.
```bash ```bash
npm create @quick-start/electron@latest npm create @quick-start/electron
``` ```
Currently supported template presets include: Currently supported template presets include:

View File

@ -1,15 +0,0 @@
const vm = require('node:vm')
const v8 = require('node:v8')
v8.setFlagsFromString('--no-lazy')
v8.setFlagsFromString('--no-flush-bytecode')
const params = ['exports', 'require', 'module', '__filename', '__dirname']
module.exports = function compile(code) {
if (typeof code !== 'string') throw new TypeError('Compilation source must be a string')
const { cachedData } = vm.compileFunction(code, params, { produceCachedData: true })
if (!Buffer.isBuffer(cachedData)) throw new Error('V8 did not produce cached data')
// Explicit wire format works across both Node IPC and Electron structured clone.
return { sourceLength: code.length, cachedData: cachedData.toString('base64') }
}

View File

@ -1,109 +0,0 @@
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('node:path')
const RpcPeer = require('./rpc-peer.cjs')
const compile = require('./bytecode-compiler.cjs')
if (process.platform === 'darwin') {
app.dock.hide()
}
const channel = 'electron-vite:bytecode:rpc'
const readyChannel = 'electron-vite:bytecode:ready'
let win
let rendererRPC
let rendererStarting
let stopping = false
function fail(error) {
console.error(error)
parentRPC.close(error)
rendererRPC?.close(error)
app.exit(1)
}
const parentRPC = new RpcPeer(
message =>
new Promise((resolve, reject) => {
if (!process.connected || !process.send) return reject(new Error('Parent IPC is disconnected'))
process.send(message, error => (error ? reject(error) : resolve()))
})
)
function createRenderer() {
return new Promise((resolve, reject) => {
let ready = false
const timer = setTimeout(() => finish(new Error('Compiler preload readiness timed out')), 30000)
function finish(error) {
clearTimeout(timer)
ipcMain.removeListener(readyChannel, onReady)
if (error) reject(error)
else {
ready = true
resolve()
}
}
function onReady(event) {
if (event.sender === win.webContents) finish()
}
function onFailure(error) {
if (stopping) return
if (ready) fail(error)
else finish(error)
}
ipcMain.on(readyChannel, onReady)
win = new BrowserWindow({
show: false,
webPreferences: {
preload: path.join(__dirname, 'electron-bytecode-preload.cjs'),
sandbox: false,
contextIsolation: true
}
})
rendererRPC = new RpcPeer(message => {
if (win.isDestroyed()) throw new Error('Compiler window is closed')
win.webContents.send(channel, message)
})
ipcMain.on(channel, (event, message) => {
if (!win.isDestroyed() && event.sender === win.webContents) void rendererRPC.receive(message)
})
win.webContents.on('preload-error', (_event, _path, error) => onFailure(error))
win.webContents.on('render-process-gone', (_event, details) => {
onFailure(new Error(`Compiler renderer exited: ${details.reason}`))
})
win.on('closed', () => onFailure(new Error('Compiler window closed unexpectedly')))
win.loadURL('data:text/html,<!doctype html><html></html>').catch(onFailure)
})
}
const initialized = app.whenReady().then(() => true)
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)
process.on('message', message => {
if (message?.type === 'shutdown') {
stopping = true
app.quit()
} else {
void parentRPC.receive(message)
}
})
process.on('disconnect', () => {
stopping = true
app.quit()
})
app.on('before-quit', () => {
stopping = true
parentRPC.close()
rendererRPC?.close()
})
if (!process.send) fail(new Error('Bytecode compiler must be launched with an IPC channel'))

View File

@ -1,10 +0,0 @@
const { ipcRenderer } = require('electron')
const RpcPeer = require('./rpc-peer.cjs')
const compile = require('./bytecode-compiler.cjs')
const channel = 'electron-vite:bytecode:rpc'
const rpc = new RpcPeer(message => ipcRenderer.send(channel, message))
rpc.register('ready', () => true)
rpc.register('compile', compile)
ipcRenderer.on(channel, (_event, message) => void rpc.receive(message))
ipcRenderer.send('electron-vite:bytecode:ready')

View File

@ -1,89 +0,0 @@
// Transport-independent peer. Messages must be JSON serializable.
class RpcPeer {
constructor(send, timeout = 30000) {
this.send = send
this.timeout = timeout
this.methods = new Map()
this.pending = new Map()
this.nextId = 0
this.closed = false
}
register(cmd, method) {
this.methods.set(cmd, method)
return this
}
request(cmd, ...args) {
if (this.closed) return Promise.reject(new Error('RPC connection is closed'))
const invocationId = this.nextId++
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
this.pending.delete(invocationId)
reject(new Error(`RPC request timed out: ${cmd}`))
}, this.timeout)
this.pending.set(invocationId, { resolve, reject, timer })
Promise.resolve()
.then(() => this.send({ type: 'request', invocationId, cmd, args }))
.catch(error => {
const pending = this.pending.get(invocationId)
if (!pending) return
clearTimeout(timer)
this.pending.delete(invocationId)
reject(error)
})
})
}
async receive(message) {
if (this.closed || !message || !Number.isSafeInteger(message.invocationId)) return
const { type, invocationId } = message
if (type === 'response') {
const pending = this.pending.get(invocationId)
if (!pending) return
this.pending.delete(invocationId)
clearTimeout(pending.timer)
if (message.error) {
const error = new Error(message.error.message)
error.name = message.error.name || 'Error'
if (message.error.stack) error.stack = message.error.stack
pending.reject(error)
} else {
pending.resolve(message.result)
}
return
}
if (type !== 'request' || typeof message.cmd !== 'string' || !Array.isArray(message.args)) return
const response = { type: 'response', invocationId }
try {
const method = this.methods.get(message.cmd)
if (!method) throw new Error(`Invalid method: ${message.cmd}`)
response.result = await method(...message.args)
} catch (error) {
response.error = {
name: error instanceof Error ? error.name : 'Error',
message: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined
}
}
if (!this.closed) {
try {
await this.send(response)
} catch (error) {
this.close(error)
}
}
}
close(error = new Error('RPC connection is closed')) {
this.closed = true
for (const pending of this.pending.values()) {
clearTimeout(pending.timer)
pending.reject(error)
}
this.pending.clear()
this.methods.clear()
}
}
module.exports = RpcPeer

View File

@ -1,6 +1,6 @@
{ {
"name": "electron-vite", "name": "electron-vite",
"version": "6.0.0", "version": "5.0.0-beta.1",
"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",
@ -61,7 +61,7 @@
}, },
"peerDependencies": { "peerDependencies": {
"@swc/core": "^1.0.0", "@swc/core": "^1.0.0",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0" "vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@swc/core": { "@swc/core": {
@ -69,39 +69,40 @@
} }
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^9.37.0",
"@rollup/plugin-json": "^6.1.0", "@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^16.0.3", "@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-typescript": "^12.3.0", "@rollup/plugin-typescript": "^12.1.4",
"@swc/core": "^1.16.2", "@swc/core": "^1.13.5",
"@types/babel__core": "^7.20.5", "@types/babel__core": "^7.20.5",
"@types/node": "^22.20.4", "@types/node": "^22.18.11",
"eslint": "^10.11.0", "eslint": "^9.37.0",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.6", "eslint-plugin-prettier": "^5.5.4",
"globals": "^17.12.0", "globals": "^16.4.0",
"lint-staged": "^17.5.1", "lint-staged": "^16.2.4",
"prettier": "^3.9.9", "prettier": "^3.6.2",
"rollup": "^4.63.5", "rollup": "^4.52.4",
"rollup-plugin-dts": "^6.5.1", "rollup-plugin-dts": "^6.2.3",
"rollup-plugin-rm": "^1.0.2", "rollup-plugin-rm": "^1.0.2",
"simple-git-hooks": "^2.14.0", "simple-git-hooks": "^2.13.1",
"tslib": "^2.8.1", "tslib": "^2.8.1",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.70.1", "typescript-eslint": "^8.46.1",
"vite": "^8.3.1" "vite": "^7.1.10"
}, },
"dependencies": { "dependencies": {
"@babel/core": "^7.29.7", "@babel/core": "^7.28.4",
"@babel/plugin-transform-arrow-functions": "^7.29.7", "@babel/plugin-transform-arrow-functions": "^7.27.1",
"cac": "^7.0.0", "cac": "^6.7.14",
"magic-string": "^1.4.2", "esbuild": "^0.25.11",
"picocolors": "^1.1.1", "magic-string": "^0.30.19",
"rolldown": "~1.2.10" "picocolors": "^1.1.1"
}, },
"pnpm": { "pnpm": {
"onlyBuiltDependencies": [ "onlyBuiltDependencies": [
"@swc/core", "@swc/core",
"esbuild",
"simple-git-hooks" "simple-git-hooks"
] ]
} }

2429
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -26,7 +26,7 @@ export default defineConfig([
plugins: [ plugins: [
rm('dist', 'buildStart'), rm('dist', 'buildStart'),
json(), json(),
ts({ compilerOptions: { rootDir: 'src', outDir: 'dist', declaration: true, declarationDir: 'dist/types' } }), ts({ compilerOptions: { rootDir: 'src', declaration: true, declarationDir: 'dist/types' } }),
resolve() resolve()
], ],
treeshake: { treeshake: {
@ -35,7 +35,6 @@ export default defineConfig([
}, },
{ {
input: 'dist/types/index.d.ts', input: 'dist/types/index.d.ts',
external: ['vite'],
output: [{ file: pkg.types, format: 'es' }], output: [{ file: pkg.types, format: 'es' }],
plugins: [dts(), rm('dist/types', 'buildEnd')] plugins: [dts(), rm('dist/types', 'buildEnd')]
} }

View File

@ -1,40 +1,22 @@
import { build as viteBuild, mergeConfig } from 'vite' import { build as viteBuild } from 'vite'
import { type InlineConfig, type MainViteConfig, type PreloadViteConfig, resolveConfig } from './config' import { type InlineConfig, 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
} }
const hasBytecode = (await usesBytecode(config.config.main)) || (await usesBytecode(config.config.preload))
const buildTargets = async (): Promise<void> => {
// Build targets in order: main -> preload -> renderer // Build targets in order: main -> preload -> renderer
for (const target of ['main', 'preload', 'renderer'] as const) { const buildTargets = ['main', 'preload', 'renderer'] as const
const viteConfig = config.config?.[target]
for (const target of buildTargets) {
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) {
@ -44,13 +26,3 @@ 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,188 +0,0 @@
import { type ChildProcess, spawn } from 'node:child_process'
import path from 'node:path'
import { createRequire } from 'node:module'
import { getElectronPath } from './electron'
const require = createRequire(import.meta.url)
const RPC_TIMEOUT = 30_000
const SHUTDOWN_TIMEOUT = 3_000
export type BytecodeCompileResult = {
sourceLength: number
/** Base64-encoded V8 cachedData; decode with Buffer.from(cachedData, 'base64'). */
cachedData: string
}
export type BytecodeTarget = 'main' | 'preload'
type CompilerResponse = {
type: 'response'
invocationId: number
result?: unknown
error?: { name: string; message: string; stack?: string }
}
type PendingRequest = {
resolve: (value: unknown) => void
reject: (reason: Error) => void
timer: ReturnType<typeof setTimeout>
}
export class BytecodeCompiler {
private ps?: ChildProcess
private rpcMap = new Map<number, PendingRequest>()
private invocationId = 0
private starting?: Promise<void>
private stopping?: Promise<void>
private ready = false
private stderr = ''
start(): Promise<void> {
if (this.stopping) return Promise.reject(new Error('BytecodeCompiler is stopping'))
if (this.starting) return this.starting
if (this.ready) return Promise.resolve()
if (this.ps) return Promise.reject(new Error('Previous Electron process has not closed; await stop() first'))
this.starting = this.initialize().finally(() => {
this.starting = undefined
})
return this.starting
}
private async initialize(): Promise<void> {
const env: NodeJS.ProcessEnv = { ...process.env, ELECTRON_VITE_RPC: 'true' }
delete env.ELECTRON_RUN_AS_NODE
this.stderr = ''
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,
env
})
this.ps = ps
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
const pending = this.rpcMap.get(message.invocationId)
if (!pending) return
clearTimeout(pending.timer)
this.rpcMap.delete(message.invocationId)
if (message.error) {
const error = new Error(message.error.message)
error.name = message.error.name
if (message.error.stack) error.stack = message.error.stack
pending.reject(error)
} else pending.resolve(message.result)
})
ps.on('error', error => {
if (this.ps === ps) this.rejectPending(error)
})
ps.on('disconnect', () => {
if (this.ps === ps) this.rejectPending(new Error('Electron IPC disconnected'))
})
ps.on('exit', (code, signal) => {
if (this.ps === ps) this.rejectPending(new Error('Electron exited (code ' + code + ', signal ' + signal + ')'))
})
ps.on('close', () => {
if (this.ps === ps) {
this.rejectPending(new Error('Electron process closed'))
this.ps = undefined
}
})
try {
await this.sendRequest('ready', [])
if (!ps.connected || this.stopping) throw new Error('Electron stopped during initialization')
this.ready = true
} catch (error) {
await this.stop()
throw error
}
}
compile(target: BytecodeTarget, code: string): Promise<BytecodeCompileResult> {
return this.request<BytecodeCompileResult>('compile', target, code)
}
async request<T = unknown>(cmd: string, ...args: unknown[]): Promise<T> {
if (!this.ready || this.stopping) throw new Error('BytecodeCompiler is not ready; await start() first')
return this.sendRequest(cmd, args) as Promise<T>
}
private sendRequest(cmd: string, args: unknown[]): Promise<unknown> {
const ps = this.ps
if (!ps?.connected) return Promise.reject(new Error('Electron IPC is not connected'))
const invocationId = this.invocationId++
return new Promise((resolve, reject) => {
const fail = (error: Error): void => {
const pending = this.rpcMap.get(invocationId)
if (!pending) return
clearTimeout(pending.timer)
this.rpcMap.delete(invocationId)
reject(this.withStderr(error))
}
const timer = setTimeout(() => fail(new Error('Electron RPC timed out: ' + cmd)), RPC_TIMEOUT)
this.rpcMap.set(invocationId, { resolve, reject, timer })
try {
ps.send({ type: 'request', invocationId, cmd, args }, error => {
if (error) fail(error)
})
} catch (error) {
fail(error instanceof Error ? error : new Error(String(error)))
}
})
}
private rejectPending(error: Error): void {
this.ready = false
if (this.rpcMap.size) this.withStderr(error)
for (const pending of this.rpcMap.values()) {
clearTimeout(pending.timer)
pending.reject(error)
}
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> {
if (this.stopping) return this.stopping
this.rejectPending(new Error('BytecodeCompiler stopped'))
const ps = this.ps
if (!ps) return Promise.resolve()
this.stopping = new Promise<void>(resolve => {
const timer = setTimeout(() => ps.kill('SIGKILL'), SHUTDOWN_TIMEOUT)
ps.once('close', () => {
clearTimeout(timer)
resolve()
})
if (ps.connected) {
try {
ps.send({ type: 'shutdown' }, error => {
if (error) ps.kill()
})
} catch {
ps.kill()
}
} else ps.kill()
}).finally(() => {
this.stopping = undefined
})
return this.stopping
}
}

View File

@ -1,16 +1,18 @@
import path from 'node:path' import path from 'node:path'
import fs from 'node:fs'
import { pathToFileURL } from 'node:url'
import { createRequire } from 'node:module'
import colors from 'picocolors' import colors from 'picocolors'
import { import {
type UserConfig as ViteConfig, type UserConfig as ViteConfig,
type ConfigEnv, type ConfigEnv,
type PluginOption, type PluginOption,
type Plugin,
type BuildEnvironmentOptions as ViteBuildOptions,
type LogLevel, type LogLevel,
createLogger, createLogger,
mergeConfig, mergeConfig,
normalizePath normalizePath
} from 'vite' } from 'vite'
import { build } from 'esbuild'
import { import {
electronMainConfigPresetPlugin, electronMainConfigPresetPlugin,
@ -26,14 +28,11 @@ import importMetaPlugin from './plugins/importMeta'
import esmShimPlugin from './plugins/esmShim' import esmShimPlugin from './plugins/esmShim'
import modulePathPlugin from './plugins/modulePath' import modulePathPlugin from './plugins/modulePath'
import isolateEntriesPlugin from './plugins/isolateEntries' import isolateEntriesPlugin from './plugins/isolateEntries'
import { type ExternalOptions, externalizeDepsPlugin } from './plugins/externalizeDeps' import { isObject, isFilePathESM, deepClone } from './utils'
import { type BytecodeOptions, bytecodePlugin } from './plugins/bytecode'
import { isObject, isFilePathESM, cloneConfig, asyncFlatten } from './utils'
import { findConfigFile, bundleConfigFile, loadConfigFormBundledFile } from './load'
export { defineConfig as defineViteConfig } from 'vite' export { defineConfig as defineViteConfig } from 'vite'
interface IsolatedEntriesMixin { interface IsolatedEntriesOption {
/** /**
* Build each entry point as an isolated bundle without code splitting. * Build each entry point as an isolated bundle without code splitting.
* *
@ -41,56 +40,17 @@ interface IsolatedEntriesMixin {
* preventing automatic code splitting across entries and ensuring each * preventing automatic code splitting across entries and ensuring each
* output file is fully standalone. * output file is fully standalone.
* *
* **Important**: When using `isolatedEntries` in `preload` config, you
* should also disable `build.externalizeDeps` to ensure third-party dependencies
* from `node_modules` are bundled together, which is required for Electron
* sandbox support.
*
* @experimental * @experimental
* @default false * @default false
*/ */
isolatedEntries?: boolean isolatedEntries?: boolean
} }
interface ExternalizeDepsMixin { export interface MainViteConfig extends ViteConfig {}
/**
* Options pass on to `externalizeDeps` plugin in electron-vite.
*
* Automatically externalize dependencies.
*
* @default true
*/
externalizeDeps?: boolean | ExternalOptions
}
interface BytecodeMixin { export interface PreloadViteConfig extends ViteConfig, IsolatedEntriesOption {}
/**
* Options pass on to `bytecode` plugin in electron-vite.
* https://electron-vite.org/guide/source-code-protection#options
*
* Compiles source code to v8 bytecode and works only in the `build` command.
*/
bytecode?: boolean | BytecodeOptions
}
interface MainBuildOptions extends ViteBuildOptions, ExternalizeDepsMixin, BytecodeMixin {} export interface RendererViteConfig extends ViteConfig, IsolatedEntriesOption {}
interface PreloadBuildOptions extends ViteBuildOptions, ExternalizeDepsMixin, BytecodeMixin, IsolatedEntriesMixin {}
interface RendererBuildOptions extends ViteBuildOptions, IsolatedEntriesMixin {}
interface BaseViteConfig<T> extends Omit<ViteConfig, 'build'> {
/**
* Build specific options
*/
build?: T
}
export interface MainViteConfig extends BaseViteConfig<MainBuildOptions> {}
export interface PreloadViteConfig extends BaseViteConfig<PreloadBuildOptions> {}
export interface RendererViteConfig extends BaseViteConfig<RendererBuildOptions> {}
export interface UserConfig { export interface UserConfig {
/** /**
@ -118,7 +78,11 @@ export type ElectronViteConfigFnPromise = (env: ConfigEnv) => Promise<UserConfig
export type ElectronViteConfigFn = (env: ConfigEnv) => UserConfig | Promise<UserConfig> export type ElectronViteConfigFn = (env: ConfigEnv) => UserConfig | Promise<UserConfig>
export type ElectronViteConfigExport = export type ElectronViteConfigExport =
UserConfig | Promise<UserConfig> | ElectronViteConfigFnObject | ElectronViteConfigFnPromise | ElectronViteConfigFn | UserConfig
| Promise<UserConfig>
| ElectronViteConfigFnObject
| ElectronViteConfigFnPromise
| ElectronViteConfigFn
/** /**
* Type helper to make it easier to use `electron.vite.config.*` * Type helper to make it easier to use `electron.vite.config.*`
@ -157,8 +121,7 @@ export async function resolveConfig(
process.env.NODE_ENV = defaultMode process.env.NODE_ENV = defaultMode
const userConfig: UserConfig | undefined = {} let userConfig: UserConfig | undefined
let configFileDependencies: string[] = [] let configFileDependencies: string[] = []
let { configFile } = config let { configFile } = config
@ -185,20 +148,110 @@ export async function resolveConfig(
const outDir = config.build?.outDir const outDir = config.build?.outDir
const { main, preload, renderer } = loadResult.config if (loadResult.config.main) {
const mainViteConfig: MainViteConfig = mergeConfig(loadResult.config.main, deepClone(config))
if (main) { mainViteConfig.mode = inlineConfig.mode || mainViteConfig.mode || defaultMode
userConfig.main = await new MainConfigFactory(main, config, { outDir, root }).build()
if (outDir) {
resetOutDir(mainViteConfig, outDir, 'main')
} }
if (preload) { const builtInMainPlugins: PluginOption[] = [
userConfig.preload = await new PreloadConfigFactory(preload, config, { outDir, root }).build() electronMainConfigPresetPlugin({ root }),
electronMainConfigValidatorPlugin(),
assetPlugin(),
workerPlugin(),
modulePathPlugin(
mergeConfig(
{
plugins: [electronMainConfigPresetPlugin({ root }), assetPlugin(), importMetaPlugin(), esmShimPlugin()]
},
mainViteConfig
)
),
importMetaPlugin(),
esmShimPlugin()
]
mainViteConfig.plugins = builtInMainPlugins.concat(mainViteConfig.plugins || [])
loadResult.config.main = mainViteConfig
} }
if (renderer) { if (loadResult.config.preload) {
userConfig.renderer = await new RendererConfigFactory(renderer, config, { outDir, root }).build() const preloadViteConfig: PreloadViteConfig = mergeConfig(loadResult.config.preload, deepClone(config))
preloadViteConfig.mode = inlineConfig.mode || preloadViteConfig.mode || defaultMode
if (outDir) {
resetOutDir(preloadViteConfig, outDir, 'preload')
} }
const builtInPreloadPlugins: PluginOption[] = [
electronPreloadConfigPresetPlugin({ root }),
electronPreloadConfigValidatorPlugin(),
assetPlugin(),
importMetaPlugin(),
esmShimPlugin()
]
if (preloadViteConfig.isolatedEntries) {
builtInPreloadPlugins.push(
isolateEntriesPlugin(
mergeConfig(
{
plugins: [
electronPreloadConfigPresetPlugin({ root }),
assetPlugin(),
importMetaPlugin(),
esmShimPlugin()
]
},
preloadViteConfig
)
)
)
}
preloadViteConfig.plugins = builtInPreloadPlugins.concat(preloadViteConfig.plugins)
loadResult.config.preload = preloadViteConfig
}
if (loadResult.config.renderer) {
const rendererViteConfig: RendererViteConfig = mergeConfig(loadResult.config.renderer, deepClone(config))
rendererViteConfig.mode = inlineConfig.mode || rendererViteConfig.mode || defaultMode
if (outDir) {
resetOutDir(rendererViteConfig, outDir, 'renderer')
}
const builtInRendererPlugins: PluginOption[] = [
electronRendererConfigPresetPlugin({ root }),
electronRendererConfigValidatorPlugin()
]
if (rendererViteConfig.isolatedEntries) {
builtInRendererPlugins.push(
isolateEntriesPlugin(
mergeConfig(
{
plugins: [electronRendererConfigPresetPlugin({ root })]
},
rendererViteConfig
)
)
)
}
rendererViteConfig.plugins = builtInRendererPlugins.concat(rendererViteConfig.plugins || [])
loadResult.config.renderer = rendererViteConfig
}
userConfig = loadResult.config
configFile = loadResult.path configFile = loadResult.path
configFileDependencies = loadResult.dependencies configFileDependencies = loadResult.dependencies
} }
@ -213,111 +266,6 @@ export async function resolveConfig(
return resolved return resolved
} }
export abstract class ConfigFactory<T extends MainViteConfig | PreloadViteConfig | RendererViteConfig> {
constructor(
protected readonly baseConfig: T,
protected readonly inlineConfig: InlineConfig,
protected readonly options: { outDir?: string; root?: string }
) {
baseConfig.build ??= {}
baseConfig.build.rolldownOptions ??= baseConfig.build.rollupOptions
}
async build(cleanMode?: boolean): Promise<T> {
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
const config = mergeConfig(cloneConfig(this.baseConfig) as any, cloneConfig(this.inlineConfig)) as T
config.mode = this.inlineConfig.mode || config.mode || process.env.NODE_ENV
if (this.options.outDir) {
resetOutDir(config, this.options.outDir, this.processType())
}
const builtinPlugins = await this.resolveBuiltinPlugins(config, cleanMode)
config.plugins = builtinPlugins.concat(config.plugins || [])
return config
}
protected abstract processType(): 'main' | 'preload' | 'renderer'
protected abstract resolveBuiltinPlugins(config: T, cleanMode?: boolean): Promise<PluginOption[]>
}
export class MainConfigFactory extends ConfigFactory<MainViteConfig> {
protected processType(): 'main' {
return 'main'
}
protected async resolveBuiltinPlugins(config: MainViteConfig, cleanMode?: boolean): Promise<PluginOption[]> {
const configDrivenPlugins: PluginOption[] = await resolveConfigDrivenPlugins(config)
return cleanMode
? [
electronMainConfigPresetPlugin({ root: this.options.root }),
assetPlugin(),
importMetaPlugin(),
esmShimPlugin(),
...configDrivenPlugins
]
: [
electronMainConfigPresetPlugin({ root: this.options.root }),
electronMainConfigValidatorPlugin(),
assetPlugin(),
workerPlugin(),
modulePathPlugin(this),
importMetaPlugin(),
esmShimPlugin(),
...configDrivenPlugins
]
}
}
export class PreloadConfigFactory extends ConfigFactory<PreloadViteConfig> {
protected processType(): 'preload' {
return 'preload'
}
protected async resolveBuiltinPlugins(config: PreloadViteConfig, cleanMode?: boolean): Promise<PluginOption[]> {
const configDrivenPlugins: PluginOption[] = await resolveConfigDrivenPlugins(config)
return cleanMode
? [
electronPreloadConfigPresetPlugin({ root: this.options.root }),
assetPlugin(),
importMetaPlugin(),
esmShimPlugin(),
...configDrivenPlugins
]
: [
electronPreloadConfigPresetPlugin({ root: this.options.root }),
electronPreloadConfigValidatorPlugin(),
assetPlugin(),
importMetaPlugin(),
esmShimPlugin(),
...configDrivenPlugins,
...(config.build?.isolatedEntries ? [isolateEntriesPlugin(this)] : [])
]
}
}
export class RendererConfigFactory extends ConfigFactory<RendererViteConfig> {
protected processType(): 'renderer' {
return 'renderer'
}
protected async resolveBuiltinPlugins(config: RendererViteConfig, cleanMode?: boolean): Promise<PluginOption[]> {
return cleanMode
? [electronRendererConfigPresetPlugin({ root: this.options.root })]
: [
electronRendererConfigPresetPlugin({ root: this.options.root }),
electronRendererConfigValidatorPlugin(),
...(config.build?.isolatedEntries ? [isolateEntriesPlugin(this)] : [])
]
}
}
function resetOutDir(config: ViteConfig, outDir: string, subOutDir: string): void { function resetOutDir(config: ViteConfig, outDir: string, subOutDir: string): void {
let userOutDir = config.build?.outDir let userOutDir = config.build?.outDir
if (outDir === userOutDir) { if (outDir === userOutDir) {
@ -330,37 +278,7 @@ function resetOutDir(config: ViteConfig, outDir: string, subOutDir: string): voi
} }
} }
async function resolveConfigDrivenPlugins(config: MainViteConfig | PreloadViteConfig): Promise<PluginOption[]> { const CONFIG_FILE_NAME = 'electron.vite.config'
const userPlugins = (await asyncFlatten(config.plugins || [])).filter(Boolean) as Plugin[]
const configDrivenPlugins: PluginOption[] = []
const hasExternalizeDepsPlugin = userPlugins.some(p => p.name === 'vite:externalize-deps')
if (!hasExternalizeDepsPlugin) {
const externalOptions = config.build?.externalizeDeps ?? true
if (externalOptions) {
isOptions<ExternalOptions>(externalOptions)
? configDrivenPlugins.push(externalizeDepsPlugin(externalOptions))
: configDrivenPlugins.push(externalizeDepsPlugin())
}
}
const hasBytecodePlugin = userPlugins.some(p => p.name === 'vite:bytecode')
if (!hasBytecodePlugin) {
const bytecodeOptions = config.build?.bytecode
if (bytecodeOptions) {
isOptions<BytecodeOptions>(bytecodeOptions)
? configDrivenPlugins.push(bytecodePlugin(bytecodeOptions))
: configDrivenPlugins.push(bytecodePlugin())
}
}
return configDrivenPlugins
}
function isOptions<T extends object>(value: boolean | T): value is T {
return typeof value === 'object' && value !== null
}
export async function loadConfigFromFile( export async function loadConfigFromFile(
configEnv: ConfigEnv, configEnv: ConfigEnv,
@ -377,7 +295,9 @@ export async function loadConfigFromFile(
throw new Error(`config file cannot be named ${configFile}.`) throw new Error(`config file cannot be named ${configFile}.`)
} }
const resolvedPath = configFile ? path.resolve(configFile) : findConfigFile(configRoot) const resolvedPath = configFile
? path.resolve(configFile)
: findConfigFile(configRoot, ['js', 'ts', 'mjs', 'cjs', 'mts', 'cts'])
if (!resolvedPath) { if (!resolvedPath) {
return { return {
@ -390,30 +310,179 @@ export async function loadConfigFromFile(
const isESM = isFilePathESM(resolvedPath) const isESM = isFilePathESM(resolvedPath)
try { try {
const { code, dependencies } = await bundleConfigFile(resolvedPath, isESM) const bundled = await bundleConfigFile(resolvedPath, isESM)
const configExport = await loadConfigFormBundledFile<ElectronViteConfigExport>(resolvedPath, code, isESM) const userConfig = await loadConfigFormBundledFile(configRoot, resolvedPath, bundled.code, isESM)
const config = await (typeof configExport === 'function' ? configExport(configEnv) : configExport) const config = await (typeof userConfig === 'function' ? userConfig(configEnv) : userConfig)
if (!isObject(config)) { if (!isObject(config)) {
throw new Error(`config must export or return an object`) throw new Error(`config must export or return an object`)
} }
if (!ignoreConfigWarning) { const configRequired: string[] = []
const missingFields = ['main', 'renderer', 'preload'].filter(field => !config[field])
if (missingFields.length > 0) { let mainConfig
createLogger(logLevel).warn( if (config.main) {
`${colors.yellow(colors.bold('(!)'))} ${colors.yellow(`${missingFields.join(' and ')} config is missing`)}\n` const mainViteConfig = config.main
) mainConfig = await (typeof mainViteConfig === 'function' ? mainViteConfig(configEnv) : mainViteConfig)
if (!isObject(mainConfig)) {
throw new Error(`main config must export or return an object`)
} }
} else {
configRequired.push('main')
}
let rendererConfig
if (config.renderer) {
const rendererViteConfig = config.renderer
rendererConfig = await (typeof rendererViteConfig === 'function'
? rendererViteConfig(configEnv)
: rendererViteConfig)
if (!isObject(rendererConfig)) {
throw new Error(`renderer config must export or return an object`)
}
} else {
configRequired.push('renderer')
}
let preloadConfig
if (config.preload) {
const preloadViteConfig = config.preload
preloadConfig = await (typeof preloadViteConfig === 'function' ? preloadViteConfig(configEnv) : preloadViteConfig)
if (!isObject(preloadConfig)) {
throw new Error(`preload config must export or return an object`)
}
} else {
configRequired.push('preload')
}
if (!ignoreConfigWarning && configRequired.length > 0) {
createLogger(logLevel).warn(colors.yellow(`${configRequired.join(' and ')} config is missing`))
} }
return { return {
path: normalizePath(resolvedPath), path: normalizePath(resolvedPath),
config, config: {
dependencies main: mainConfig,
renderer: rendererConfig,
preload: preloadConfig
},
dependencies: bundled.dependencies
} }
} catch (e) { } catch (e) {
createLogger(logLevel).error(colors.red(`failed to load config from ${resolvedPath}`), { error: e as Error }) createLogger(logLevel).error(colors.red(`failed to load config from ${resolvedPath}`), { error: e as Error })
throw e throw e
} }
} }
function findConfigFile(configRoot: string, extensions: string[]): string {
for (const ext of extensions) {
const configFile = path.resolve(configRoot, `${CONFIG_FILE_NAME}.${ext}`)
if (fs.existsSync(configFile)) {
return configFile
}
}
return ''
}
async function bundleConfigFile(fileName: string, isESM: boolean): Promise<{ code: string; dependencies: string[] }> {
const dirnameVarName = '__electron_vite_injected_dirname'
const filenameVarName = '__electron_vite_injected_filename'
const importMetaUrlVarName = '__electron_vite_injected_import_meta_url'
const result = await build({
absWorkingDir: process.cwd(),
entryPoints: [fileName],
write: false,
target: ['node20'],
platform: 'node',
bundle: true,
format: isESM ? 'esm' : 'cjs',
sourcemap: false,
metafile: true,
define: {
__dirname: dirnameVarName,
__filename: filenameVarName,
'import.meta.url': importMetaUrlVarName
},
plugins: [
{
name: 'externalize-deps',
setup(build): void {
build.onResolve({ filter: /.*/ }, args => {
const id = args.path
if (id[0] !== '.' && !path.isAbsolute(id)) {
return {
external: true
}
}
return null
})
}
},
{
name: 'replace-import-meta',
setup(build): void {
build.onLoad({ filter: /\.[cm]?[jt]s$/ }, async args => {
const contents = await fs.promises.readFile(args.path, 'utf8')
const injectValues =
`const ${dirnameVarName} = ${JSON.stringify(path.dirname(args.path))};` +
`const ${filenameVarName} = ${JSON.stringify(args.path)};` +
`const ${importMetaUrlVarName} = ${JSON.stringify(pathToFileURL(args.path).href)};`
return {
loader: args.path.endsWith('ts') ? 'ts' : 'js',
contents: injectValues + contents
}
})
}
}
]
})
const { text } = result.outputFiles[0]
return {
code: text,
dependencies: result.metafile ? Object.keys(result.metafile.inputs) : []
}
}
interface NodeModuleWithCompile extends NodeModule {
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
_compile(code: string, filename: string): any
}
const _require = createRequire(import.meta.url)
async function loadConfigFormBundledFile(
configRoot: string,
configFile: string,
bundledCode: string,
isESM: boolean
): Promise<ElectronViteConfigExport> {
if (isESM) {
const fileNameTmp = path.resolve(configRoot, `${CONFIG_FILE_NAME}.${Date.now()}.mjs`)
fs.writeFileSync(fileNameTmp, bundledCode)
const fileUrl = pathToFileURL(fileNameTmp)
try {
return (await import(fileUrl.href)).default
} finally {
try {
fs.unlinkSync(fileNameTmp)
} catch {}
}
} else {
const extension = path.extname(configFile)
const realFileName = fs.realpathSync(configFile)
const loaderExt = extension in _require.extensions ? extension : '.js'
const defaultLoader = _require.extensions[loaderExt]!
_require.extensions[loaderExt] = (module: NodeModule, filename: string): void => {
if (filename === realFileName) {
;(module as NodeModuleWithCompile)._compile(bundledCode, filename)
} else {
defaultLoader(module, filename)
}
}
delete _require.cache[_require.resolve(configFile)]
const raw = _require(configFile)
_require.extensions[loaderExt] = defaultLoader
return raw.__esModule ? raw.default : raw
}
}

View File

@ -69,11 +69,6 @@ export function getElectronNodeTarget(): string {
const electronVer = getElectronMajorVer() const electronVer = getElectronMajorVer()
const nodeVer = { const nodeVer = {
'44': '24.18',
'43': '24.17',
'42': '24.15',
'41': '24.14',
'40': '24.14',
'39': '22.20', '39': '22.20',
'38': '22.19', '38': '22.19',
'37': '22.16', '37': '22.16',
@ -105,11 +100,6 @@ export function getElectronChromeTarget(): string {
const electronVer = getElectronMajorVer() const electronVer = getElectronMajorVer()
const chromeVer = { const chromeVer = {
'44': '152',
'43': '150',
'42': '148',
'41': '146',
'40': '144',
'39': '142', '39': '142',
'38': '140', '38': '140',
'37': '138', '37': '138',
@ -165,36 +155,7 @@ 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' })
// `code` is null when the child was terminated by a signal, and `process.exit` ignores ps.on('close', process.exit)
// 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

@ -1,130 +0,0 @@
import path from 'node:path'
import fs from 'node:fs'
import { pathToFileURL } from 'node:url'
import { createRequire } from 'node:module'
import { rolldown } from 'rolldown'
import type { RolldownOutput, OutputChunk } from 'rolldown'
const CONFIG_FILE_NAME = 'electron.vite.config'
export function findConfigFile(
configRoot: string,
extensions: string[] = ['js', 'ts', 'mjs', 'cjs', 'mts', 'cts']
): string {
for (const ext of extensions) {
const configFile = path.resolve(configRoot, `${CONFIG_FILE_NAME}.${ext}`)
if (fs.existsSync(configFile)) {
return configFile
}
}
return ''
}
export async function bundleConfigFile(
fileName: string,
isESM: boolean
): Promise<{ code: string; dependencies: string[] }> {
const dirnameVarName = '__electron_vite_injected_dirname'
const filenameVarName = '__electron_vite_injected_filename'
const importMetaUrlVarName = '__electron_vite_injected_import_meta_url'
const bundle = await rolldown({
input: path.resolve(fileName),
platform: 'node',
transform: {
target: 'node20',
define: {
__dirname: dirnameVarName,
__filename: filenameVarName,
'import.meta.url': importMetaUrlVarName
}
},
plugins: [
{
name: 'externalize-deps',
resolveId(id) {
if (id[0] !== '.' && !path.isAbsolute(id)) {
return { id, external: true }
}
return null
}
},
{
name: 'inject-file-scope-variables',
transform: {
filter: { id: /\.[cm]?[jt]s$/ },
handler(code, id) {
const injectValues =
`const ${dirnameVarName} = ${JSON.stringify(path.dirname(id))};` +
`const ${filenameVarName} = ${JSON.stringify(id)};` +
`const ${importMetaUrlVarName} = ${JSON.stringify(pathToFileURL(id).href)};`
return { code: injectValues + code, map: null }
}
}
}
]
})
let result: RolldownOutput
try {
result = await bundle.generate({
format: isESM ? 'esm' : 'cjs',
sourcemap: 'inline',
sourcemapPathTransform(relative, sourcemapPath) {
return path.resolve(path.dirname(sourcemapPath), relative)
},
codeSplitting: false
})
} finally {
await bundle.close()
}
const entryChunk = result.output.find((chunk): chunk is OutputChunk => chunk.type === 'chunk' && chunk.isEntry)
if (!entryChunk) {
throw new Error(`Failed to bundle config file: ${fileName}`)
}
return {
code: entryChunk.code,
dependencies: entryChunk.moduleIds.filter(id => !id.startsWith('\0'))
}
}
interface NodeModuleWithCompile extends NodeModule {
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
_compile(code: string, filename: string): any
}
const _require = createRequire(import.meta.url)
export async function loadConfigFormBundledFile<T>(fileName: string, bundledCode: string, isESM: boolean): Promise<T> {
if (isESM) {
const fileNameTmp = `${fileName}.${Date.now()}-${Math.random().toString(16).slice(2)}.mjs`
fs.writeFileSync(fileNameTmp, bundledCode)
const fileUrl = pathToFileURL(fileNameTmp)
try {
return (await import(fileUrl.href)).default
} finally {
try {
fs.unlinkSync(fileNameTmp)
} catch {}
}
} else {
const extension = path.extname(fileName)
const realFileName = fs.realpathSync(fileName)
const loaderExt = extension in _require.extensions ? extension : '.js'
const defaultLoader = _require.extensions[loaderExt]!
_require.extensions[loaderExt] = (module: NodeModule, filename: string): void => {
if (filename === realFileName) {
;(module as NodeModuleWithCompile)._compile(bundledCode, filename)
} else {
defaultLoader(module, filename)
}
}
delete _require.cache[_require.resolve(fileName)]
const raw = _require(fileName)
_require.extensions[loaderExt] = defaultLoader
return raw.__esModule ? raw.default : raw
}
}

View File

@ -1,6 +1,7 @@
import path from 'node:path' import path from 'node:path'
import fs from 'node:fs/promises' import fs from 'node:fs/promises'
import { type Plugin, type Environment, normalizePath } from 'vite' import type { SourceMapInput } from 'rollup'
import { type Plugin } from 'vite'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { cleanUrl, getHash, toRelativePath } from '../utils' import { cleanUrl, getHash, toRelativePath } from '../utils'
import { supportImportMetaPaths } from '../electron' import { supportImportMetaPaths } from '../electron'
@ -25,18 +26,23 @@ export default async function loadWasm(file, importObject = {}) {
} }
` `
const assetCache = new WeakMap<Environment, Map<string, string>>()
const publicAssetPathCache = new WeakMap<Environment, Map<string, string>>()
export default function assetPlugin(): Plugin { export default function assetPlugin(): Plugin {
let publicDir = ''
let outDir = ''
const publicAssetPathCache = new Map<string, string>()
const assetCache = new Map<string, string>()
const isImportMetaPathSupported = supportImportMetaPaths() const isImportMetaPathSupported = supportImportMetaPaths()
return { return {
name: 'vite:node-asset', name: 'vite:node-asset',
apply: 'build', apply: 'build',
enforce: 'pre', enforce: 'pre',
buildStart(): void { buildStart(): void {
publicAssetPathCache.set(this.environment, new Map()) publicAssetPathCache.clear()
assetCache.set(this.environment, new Map()) assetCache.clear()
},
configResolved(config): void {
publicDir = config.publicDir
outDir = config.build.outDir
}, },
resolveId(id): string | void { resolveId(id): string | void {
if (id === wasmHelperId) { if (id === wasmHelperId) {
@ -53,21 +59,15 @@ export default function assetPlugin(): Plugin {
} }
let referenceId: string let referenceId: string
const file = cleanUrl(id) const file = cleanUrl(id)
const { publicDir } = this.environment.config
if (publicDir && file.startsWith(publicDir)) { if (publicDir && file.startsWith(publicDir)) {
const hash = getHash(file) const hash = getHash(file)
const publicAssetPathMap = publicAssetPathCache.get(this.environment)! if (!publicAssetPathCache.get(hash)) {
if (!publicAssetPathMap.get(hash)) { publicAssetPathCache.set(hash, file)
publicAssetPathMap.set(hash, file)
} }
referenceId = `__VITE_NODE_PUBLIC_ASSET__${hash}__` referenceId = `__VITE_NODE_PUBLIC_ASSET__${hash}__`
} else { } else {
const cache = assetCache.get(this.environment)! const cached = assetCache.get(file)
const cached = cache.get(file)
if (cached) { if (cached) {
referenceId = cached referenceId = cached
} else { } else {
@ -78,7 +78,7 @@ export default function assetPlugin(): Plugin {
source: source as unknown as Uint8Array source: source as unknown as Uint8Array
}) })
referenceId = `__VITE_NODE_ASSET__${hash}__` referenceId = `__VITE_NODE_ASSET__${hash}__`
cache.set(file, referenceId) assetCache.set(file, referenceId)
} }
} }
@ -105,7 +105,7 @@ export default function assetPlugin(): Plugin {
export default importObject => loadWasm(${referenceId}, importObject)` export default importObject => loadWasm(${referenceId}, importObject)`
} }
}, },
renderChunk(code, chunk, { sourcemap, dir }) { renderChunk(code, chunk, { sourcemap }): { code: string; map: SourceMapInput } | null {
let match: RegExpExecArray | null let match: RegExpExecArray | null
let s: MagicString | undefined let s: MagicString | undefined
@ -121,14 +121,12 @@ export default function assetPlugin(): Plugin {
}) })
} }
const publicAssetPathMap = publicAssetPathCache.get(this.environment)!
nodePublicAssetRE.lastIndex = 0 nodePublicAssetRE.lastIndex = 0
while ((match = nodePublicAssetRE.exec(code))) { while ((match = nodePublicAssetRE.exec(code))) {
s ||= new MagicString(code) s ||= new MagicString(code)
const [full, hash] = match const [full, hash] = match
const filename = publicAssetPathMap.get(hash)! const filename = publicAssetPathCache.get(hash)!
const outputFilepath = toRelativePath(filename, normalizePath(path.join(dir!, chunk.fileName))) const outputFilepath = toRelativePath(filename, path.join(outDir, chunk.fileName))
const replacement = JSON.stringify(outputFilepath) const replacement = JSON.stringify(outputFilepath)
s.overwrite(match.index, match.index + full.length, replacement, { s.overwrite(match.index, match.index + full.length, replacement, {
contentOnly: true contentOnly: true

View File

@ -1,89 +1,156 @@
import path from 'node:path' import path from 'node:path'
import { randomBytes } from 'node:crypto' import { spawn } from 'node:child_process'
import { createRequire } from 'node:module' import { createRequire } from 'node:module'
import colors from 'picocolors' import colors from 'picocolors'
import { type Plugin, type Rolldown, normalizePath } from 'vite' import { type Plugin, type Logger, type LibraryOptions, normalizePath } from 'vite'
import type * as Babel from '@babel/core' import * as babel from '@babel/core'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { toRelativePath, resolveBuildOutputs } from '../utils' import type { SourceMapInput, OutputChunk, OutputOptions } from 'rollup'
import type { BytecodeCompiler, BytecodeTarget } from '../bytecodeCompiler' import { getElectronPath } from '../electron'
import { toRelativePath } from '../utils'
// 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 getBytecodeCompiler(plugins: readonly Plugin[]): BytecodeCompiler | undefined { function getBytecodeCompilerPath(): string {
const plugin = plugins.find(plugin => plugin.name === 'vite:electron-bytecode-build') return path.join(path.dirname(_require.resolve('electron-vite/package.json')), 'bin', 'electron-bytecode.cjs')
return (plugin?.api as { compiler?: BytecodeCompiler } | undefined)?.compiler
} }
// Versioned envelope: magic/version (8), UTF-16 source length (4), module ID (16), V8 cache. function compileToBytecode(code: string): Promise<Buffer> {
// Keep our metadata separate from the private V8 cache header. return new Promise((resolve, reject) => {
async function compileToBytecode(compiler: BytecodeCompiler, target: BytecodeTarget, code: string): Promise<Buffer> { let data = Buffer.from([])
const id = randomBytes(16)
// Reserve enough source space for an untruncated, per-artifact placeholder identity. const electronPath = getElectronPath()
const source = code + '\n/*' + id.toString('hex') + '*/' const bytecodePath = getBytecodeCompilerPath()
const result = await compiler.compile(target, source)
const header = Buffer.alloc(28) const proc = spawn(electronPath, [bytecodePath], {
header.write('EVBC0001', 0, 'ascii') env: { ELECTRON_RUN_AS_NODE: '1' },
header.writeUInt32LE(result.sourceLength, 8) stdio: ['pipe', 'pipe', 'pipe', 'ipc']
id.copy(header, 12) })
return Buffer.concat([header, Buffer.from(result.cachedData, 'base64')])
if (proc.stdin) {
proc.stdin.write(code)
proc.stdin.end()
}
if (proc.stdout) {
proc.stdout.on('data', chunk => {
data = Buffer.concat([data, chunk])
})
proc.stdout.on('error', err => {
console.error(err)
})
proc.stdout.on('end', () => {
resolve(data)
})
}
if (proc.stderr) {
proc.stderr.on('data', chunk => {
console.error('Error: ', chunk.toString())
})
proc.stderr.on('error', err => {
console.error('Error: ', err)
})
}
proc.addListener('message', message => console.log(message))
proc.addListener('error', err => console.error(err))
proc.on('error', err => reject(err))
proc.on('exit', () => {
resolve(data)
})
})
} }
const bytecodeModuleLoaderCode = [ const bytecodeModuleLoaderCode = [
'"use strict";', `"use strict";`,
'const fs = require("node:fs");', `const fs = require("fs");`,
'const path = require("node:path");', `const path = require("path");`,
'const vm = require("node:vm");', `const vm = require("vm");`,
'const v8 = require("node:v8");', `const v8 = require("v8");`,
'const Module = require("node:module");', `const Module = require("module");`,
'v8.setFlagsFromString("--no-lazy");', `v8.setFlagsFromString("--no-lazy");`,
'v8.setFlagsFromString("--no-flush-bytecode");', `v8.setFlagsFromString("--no-flush-bytecode");`,
'const params = ["exports", "require", "module", "__filename", "__dirname"];', `const FLAG_HASH_OFFSET = 12;`,
'Module._extensions[".jsc"] = Module._extensions[".cjsc"] = function (module, filename) {', `const SOURCE_HASH_OFFSET = 8;`,
' const data = fs.readFileSync(filename);', `let dummyBytecode;`,
' if (data.length <= 28 || data.toString("ascii", 0, 8) !== "EVBC0001") {', `function setFlagHashHeader(bytecodeBuffer) {`,
' throw new Error("Invalid electron-vite bytecode format; rebuild the application: " + filename);', ` if (!dummyBytecode) {`,
' }', ` const script = new vm.Script("", {`,
' const length = data.readUInt32LE(8);', ` produceCachedData: true`,
' const tag = "/*" + data.subarray(12, 28).toString("hex") + "*/";', ` });`,
' if (length < tag.length || length > 0x1fffffff) {', ` dummyBytecode = script.createCachedData();`,
' throw new Error("Invalid bytecode source length: " + filename);', ` }`,
' }', ` dummyBytecode.slice(FLAG_HASH_OFFSET, FLAG_HASH_OFFSET + 4).copy(bytecodeBuffer, FLAG_HASH_OFFSET);`,
' const placeholder = tag + " ".repeat(length - tag.length);', `};`,
' const compiledWrapper = vm.compileFunction(placeholder, params, {', `function getSourceHashHeader(bytecodeBuffer) {`,
' filename,', ` return bytecodeBuffer.slice(SOURCE_HASH_OFFSET, SOURCE_HASH_OFFSET + 4);`,
' cachedData: data.subarray(28)', `};`,
' });', `function buffer2Number(buffer) {`,
' if (compiledWrapper.cachedDataRejected) {', ` let ret = 0;`,
' throw new Error("Invalid or incompatible cached data (cachedDataRejected): " + filename);', ` ret |= buffer[3] << 24;`,
' }', ` ret |= buffer[2] << 16;`,
' const require = function (id) { return module.require(id); };', ` ret |= buffer[1] << 8;`,
' require.resolve = function (request, options) {', ` ret |= buffer[0];`,
' return Module._resolveFilename(request, module, false, options);', ` return ret;`,
' };', `};`,
' if (process.mainModule) require.main = process.mainModule;', `Module._extensions[".jsc"] = Module._extensions[".cjsc"] = function (module, filename) {`,
' require.extensions = Module._extensions;', ` const bytecodeBuffer = fs.readFileSync(filename);`,
' require.cache = Module._cache;', ` if (!Buffer.isBuffer(bytecodeBuffer)) {`,
' return compiledWrapper.call(module.exports, module.exports, require, module, filename, path.dirname(filename));', ` throw new Error("BytecodeBuffer must be a buffer object.");`,
'};' ` }`,
` setFlagHashHeader(bytecodeBuffer);`,
` const length = buffer2Number(getSourceHashHeader(bytecodeBuffer));`,
` let dummyCode = "";`,
` if (length > 1) {`,
` dummyCode = "\\"" + "\\u200b".repeat(length - 2) + "\\"";`,
` }`,
` const script = new vm.Script(dummyCode, {`,
` filename: filename,`,
` lineOffset: 0,`,
` displayErrors: true,`,
` cachedData: bytecodeBuffer`,
` });`,
` if (script.cachedDataRejected) {`,
` throw new Error("Invalid or incompatible cached data (cachedDataRejected)");`,
` }`,
` const require = function (id) {`,
` return module.require(id);`,
` };`,
` require.resolve = function (request, options) {`,
` return Module._resolveFilename(request, module, false, options);`,
` };`,
` if (process.mainModule) {`,
` require.main = process.mainModule;`,
` }`,
` require.extensions = Module._extensions;`,
` require.cache = Module._cache;`,
` const compiledWrapper = script.runInThisContext({`,
` filename: filename,`,
` lineOffset: 0,`,
` columnOffset: 0,`,
` displayErrors: true`,
` });`,
` const dirname = path.dirname(filename);`,
` const args = [module.exports, require, module, filename, dirname, process, global];`,
` return compiledWrapper.apply(module.exports, args);`,
`};`
] ]
const bytecodeChunkExtensionRE = /.(jsc|cjsc)$/ const bytecodeChunkExtensionRE = /.(jsc|cjsc)$/
export interface BytecodeOptions { export interface BytecodeOptions {
chunkAlias?: string | string[] | RegExp chunkAlias?: string | string[]
transformArrowFunctions?: boolean transformArrowFunctions?: boolean
removeBundleJS?: boolean removeBundleJS?: boolean
protectedStrings?: string[] protectedStrings?: string[]
} }
/** /**
* Compile source code to v8 bytecode. * Compile to v8 bytecode to protect source code.
*
* @deprecated use `build.bytecode` config option instead
*/ */
export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null { export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
if (process.env.NODE_ENV_ELECTRON_VITE !== 'production') { if (process.env.NODE_ENV_ELECTRON_VITE !== 'production') {
@ -91,75 +158,67 @@ 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 = chunkAlias instanceof RegExp ? chunkAlias : Array.isArray(chunkAlias) ? chunkAlias : [chunkAlias] const _chunkAlias = Array.isArray(chunkAlias) ? chunkAlias : [chunkAlias]
const transformAllChunks = _chunkAlias.length === 0
const isBytecodeChunk = (chunkName: string): boolean => { const isBytecodeChunk = (chunkName: string): boolean => {
if (_chunkAlias instanceof RegExp) { return transformAllChunks || _chunkAlias.some(alias => alias === chunkName)
_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, { protectedStrings: new Set(protectedStrings) }])
} }
const shouldTransformBytecodeChunk = plugins.length !== 0 const shouldTransformBytecodeChunk = plugins.length !== 0
const _transform = ( const _transform = (code: string, sourceMaps: boolean = false): { code: string; map?: SourceMapInput } | null => {
code: string, const re = babel.transform(code, { plugins, sourceMaps })
sourceMaps: boolean = false return re ? { code: re.code || '', map: re.map } : null
): { code: string; map?: Rolldown.SourceMapInput } | null => {
babel ??= require('@babel/core') as typeof Babel
const re = babel.transformSync(code, { plugins, sourceMaps })
return re ? { code: re.code || '', map: re.map as Rolldown.SourceMapInput } : null
} }
const useStrict = '"use strict";' const useStrict = '"use strict";'
const bytecodeModuleLoader = 'bytecode-loader.cjs' const bytecodeModuleLoader = 'bytecode-loader.cjs'
let logger: Logger
let supported = false let supported = 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
} }
isPreload = config.plugins.some(p => p.name === 'vite:electron-preload-config-preset') logger = config.logger
const useInRenderer = config.plugins.some(p => p.name === 'vite:electron-renderer-config-preset') const useInRenderer = config.plugins.some(p => p.name === 'vite:electron-renderer-preset-config')
if (useInRenderer) { if (useInRenderer) {
config.logger.warn(colors.yellow('bytecodePlugin does not support renderer.')) config.logger.warn(colors.yellow('bytecodePlugin does not support renderer.'))
return return
} }
const build = config.build const build = config.build
const outputs = resolveBuildOutputs(build.rolldownOptions.output, build.lib) const resolvedOutputs = resolveBuildOutputs(build.rollupOptions.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(
'bytecodePlugin does not support ES module, please remove "type": "module" ' + 'bytecodePlugin does not support ES module, please remove "type": "module" ' +
'in package.json or set build.rollupOptions.output.format (or build.rolldownOptions.output.format) to "cjs".' 'in package.json or set the "build.rollupOptions.output.format" option to "cjs".'
) )
) )
} }
supported = output.format === 'cjs' && !useInRenderer supported = output.format === 'cjs' && !useInRenderer
} }
}, },
renderChunk(code, chunk, { sourcemap }): { code: string; map?: Rolldown.SourceMapInput } | null { renderChunk(code, chunk, { sourcemap }): { code: string; map?: SourceMapInput } | null {
if (supported && isBytecodeChunk(chunk.name) && shouldTransformBytecodeChunk) { if (supported && isBytecodeChunk(chunk.name) && shouldTransformBytecodeChunk) {
return _transform(code, !!sourcemap) return _transform(code, !!sourcemap)
} }
@ -170,9 +229,7 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
return return
} }
const _chunks = Object.values(output) const _chunks = Object.values(output)
const chunks = _chunks.filter( const chunks = _chunks.filter(chunk => chunk.type === 'chunk' && isBytecodeChunk(chunk.name)) as OutputChunk[]
chunk => chunk.type === 'chunk' && isBytecodeChunk(chunk.name)
) as Rolldown.OutputChunk[]
if (chunks.length === 0) { if (chunks.length === 0) {
return return
@ -192,10 +249,8 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
const bundles = Object.keys(output) const bundles = Object.keys(output)
if (!bytecodeCompiler) { await Promise.all(
throw new Error('Bytecode compilation requires an electron-vite build') bundles.map(async name => {
}
for (const name of bundles) {
const chunk = output[name] const chunk = output[name]
if (chunk.type === 'chunk') { if (chunk.type === 'chunk') {
let _code = chunk.code let _code = chunk.code
@ -215,7 +270,7 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
} }
} }
if (bytecodeChunks.includes(name)) { if (bytecodeChunks.includes(name)) {
const bytecodeBuffer = await compileToBytecode(bytecodeCompiler, isPreload ? 'preload' : 'main', _code) const bytecodeBuffer = await compileToBytecode(_code)
this.emitFile({ this.emitFile({
type: 'asset', type: 'asset',
fileName: name + 'c', fileName: name + 'c',
@ -247,24 +302,24 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
break break
} }
const moduleInfo = this.getModuleInfo(moduleId) const moduleInfo = this.getModuleInfo(moduleId)
if (moduleInfo) { if (moduleInfo && !moduleInfo.isExternal) {
const { importers, dynamicImporters } = moduleInfo const { importers, dynamicImporters } = moduleInfo
for (const importerId of importers) idsToHandle.add(importerId) for (const importerId of importers) idsToHandle.add(importerId)
for (const importerId of dynamicImporters) idsToHandle.add(importerId) for (const importerId of dynamicImporters) idsToHandle.add(importerId)
} }
} }
if (hasBytecodeMoudle) { _code = hasBytecodeMoudle
const loader = getBytecodeLoaderBlock(chunk.fileName) ? _code.replace(
const strictRE = /^(#![^\n]*\n)?(["'])use strict\2;/ /("use strict";)|('use strict';)/,
_code = strictRE.test(_code) `${useStrict}\n${getBytecodeLoaderBlock(chunk.fileName)}`
? _code.replace(strictRE, `$&\n${loader}`) )
: _code.replace(/^(#![^\n]*\n)?/, `$&${loader}\n`) : _code
}
} }
chunk.code = _code chunk.code = _code
} }
} }
} })
)
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({
@ -278,20 +333,31 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
writeBundle(_, output): void { writeBundle(_, output): void {
if (supported) { if (supported) {
const bytecodeChunkCount = Object.keys(output).filter(chunk => bytecodeChunkExtensionRE.test(chunk)).length const bytecodeChunkCount = Object.keys(output).filter(chunk => bytecodeChunkExtensionRE.test(chunk)).length
this.environment.logger.info(`${colors.green(`✓`)} ${bytecodeChunkCount} chunks compiled into bytecode.`) logger.info(`${colors.green(`✓`)} ${bytecodeChunkCount} chunks compiled into bytecode.`)
} }
} }
} }
} }
interface ProtectStringsPluginState extends Babel.PluginPass { function resolveBuildOutputs(
outputs: OutputOptions | OutputOptions[] | undefined,
libOptions: LibraryOptions | false
): OutputOptions | 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,10 @@ 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 UserConfig, mergeConfig, normalizePath } from 'vite' import { type Plugin, type LibraryOptions, mergeConfig, normalizePath } from 'vite'
import type { OutputOptions } from 'rollup'
import { getElectronNodeTarget, getElectronChromeTarget, supportESM } from '../electron' import { getElectronNodeTarget, getElectronChromeTarget, supportESM } from '../electron'
import { loadPackageData, resolveBuildOutputs } from '../utils' import { loadPackageData } from '../utils'
export interface ElectronPluginOptions { export interface ElectronPluginOptions {
root?: string root?: string
@ -38,6 +39,17 @@ function processEnvDefine(): Record<string, string> {
} }
} }
function resolveBuildOutputs(
outputs: OutputOptions | OutputOptions[] | undefined,
libOptions: LibraryOptions | false
): OutputOptions | 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',
@ -53,24 +65,29 @@ export function electronMainConfigPresetPlugin(options?: ElectronPluginOptions):
const format = pkg.type && pkg.type === 'module' && supportESM() ? 'es' : 'cjs' const format = pkg.type && pkg.type === 'module' && supportESM() ? 'es' : 'cjs'
const defaultConfig = { const defaultConfig = {
resolve: {
browserField: false,
mainFields: ['module', 'jsnext:main', 'jsnext'],
conditions: ['node']
},
build: { build: {
outDir: path.resolve(root, 'out', 'main'), outDir: path.resolve(root, 'out', 'main'),
target: nodeTarget, target: nodeTarget,
assetsDir: 'chunks', assetsDir: 'chunks',
rolldownOptions: { rollupOptions: {
external: ['electron', /^electron\/.+/, ...builtinModules.flatMap(m => [m, `node:${m}`])], external: ['electron', /^electron\/.+/, ...builtinModules.flatMap(m => [m, `node:${m}`])],
output: {} output: {}
}, },
reportCompressedSize: false, reportCompressedSize: false,
minify: false minify: false
} }
} satisfies UserConfig }
const build = config.build || {} const build = config.build || {}
const rolldownOptions = build.rolldownOptions || {} const rollupOptions = build.rollupOptions || {}
if (!rolldownOptions.input) { if (!rollupOptions.input) {
const libOptions = build.lib const libOptions = build.lib
const outputOptions = rolldownOptions.output const outputOptions = rollupOptions.output
defaultConfig.build['lib'] = { defaultConfig.build['lib'] = {
entry: findLibEntry(root, 'main'), entry: findLibEntry(root, 'main'),
formats: formats:
@ -79,10 +96,10 @@ export function electronMainConfigPresetPlugin(options?: ElectronPluginOptions):
: [outputOptions && !Array.isArray(outputOptions) && outputOptions.format ? outputOptions.format : format] : [outputOptions && !Array.isArray(outputOptions) && outputOptions.format ? outputOptions.format : format]
} }
} else { } else {
defaultConfig.build.rolldownOptions.output['format'] = format defaultConfig.build.rollupOptions.output['format'] = format
} }
defaultConfig.build.rolldownOptions.output['assetFileNames'] = path.posix.join( defaultConfig.build.rollupOptions.output['assetFileNames'] = path.posix.join(
build.assetsDir || defaultConfig.build.assetsDir, build.assetsDir || defaultConfig.build.assetsDir,
'[name]-[hash].[ext]' '[name]-[hash].[ext]'
) )
@ -90,6 +107,8 @@ export function electronMainConfigPresetPlugin(options?: ElectronPluginOptions):
const buildConfig = mergeConfig(defaultConfig.build, build) const buildConfig = mergeConfig(defaultConfig.build, build)
config.build = buildConfig config.build = buildConfig
config.resolve = mergeConfig(defaultConfig.resolve, config.resolve || {})
config.define = config.define || {} config.define = config.define || {}
config.define = { ...processEnvDefine(), ...config.define } config.define = { ...processEnvDefine(), ...config.define }
@ -104,8 +123,6 @@ export function electronMainConfigPresetPlugin(options?: ElectronPluginOptions):
config.build.ssr = true config.build.ssr = true
config.build.ssrEmitAssets = true config.build.ssrEmitAssets = true
config.ssr = { ...config.ssr, ...{ noExternal: true } } config.ssr = { ...config.ssr, ...{ noExternal: true } }
config.build.rollupOptions = config.build.rolldownOptions
} }
} }
} }
@ -127,25 +144,25 @@ export function electronMainConfigValidatorPlugin(): Plugin {
} }
const libOptions = build.lib const libOptions = build.lib
const rolldownOptions = build.rolldownOptions const rollupOptions = build.rollupOptions
if (!(libOptions && libOptions.entry) && !rolldownOptions?.input) { if (!(libOptions && libOptions.entry) && !rollupOptions?.input) {
throw new Error( throw new Error(
'An entry point is required in the electron vite main config, ' + 'An entry point is required in the electron vite main config, ' +
'which can be specified using build.lib.entry, build.rollupOptions.input, ' + 'which can be specified using "build.lib.entry" or "build.rollupOptions.input".'
'or build.rolldownOptions.input.'
) )
} }
const outputs = resolveBuildOutputs(rolldownOptions.output, libOptions) const resolvedOutputs = resolveBuildOutputs(rollupOptions.output, libOptions)
if (outputs.length > 0) { if (resolvedOutputs) {
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 output = outputs[0] const outpout = outputs[0]
if (['es', 'cjs'].includes(output.format || '')) { if (['es', 'cjs'].includes(outpout.format || '')) {
if (output.format === 'es' && !supportESM()) { if (outpout.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.'
@ -187,20 +204,20 @@ export function electronPreloadConfigPresetPlugin(options?: ElectronPluginOption
outDir: path.resolve(root, 'out', 'preload'), outDir: path.resolve(root, 'out', 'preload'),
target: nodeTarget, target: nodeTarget,
assetsDir: 'chunks', assetsDir: 'chunks',
rolldownOptions: { rollupOptions: {
external: ['electron', /^electron\/.+/, ...builtinModules.flatMap(m => [m, `node:${m}`])], external: ['electron', /^electron\/.+/, ...builtinModules.flatMap(m => [m, `node:${m}`])],
output: {} output: {}
}, },
reportCompressedSize: false, reportCompressedSize: false,
minify: false minify: false
} }
} satisfies UserConfig }
const build = config.build || {} const build = config.build || {}
const rolldownOptions = build.rolldownOptions || {} const rollupOptions = build.rollupOptions || {}
if (!rolldownOptions.input) { if (!rollupOptions.input) {
const libOptions = build.lib const libOptions = build.lib
const outputOptions = rolldownOptions.output const outputOptions = rollupOptions.output
defaultConfig.build['lib'] = { defaultConfig.build['lib'] = {
entry: findLibEntry(root, 'preload'), entry: findLibEntry(root, 'preload'),
formats: formats:
@ -209,10 +226,10 @@ export function electronPreloadConfigPresetPlugin(options?: ElectronPluginOption
: [outputOptions && !Array.isArray(outputOptions) && outputOptions.format ? outputOptions.format : format] : [outputOptions && !Array.isArray(outputOptions) && outputOptions.format ? outputOptions.format : format]
} }
} else { } else {
defaultConfig.build.rolldownOptions.output['format'] = format defaultConfig.build.rollupOptions.output['format'] = format
} }
defaultConfig.build.rolldownOptions.output['assetFileNames'] = path.posix.join( defaultConfig.build.rollupOptions.output['assetFileNames'] = path.posix.join(
build.assetsDir || defaultConfig.build.assetsDir, build.assetsDir || defaultConfig.build.assetsDir,
'[name]-[hash].[ext]' '[name]-[hash].[ext]'
) )
@ -220,21 +237,23 @@ export function electronPreloadConfigPresetPlugin(options?: ElectronPluginOption
const buildConfig = mergeConfig(defaultConfig.build, build) const buildConfig = mergeConfig(defaultConfig.build, build)
config.build = buildConfig config.build = buildConfig
const outputs = resolveBuildOutputs(config.build.rolldownOptions!.output, config.build.lib) const resolvedOutputs = resolveBuildOutputs(config.build.rollupOptions!.output, config.build.lib || false)
if (outputs.some(({ format }) => format === 'es')) { if (resolvedOutputs) {
if (Array.isArray(config.build.rolldownOptions!.output)) { const outputs = Array.isArray(resolvedOutputs) ? resolvedOutputs : [resolvedOutputs]
config.build.rolldownOptions!.output.forEach(output => {
if (outputs.find(({ format }) => format === 'es')) {
if (Array.isArray(config.build.rollupOptions!.output)) {
config.build.rollupOptions!.output.forEach(output => {
if (output.format === 'es') { if (output.format === 'es') {
output['entryFileNames'] = '[name].mjs' output['entryFileNames'] = '[name].mjs'
output['chunkFileNames'] = '[name]-[hash].mjs' output['chunkFileNames'] = '[name]-[hash].mjs'
} }
}) })
} else { } else {
config.build.rolldownOptions!.output!['entryFileNames'] = '[name].mjs' config.build.rollupOptions!.output!['entryFileNames'] = '[name].mjs'
config.build.rolldownOptions!.output!['chunkFileNames'] = config.build.lib config.build.rollupOptions!.output!['chunkFileNames'] = '[name]-[hash].mjs'
? '[name]-[hash].mjs' }
: path.posix.join(build.assetsDir || defaultConfig.build.assetsDir, '[name]-[hash].mjs')
} }
} }
@ -253,8 +272,6 @@ export function electronPreloadConfigPresetPlugin(options?: ElectronPluginOption
config.build.ssrEmitAssets = true config.build.ssrEmitAssets = true
config.ssr = mergeConfig(defaultConfig.ssr, config.ssr || {}) config.ssr = mergeConfig(defaultConfig.ssr, config.ssr || {})
config.ssr.noExternal = true config.ssr.noExternal = true
config.build.rollupOptions = config.build.rolldownOptions
} }
} }
} }
@ -276,25 +293,25 @@ export function electronPreloadConfigValidatorPlugin(): Plugin {
} }
const libOptions = build.lib const libOptions = build.lib
const rolldownOptions = build.rolldownOptions const rollupOptions = build.rollupOptions
if (!(libOptions && libOptions.entry) && !rolldownOptions?.input) { if (!(libOptions && libOptions.entry) && !rollupOptions?.input) {
throw new Error( throw new Error(
'An entry point is required in the electron vite preload config, ' + 'An entry point is required in the electron vite preload config, ' +
'which can be specified using build.lib.entry, build.rollupOptions.input, ' + 'which can be specified using "build.lib.entry" or "build.rollupOptions.input".'
' or build.rolldownOptions.input.'
) )
} }
const outputs = resolveBuildOutputs(rolldownOptions.output, libOptions) const resolvedOutputs = resolveBuildOutputs(rollupOptions.output, libOptions)
if (outputs.length > 0) { if (resolvedOutputs) {
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 output = outputs[0] const outpout = outputs[0]
if (['es', 'cjs'].includes(output.format || '')) { if (['es', 'cjs'].includes(outpout.format || '')) {
if (output.format === 'es' && !supportESM()) { if (outpout.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.'
@ -341,14 +358,14 @@ export function electronRendererConfigPresetPlugin(options?: ElectronPluginOptio
outDir: path.resolve(root, 'out', 'renderer'), outDir: path.resolve(root, 'out', 'renderer'),
target: chromeTarget, target: chromeTarget,
modulePreload: { polyfill: false }, modulePreload: { polyfill: false },
rolldownOptions: { rollupOptions: {
input: findInput(root) input: findInput(root)
}, },
reportCompressedSize: false, reportCompressedSize: false,
minify: false, minify: false,
emptyOutDir: emptyOutDir() emptyOutDir: emptyOutDir()
} }
} satisfies UserConfig }
if (config.build?.outDir) { if (config.build?.outDir) {
config.build.outDir = path.resolve(root, config.build.outDir) config.build.outDir = path.resolve(root, config.build.outDir)
@ -360,8 +377,6 @@ export function electronRendererConfigPresetPlugin(options?: ElectronPluginOptio
config.envDir = config.envDir || path.resolve(root) config.envDir = config.envDir || path.resolve(root)
config.envPrefix = config.envPrefix || ['RENDERER_VITE_', 'VITE_'] config.envPrefix = config.envPrefix || ['RENDERER_VITE_', 'VITE_']
config.build.rollupOptions = config.build.rolldownOptions
} }
} }
} }
@ -387,12 +402,10 @@ export function electronRendererConfigValidatorPlugin(): Plugin {
} }
} }
const rolldownOptions = build.rolldownOptions const rollupOptions = build.rollupOptions
if (!rolldownOptions.input) { if (!rollupOptions.input) {
config.logger.warn(colors.yellow(`index.html file is not found in ${colors.dim('/src/renderer')} directory.`)) config.logger.warn(colors.yellow(`index.html file is not found in ${colors.dim('/src/renderer')} directory.`))
throw new Error( throw new Error('build.rollupOptions.input option is required in the electron vite renderer config.')
'build.rollupOptions.input or build.rolldownOptions.input option is required in the electron vite renderer config.'
)
} }
} }
} }

View File

@ -1,4 +1,11 @@
/*
* The core of this plugin was conceived by pi0 and is taken from the following repository:
* https://github.com/unjs/unbuild/blob/main/src/builder/plugins/cjs.ts
* license: https://github.com/unjs/unbuild/blob/main/LICENSE
*/
import MagicString from 'magic-string' import MagicString from 'magic-string'
import type { SourceMapInput } from 'rollup'
import type { Plugin } from 'vite' import type { Plugin } from 'vite'
import { supportImportMetaPaths } from '../electron' import { supportImportMetaPaths } from '../electron'
@ -7,22 +14,37 @@ const CJSyntaxRe = /__filename|__dirname|require\(|require\.resolve\(/
const CJSShim_normal = ` const CJSShim_normal = `
// -- CommonJS Shims -- // -- CommonJS Shims --
import __shims_url__ from 'node:url'; import __cjs_url__ from 'node:url';
import __shims_path__ from 'node:path'; import __cjs_path__ from 'node:path';
import __shims_module__ from 'node:module'; import __cjs_mod__ from 'node:module';
const __filename = __shims_url__.fileURLToPath(import.meta.url); const __filename = __cjs_url__.fileURLToPath(import.meta.url);
const __dirname = __shims_path__.dirname(__filename); const __dirname = __cjs_path__.dirname(__filename);
const require = __shims_module__.createRequire(import.meta.url); const require = __cjs_mod__.createRequire(import.meta.url);
` `
const CJSShim_node_20_11 = ` const CJSShim_node_20_11 = `
// -- CommonJS Shims -- // -- CommonJS Shims --
import __shims_module__ from 'node:module'; import __cjs_mod__ from 'node:module';
const __filename = import.meta.filename; const __filename = import.meta.filename;
const __dirname = import.meta.dirname; const __dirname = import.meta.dirname;
const require = __shims_module__.createRequire(import.meta.url); const require = __cjs_mod__.createRequire(import.meta.url);
` `
const ESMStaticImportRe =
/(?<=\s|^|;)import\s*([\s"']*(?<imports>[\p{L}\p{M}\w\t\n\r $*,/{}@.]+)from\s*)?["']\s*(?<specifier>(?<="\s*)[^"]*[^\s"](?=\s*")|(?<='\s*)[^']*[^\s'](?=\s*'))\s*["'][\s;]*/gmu
interface StaticImport {
end: number
}
function findStaticImports(code: string): StaticImport[] {
const matches: StaticImport[] = []
for (const match of code.matchAll(ESMStaticImportRe)) {
matches.push({ end: (match.index || 0) + match[0].length })
}
return matches
}
export default function esmShimPlugin(): Plugin { export default function esmShimPlugin(): Plugin {
const CJSShim = supportImportMetaPaths() ? CJSShim_node_20_11 : CJSShim_normal const CJSShim = supportImportMetaPaths() ? CJSShim_node_20_11 : CJSShim_normal
@ -30,16 +52,13 @@ export default function esmShimPlugin(): Plugin {
name: 'vite:esm-shim', name: 'vite:esm-shim',
apply: 'build', apply: 'build',
enforce: 'post', enforce: 'post',
renderChunk(code, _chunk, { format, sourcemap }) { renderChunk(code, _chunk, { format, sourcemap }): { code: string; map?: SourceMapInput } | null {
if (format === 'es') { if (format === 'es') {
// Rolldown may remove unused shim bindings after this hook; referenced bindings remain available.
if (code.includes(CJSShim) || !CJSyntaxRe.test(code)) { if (code.includes(CJSShim) || !CJSyntaxRe.test(code)) {
return null return null
} }
const lastESMImport = this.parse(code) const lastESMImport = findStaticImports(code).pop()
.body.filter(node => node.type === 'ImportDeclaration')
.pop()
const indexToAppend = lastESMImport ? lastESMImport.end : 0 const indexToAppend = lastESMImport ? lastESMImport.end : 0
const s = new MagicString(code) const s = new MagicString(code)
s.appendRight(indexToAppend, CJSShim) s.appendRight(indexToAppend, CJSShim)

View File

@ -7,9 +7,7 @@ export interface ExternalOptions {
} }
/** /**
* Automatically externalize dependencies. * Automatically externalize dependencies
*
* @deprecated use `build.externalizeDeps` config option instead
*/ */
export function externalizeDepsPlugin(options: ExternalOptions = {}): Plugin | null { export function externalizeDepsPlugin(options: ExternalOptions = {}): Plugin | null {
const { exclude = [], include = [] } = options const { exclude = [], include = [] } = options
@ -33,15 +31,13 @@ export function externalizeDepsPlugin(options: ExternalOptions = {}): Plugin | n
config(config): void { config(config): void {
const defaultConfig = { const defaultConfig = {
build: { build: {
rolldownOptions: { rollupOptions: {
external: deps.length > 0 ? [...deps, new RegExp(`^(${deps.join('|')})/.+`)] : [] external: deps.length > 0 ? [...deps, new RegExp(`^(${deps.join('|')})/.+`)] : []
} }
} }
} }
const buildConfig = mergeConfig(defaultConfig.build, config.build || {}) const buildConfig = mergeConfig(defaultConfig.build, config.build || {})
config.build = buildConfig config.build = buildConfig
config.build.rollupOptions = config.build.rolldownOptions
} }
} }
} }

View File

@ -1,19 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ import type { Plugin } from 'vite'
import { type Plugin, version } from 'vite'
/** export default function importMetaPlugin(): Plugin {
* @deprecated Vite 8 (Rolldown) now provides native support for these
* `import.meta` properties. These properties are polyfilled when the output
* format is CJS.
*
* @see https://rolldown.rs/in-depth/non-esm-output-formats#well-known-import-meta-properties
*/
export default function importMetaPlugin(): Plugin | null {
const currentVersion = version.split('.')
const major = parseInt(currentVersion[0], 10)
if (major >= 8) {
return null
}
return { return {
name: 'vite:import-meta', name: 'vite:import-meta',
apply: 'build', apply: 'build',
@ -30,5 +17,5 @@ export default function importMetaPlugin(): Plugin | null {
} }
return null return null
} }
} as any }
} }

View File

@ -1,22 +1,13 @@
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-function-type */ import { type InlineConfig, type Plugin, type Logger, build as viteBuild, mergeConfig } from 'vite'
import path from 'node:path' import type { InputOptions, RollupOutput } from 'rollup'
import { type InlineConfig, type Plugin, type LogLevel, type Rolldown, build as viteBuild, mergeConfig } from 'vite'
import colors from 'picocolors' import colors from 'picocolors'
import { cleanUrl } from '../utils'
import type { ConfigFactory, PreloadViteConfig, RendererViteConfig } from '../config'
const VIRTUAL_ENTRY_ID = '\0virtual:isolate-entries' const VIRTUAL_ENTRY_ID = '\0virtual:isolate-entries'
const LogLevels: Record<LogLevel, number> = { export default function isolateEntriesPlugin(userConfig: InlineConfig): Plugin {
silent: 0, let logger: Logger
error: 1,
warn: 2,
info: 3
}
export default function isolateEntriesPlugin(factory: ConfigFactory<PreloadViteConfig | RendererViteConfig>): Plugin {
let entries: string[] | { [x: string]: string }[]
let entries: string[] | Record<string, string>
let transformedCount = 0 let transformedCount = 0
const assetCache = new Set<string>() const assetCache = new Set<string>()
@ -24,43 +15,40 @@ export default function isolateEntriesPlugin(factory: ConfigFactory<PreloadViteC
return { return {
name: 'vite:isolate-entries', name: 'vite:isolate-entries',
apply: 'build', apply: 'build',
configResolved(config): void {
options(opts): Rolldown.InputOptions | void { logger = config.logger
},
options(opts): InputOptions | void {
const { input } = opts const { input } = opts
if (input && typeof input === 'object') { if (input && typeof input === 'object') {
if ((Array.isArray(input) && input.length > 0) || Object.keys(input).length > 1) { if ((Array.isArray(input) && input.length > 0) || Object.keys(input).length > 1) {
opts.input = VIRTUAL_ENTRY_ID opts.input = VIRTUAL_ENTRY_ID
entries = Array.isArray(input) ? input : Object.entries(input).map(([key, value]) => ({ [key]: value })) entries = input
return opts return opts
} }
} }
}, },
buildStart(): void { buildStart(): void {
transformedCount = 0 transformedCount = 0
assetCache.clear() assetCache.clear()
}, },
resolveId(id): string | null { resolveId(id): string | null {
if (id === VIRTUAL_ENTRY_ID) { if (id === VIRTUAL_ENTRY_ID) {
return id return id
} }
return null return null
}, },
async load(id): Promise<string | void> { async load(id): Promise<string | void> {
if (id === VIRTUAL_ENTRY_ID) { if (id === VIRTUAL_ENTRY_ID) {
const userConfig = await factory.build(true) const _entries = Array.isArray(entries)
const shouldLog = LogLevels[userConfig.logLevel || 'info'] >= LogLevels.info ? entries
: Object.entries(entries).map(([key, value]) => ({ [key]: value }))
const watchFiles = new Set<string>() const watchFiles = new Set<string>()
for (const entry of _entries) {
for (const entry of entries) { const re = await bundleEntryFile(entry, userConfig, this.meta.watchMode)
const re = await bundleEntryFile(entry, userConfig, shouldLog, transformedCount)
const outputChunks = re.bundles.output const outputChunks = re.bundles.output
for (const chunk of outputChunks) { for (const chunk of outputChunks) {
if (assetCache.has(chunk.fileName)) { if (chunk.type === 'asset' && assetCache.has(chunk.fileName)) {
continue continue
} }
this.emitFile({ this.emitFile({
@ -68,43 +56,27 @@ export default function isolateEntriesPlugin(factory: ConfigFactory<PreloadViteC
fileName: chunk.fileName, fileName: chunk.fileName,
source: chunk.type === 'chunk' ? chunk.code : chunk.source source: chunk.type === 'chunk' ? chunk.code : chunk.source
}) })
if (chunk.type === 'asset') {
assetCache.add(chunk.fileName) assetCache.add(chunk.fileName)
} }
if (this.meta.watchMode) {
const moduleIds = new Set<string>()
for (const chunk of re.bundles.output) {
if (chunk.type === 'chunk') {
for (const id of chunk.moduleIds) {
if (!id.startsWith('\0')) {
moduleIds.add(cleanUrl(id))
} }
for (const id of re.watchFiles) {
watchFiles.add(id)
} }
}
}
for (const id of moduleIds) {
this.addWatchFile(id)
}
}
transformedCount += re.transformedCount transformedCount += re.transformedCount
} }
for (const id of watchFiles) { for (const id of watchFiles) {
this.addWatchFile(id) this.addWatchFile(id)
} }
return ` return `
// This is the virtual entry file // This is the virtual entry file
console.log(1)` console.log(1)`
} }
}, },
renderStart(): void { renderStart(): void {
clearLine(-1) clearLine()
this.environment.logger.info(`${colors.green(`✓`)} ${transformedCount} modules transformed.`) logger.info(`${colors.green(`✓`)} ${transformedCount} modules transformed.`)
}, },
generateBundle(_, bundle): void { generateBundle(_, bundle): void {
for (const chunkName in bundle) { for (const chunkName in bundle) {
if (chunkName.includes('virtual_isolate-entries')) { if (chunkName.includes('virtual_isolate-entries')) {
@ -118,82 +90,59 @@ export default function isolateEntriesPlugin(factory: ConfigFactory<PreloadViteC
async function bundleEntryFile( async function bundleEntryFile(
input: string | Record<string, string>, input: string | Record<string, string>,
config: InlineConfig, config: InlineConfig,
shouldLog: boolean, watch: boolean
preTransformedCount: number ): Promise<{ bundles: RollupOutput; watchFiles: string[]; transformedCount: number }> {
): Promise<{ bundles: Rolldown.RolldownOutput; transformedCount: number }> { const moduleIds: string[] = []
const transformReporter = transformReporterPlugin(preTransformedCount, shouldLog) let transformedCount = 0
const viteConfig = mergeConfig(config, { const viteConfig = mergeConfig(config, {
build: { build: {
write: false, write: false,
watch: false watch: false
}, },
plugins: [transformReporter], plugins: [
{
name: 'vite:transform-counter',
transform(): void {
transformedCount++
}
} as Plugin,
...(watch
? [
{
name: 'vite:get-watch-files',
buildEnd(): void {
const allModuleIds = Array.from(this.getModuleIds())
const sourceFiles = allModuleIds.filter(id => {
const info = this.getModuleInfo(id)
return info && !info.isExternal
})
moduleIds.push(...sourceFiles)
}
} as Plugin
]
: [])
],
logLevel: 'warn', logLevel: 'warn',
configFile: false configFile: false
}) as InlineConfig }) as InlineConfig
// rewrite the input instead of merging // rewrite the input instead of merging
viteConfig.build!.rolldownOptions!.input = input viteConfig.build!.rollupOptions!.input = input
const bundles = (await viteBuild(viteConfig)) as Rolldown.RolldownOutput const bundles = await viteBuild(viteConfig)
return { return {
bundles: bundles, bundles: bundles as RollupOutput,
transformedCount: transformReporter?.api?.getTransformedCount() || 0 watchFiles: moduleIds,
transformedCount
} }
} }
function transformReporterPlugin( function clearLine(): void {
preTransformedCount = 0, process.stdout.moveCursor(0, -1)
shouldLog = true
): Plugin<{ getTransformedCount: () => number }> {
let transformedCount = 0
const log = throttle((id, root) => {
writeLine(`transforming (${preTransformedCount + transformedCount}) ${colors.dim(path.relative(root, id))}`)
})
return {
name: 'vite:transform-reporter',
transform(_, id) {
transformedCount++
if (!shouldLog) return
if (id.includes('?')) return
log(id, this.environment.config.root)
},
api: {
getTransformedCount() {
return transformedCount
}
}
}
}
function writeLine(output: string): void {
if (!process.stdout.isTTY || process.env.CI) return
clearLine()
if (output.length < process.stdout.columns) {
process.stdout.write(output)
} else {
process.stdout.write(output.substring(0, process.stdout.columns - 1))
}
}
function clearLine(move: number = 0): void {
if (!process.stdout.isTTY || process.env.CI) return
if (move < 0) {
process.stdout.moveCursor(0, move)
}
process.stdout.clearLine(0) process.stdout.clearLine(0)
process.stdout.cursorTo(0) process.stdout.cursorTo(0)
} }
function throttle(fn: Function) {
let timerHandle: NodeJS.Timeout | null = null
return (...args: any[]) => {
if (timerHandle) return
fn(...args)
timerHandle = setTimeout(() => {
timerHandle = null
}, 100)
}
}

View File

@ -1,15 +1,16 @@
import { type Plugin, type InlineConfig, type Rolldown, build as viteBuild, mergeConfig } from 'vite' import path from 'node:path'
import { type Plugin, type InlineConfig, build as viteBuild, mergeConfig } from 'vite'
import type { SourceMapInput, RollupOutput, OutputOptions } from 'rollup'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { cleanUrl, toRelativePath } from '../utils' import { cleanUrl, toRelativePath } from '../utils'
import { supportImportMetaPaths } from '../electron' import { supportImportMetaPaths } from '../electron'
import type { ConfigFactory, MainViteConfig } from '../config'
const modulePathRE = /__VITE_MODULE_PATH__([\w$]+)__/g const modulePathRE = /__VITE_MODULE_PATH__([\w$]+)__/g
/** /**
* Resolve `?modulePath` import and return the module bundle path. * Resolve `?modulePath` import and return the module bundle path.
*/ */
export default function modulePathPlugin(factory: ConfigFactory<MainViteConfig>): Plugin { export default function modulePathPlugin(config: InlineConfig): Plugin {
const isImportMetaPathSupported = supportImportMetaPaths() const isImportMetaPathSupported = supportImportMetaPaths()
const assetCache = new Set<string>() const assetCache = new Set<string>()
return { return {
@ -22,16 +23,15 @@ export default function modulePathPlugin(factory: ConfigFactory<MainViteConfig>)
async load(id): Promise<string | void> { async load(id): Promise<string | void> {
if (id.endsWith('?modulePath')) { if (id.endsWith('?modulePath')) {
// id resolved by Vite resolve plugin // id resolved by Vite resolve plugin
const config = await factory.build(true) const re = await bundleEntryFile(cleanUrl(id), config, this.meta.watchMode)
const bundles = await bundleEntryFile(cleanUrl(id), config) const [outputChunk, ...outputChunks] = re.bundles.output
const [outputChunk, ...outputChunks] = bundles.output
const hash = this.emitFile({ const hash = this.emitFile({
type: 'asset', type: 'asset',
fileName: outputChunk.fileName, fileName: outputChunk.fileName,
source: outputChunk.code source: outputChunk.code
}) })
for (const chunk of outputChunks) { for (const chunk of outputChunks) {
if (assetCache.has(chunk.fileName)) { if (chunk.type === 'asset' && assetCache.has(chunk.fileName)) {
continue continue
} }
this.emitFile({ this.emitFile({
@ -39,23 +39,13 @@ export default function modulePathPlugin(factory: ConfigFactory<MainViteConfig>)
fileName: chunk.fileName, fileName: chunk.fileName,
source: chunk.type === 'chunk' ? chunk.code : chunk.source source: chunk.type === 'chunk' ? chunk.code : chunk.source
}) })
if (chunk.type === 'asset') {
assetCache.add(chunk.fileName) assetCache.add(chunk.fileName)
} }
if (this.meta.watchMode) {
const moduleIds = new Set<string>()
for (const chunk of bundles.output) {
if (chunk.type === 'chunk') {
for (const id of chunk.moduleIds) {
if (!id.startsWith('\0')) {
moduleIds.add(cleanUrl(id))
} }
} for (const id of re.watchFiles) {
}
}
for (const id of moduleIds) {
this.addWatchFile(id) this.addWatchFile(id)
} }
}
const refId = `__VITE_MODULE_PATH__${hash}__` const refId = `__VITE_MODULE_PATH__${hash}__`
const dirnameExpr = isImportMetaPathSupported ? 'import.meta.dirname' : '__dirname' const dirnameExpr = isImportMetaPathSupported ? 'import.meta.dirname' : '__dirname'
return ` return `
@ -63,7 +53,7 @@ export default function modulePathPlugin(factory: ConfigFactory<MainViteConfig>)
export default join(${dirnameExpr}, ${refId})` export default join(${dirnameExpr}, ${refId})`
} }
}, },
renderChunk(code, chunk, { sourcemap }): { code: string; map: Rolldown.SourceMapInput } | null { renderChunk(code, chunk, { sourcemap }): { code: string; map: SourceMapInput } | null {
let match: RegExpExecArray | null let match: RegExpExecArray | null
let s: MagicString | undefined let s: MagicString | undefined
@ -91,36 +81,53 @@ export default function modulePathPlugin(factory: ConfigFactory<MainViteConfig>)
} }
} }
async function bundleEntryFile(input: string, config: InlineConfig): Promise<Rolldown.RolldownOutput> { async function bundleEntryFile(
input: string,
config: InlineConfig,
watch: boolean
): Promise<{ bundles: RollupOutput; watchFiles: string[] }> {
const moduleIds: string[] = []
const viteConfig = mergeConfig(config, { const viteConfig = mergeConfig(config, {
build: { build: {
rollupOptions: { input },
write: false, write: false,
watch: false watch: false
}, },
plugins: [ plugins: [
{ {
name: 'vite:entry-file-name', name: 'vite:entry-file-name',
outputOptions(output): Rolldown.OutputOptions { outputOptions(output): OutputOptions {
// Prevent output conflicts if (typeof output.entryFileNames !== 'function' && output.entryFileNames) {
output.hashCharacters = 'hex' output.entryFileNames = '[name]-[hash]' + path.extname(output.entryFileNames)
// Assign chunkFileNames to entryFileNames to treat sub-entries as internal chunks }
output.entryFileNames = output.chunkFileNames
return output return output
} }
},
...(watch
? [
{
name: 'vite:get-watch-files',
buildEnd(): void {
const allModuleIds = Array.from(this.getModuleIds())
const sourceFiles = allModuleIds.filter(id => {
const info = this.getModuleInfo(id)
return info && !info.isExternal
})
moduleIds.push(...sourceFiles)
} }
] as Plugin[], } as Plugin
]
: [])
],
logLevel: 'warn', logLevel: 'warn',
configFile: false configFile: false
}) as InlineConfig })
// rewrite the input instead of merging
const buildOptions = viteConfig.build!
buildOptions.rolldownOptions = {
...buildOptions.rolldownOptions,
input
}
const bundles = await viteBuild(viteConfig) const bundles = await viteBuild(viteConfig)
return bundles as Rolldown.RolldownOutput return {
bundles: bundles as RollupOutput,
watchFiles: moduleIds
}
} }

View File

@ -79,16 +79,12 @@ export type SwcOptions = {
export function swcPlugin(options: SwcOptions = {}): Plugin { export function swcPlugin(options: SwcOptions = {}): Plugin {
const filter = createFilter(options.include || /\.(m?ts|[jt]sx)$/, options.exclude || /\.js$/) const filter = createFilter(options.include || /\.(m?ts|[jt]sx)$/, options.exclude || /\.js$/)
let sourcemap: boolean | 'inline' = false let sourcemap: boolean | 'inline' = false
let minify: boolean | 'esbuild' | 'oxc' | 'terser' = false let minify: boolean | 'esbuild' | 'terser' = false
return { return {
name: 'vite:swc', name: 'vite:swc',
config(): UserConfig { config(): UserConfig {
// Vite 8 defaults to Oxc for transform; `esbuild: false` is ignored and
// warns. SWC replaces both paths when this plugin is active.
// See https://github.com/alex8088/electron-vite/issues/916
return { return {
esbuild: false, esbuild: false
oxc: false
} }
}, },
async configResolved(resolvedConfig): Promise<void> { async configResolved(resolvedConfig): Promise<void> {
@ -105,7 +101,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' || minify === 'oxc') { if (!minify || minify === 'terser') {
return null return null
} }
const result = await transformWithSWC(code, chunk.fileName, { const result = await transformWithSWC(code, chunk.fileName, {

View File

@ -1,4 +1,5 @@
import type { Plugin } from 'vite' import type { Plugin } from 'vite'
import type { SourceMapInput } from 'rollup'
import MagicString from 'magic-string' import MagicString from 'magic-string'
import { cleanUrl, toRelativePath } from '../utils' import { cleanUrl, toRelativePath } from '../utils'
@ -35,7 +36,7 @@ export default function workerPlugin(): Plugin {
} }
} }
}, },
renderChunk(code, chunk, { sourcemap }) { renderChunk(code, chunk, { sourcemap }): { code: string; map: SourceMapInput } | null {
let match: RegExpExecArray | null let match: RegExpExecArray | null
let s: MagicString | undefined let s: MagicString | undefined

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 { type LibraryOptions, type Rolldown, loadEnv as viteLoadEnv } from 'vite' import { 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,23 +32,6 @@ 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
@ -128,65 +111,3 @@ export function deepClone<T>(value: T): DeepWritable<T> {
} }
return value as DeepWritable<T> return value as DeepWritable<T>
} }
/**
* Isolate configuration data without cloning plugins or runtime instances.
* Plugins may capture their own object in hooks, so their identity must survive.
*/
export function cloneConfig<T>(config: T): T {
const seen = new WeakMap<object, unknown>()
const pluginArrays = new WeakMap<object, unknown>()
function clone(value: unknown, plugins = false): unknown {
if (value === null || typeof value !== 'object') return value
const cache = plugins ? pluginArrays : seen
if (cache.has(value)) return cache.get(value)
if (Array.isArray(value)) {
const result: unknown[] = new Array(value.length)
cache.set(value, result)
value.forEach((item, index) => {
result[index] = clone(item, plugins)
})
return result
}
// Preserve plugin objects, promises and other opaque plugin options.
if (plugins) return value
if (value instanceof RegExp) {
const result = new RegExp(value)
result.lastIndex = value.lastIndex
cache.set(value, result)
return result
}
const prototype = Object.getPrototypeOf(value)
if (prototype !== Object.prototype && prototype !== null) return value
const result = Object.create(prototype) as Record<PropertyKey, unknown>
cache.set(value, result)
for (const key of Reflect.ownKeys(value)) {
if (!Object.prototype.propertyIsEnumerable.call(value, key)) continue
Object.defineProperty(result, key, {
value: clone((value as Record<PropertyKey, unknown>)[key], key === 'plugins'),
enumerable: true,
configurable: true,
writable: true
})
}
return result
}
return clone(config) as T
}
type AsyncFlatten<T extends unknown[]> = T extends (infer U)[] ? Exclude<Awaited<U>, U[]>[] : never
export async function asyncFlatten<T extends unknown[]>(arr: T): Promise<AsyncFlatten<T>> {
do {
arr = (await Promise.all(arr)).flat(Infinity) as any
} while (arr.some((v: any) => v?.then))
return arr as unknown[] as AsyncFlatten<T>
}