mirror of
https://github.com/alex8088/electron-vite.git
synced 2026-11-02 06:42:22 +08:00
refactor!: invoke the bytecode compiler only once to compile all code
This commit is contained in:
parent
4c4bdf5248
commit
36a0147a6d
@ -11,6 +11,7 @@ const channel = 'electron-vite:bytecode:rpc'
|
||||
const readyChannel = 'electron-vite:bytecode:ready'
|
||||
let win
|
||||
let rendererRPC
|
||||
let rendererStarting
|
||||
let stopping = false
|
||||
|
||||
function fail(error) {
|
||||
@ -74,17 +75,18 @@ function createRenderer() {
|
||||
})
|
||||
}
|
||||
|
||||
// Register readiness immediately, but only resolve it after the selected backend is usable.
|
||||
const initialized = app.whenReady().then(async () => {
|
||||
if (process.env.ELECTRON_VITE_RENDERER === '1' || process.env.ELECTRON_VITE_RENDERER === 'true') {
|
||||
await createRenderer()
|
||||
parentRPC.register('compile', (...args) => rendererRPC.request('compile', ...args))
|
||||
} else {
|
||||
parentRPC.register('compile', compile)
|
||||
}
|
||||
return true
|
||||
})
|
||||
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 => {
|
||||
|
||||
31
src/build.ts
31
src/build.ts
@ -1,5 +1,13 @@
|
||||
import { build as viteBuild } from 'vite'
|
||||
import { type InlineConfig, resolveConfig } from './config'
|
||||
import { type InlineConfig, type MainViteConfig, type PreloadViteConfig, resolveConfig } from './config'
|
||||
import { BytecodeBuild, runWithBytecodeBuild } from './bytecode/build'
|
||||
import { asyncFlatten } from './utils'
|
||||
|
||||
async function useBytecode(config?: MainViteConfig | PreloadViteConfig): Promise<boolean> {
|
||||
if (!config) return false
|
||||
const plugins = await asyncFlatten(config.plugins || [])
|
||||
return plugins.some(plugin => plugin && typeof plugin === 'object' && plugin.name === 'vite:bytecode')
|
||||
}
|
||||
|
||||
/**
|
||||
* Bundles the electron app for production.
|
||||
@ -12,11 +20,14 @@ export async function build(inlineConfig: InlineConfig = {}): Promise<void> {
|
||||
return
|
||||
}
|
||||
|
||||
// Build targets in order: main -> preload -> renderer
|
||||
const buildTargets = ['main', 'preload', 'renderer'] as const
|
||||
const mainBytecode = await useBytecode(config.config.main)
|
||||
const preloadBytecode = await useBytecode(config.config.preload)
|
||||
const bytecodeBuild = mainBytecode || preloadBytecode ? new BytecodeBuild() : undefined
|
||||
|
||||
for (const target of buildTargets) {
|
||||
const viteConfig = config.config[target]
|
||||
const buildTargets = async (): Promise<void> => {
|
||||
// Build targets in order: main -> preload -> renderer
|
||||
for (const target of ['main', 'preload', 'renderer'] as const) {
|
||||
const viteConfig = config.config?.[target]
|
||||
if (viteConfig) {
|
||||
// Disable watch mode in production builds
|
||||
if (viteConfig.build?.watch) {
|
||||
@ -26,3 +37,13 @@ export async function build(inlineConfig: InlineConfig = {}): Promise<void> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!bytecodeBuild) return buildTargets()
|
||||
|
||||
try {
|
||||
await bytecodeBuild.start()
|
||||
await runWithBytecodeBuild(bytecodeBuild, buildTargets)
|
||||
} finally {
|
||||
await bytecodeBuild.stop()
|
||||
}
|
||||
}
|
||||
|
||||
29
src/bytecode/build.ts
Normal file
29
src/bytecode/build.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { AsyncLocalStorage } from 'node:async_hooks'
|
||||
import { BytecodeCompiler, type BytecodeCompileResult, type BytecodeTarget } from './compiler'
|
||||
|
||||
// Nested isolated-entry builds inherit the compilation session from the parent build.
|
||||
const activeBuild = new AsyncLocalStorage<BytecodeBuild>()
|
||||
|
||||
export class BytecodeBuild {
|
||||
private readonly compiler = new BytecodeCompiler()
|
||||
|
||||
start(): Promise<void> {
|
||||
return this.compiler.start()
|
||||
}
|
||||
|
||||
compile(target: BytecodeTarget, code: string): Promise<BytecodeCompileResult> {
|
||||
return this.compiler.compile(target, code)
|
||||
}
|
||||
|
||||
stop(): Promise<void> {
|
||||
return this.compiler.stop()
|
||||
}
|
||||
}
|
||||
|
||||
export function runWithBytecodeBuild<T>(build: BytecodeBuild, callback: () => Promise<T>): Promise<T> {
|
||||
return activeBuild.run(build, callback)
|
||||
}
|
||||
|
||||
export function getBytecodeBuild(): BytecodeBuild | undefined {
|
||||
return activeBuild.getStore()
|
||||
}
|
||||
@ -1,12 +1,11 @@
|
||||
import { type ChildProcess, spawn } from 'node:child_process'
|
||||
import path from 'node:path'
|
||||
import { createRequire } from 'node:module'
|
||||
import { getElectronPath } from '../electron'
|
||||
|
||||
export type ElectronCompilerOptions = {
|
||||
path: string
|
||||
args: string[]
|
||||
env?: NodeJS.ProcessEnv
|
||||
timeout?: number
|
||||
shutdownTimeout?: number
|
||||
}
|
||||
const require = createRequire(import.meta.url)
|
||||
const RPC_TIMEOUT = 30_000
|
||||
const SHUTDOWN_TIMEOUT = 3_000
|
||||
|
||||
export type BytecodeCompileResult = {
|
||||
sourceLength: number
|
||||
@ -14,7 +13,9 @@ export type BytecodeCompileResult = {
|
||||
cachedData: string
|
||||
}
|
||||
|
||||
type ElectronResponse = {
|
||||
export type BytecodeTarget = 'main' | 'preload'
|
||||
|
||||
type CompilerResponse = {
|
||||
type: 'response'
|
||||
invocationId: number
|
||||
result?: unknown
|
||||
@ -27,7 +28,7 @@ type PendingRequest = {
|
||||
timer: ReturnType<typeof setTimeout>
|
||||
}
|
||||
|
||||
export class ElectronCompiler {
|
||||
export class BytecodeCompiler {
|
||||
private ps?: ChildProcess
|
||||
private rpcMap = new Map<number, PendingRequest>()
|
||||
private invocationId = 0
|
||||
@ -36,16 +37,8 @@ export class ElectronCompiler {
|
||||
private ready = false
|
||||
private stderr = ''
|
||||
|
||||
constructor(readonly options: ElectronCompilerOptions) {
|
||||
for (const timeout of [options.timeout, options.shutdownTimeout]) {
|
||||
if (timeout !== undefined && (!Number.isFinite(timeout) || timeout <= 0)) {
|
||||
throw new RangeError('ElectronCompiler timeouts must be positive finite numbers')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start(): Promise<void> {
|
||||
if (this.stopping) return Promise.reject(new Error('ElectronCompiler is stopping'))
|
||||
if (this.stopping) return Promise.reject(new Error('BytecodeCompiler is stopping'))
|
||||
if (this.starting) return this.starting
|
||||
if (this.ready) return Promise.resolve()
|
||||
if (this.ps) return Promise.reject(new Error('Previous Electron process has not closed; await stop() first'))
|
||||
@ -56,10 +49,15 @@ export class ElectronCompiler {
|
||||
}
|
||||
|
||||
private async initialize(): Promise<void> {
|
||||
const env: NodeJS.ProcessEnv = { ...process.env, ...this.options.env, ELECTRON_VITE_RPC: 'true' }
|
||||
const env: NodeJS.ProcessEnv = { ...process.env, ELECTRON_VITE_RPC: 'true' }
|
||||
delete env.ELECTRON_RUN_AS_NODE
|
||||
this.stderr = ''
|
||||
const ps = spawn(this.options.path, this.options.args, {
|
||||
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
|
||||
@ -71,7 +69,7 @@ export class ElectronCompiler {
|
||||
if (this.ps === ps) this.stderr = (this.stderr + chunk).slice(-64 * 1024)
|
||||
})
|
||||
}
|
||||
ps.on('message', (message: ElectronResponse) => {
|
||||
ps.on('message', (message: CompilerResponse) => {
|
||||
if (this.ps !== ps || !message || message.type !== 'response') return
|
||||
const pending = this.rpcMap.get(message.invocationId)
|
||||
if (!pending) return
|
||||
@ -109,12 +107,12 @@ export class ElectronCompiler {
|
||||
}
|
||||
}
|
||||
|
||||
compile(code: string): Promise<BytecodeCompileResult> {
|
||||
return this.request<BytecodeCompileResult>('compile', code)
|
||||
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('ElectronCompiler is not ready; await start() first')
|
||||
if (!this.ready || this.stopping) throw new Error('BytecodeCompiler is not ready; await start() first')
|
||||
return this.sendRequest(cmd, args) as Promise<T>
|
||||
}
|
||||
|
||||
@ -130,7 +128,7 @@ export class ElectronCompiler {
|
||||
this.rpcMap.delete(invocationId)
|
||||
reject(this.withStderr(error))
|
||||
}
|
||||
const timer = setTimeout(() => fail(new Error('Electron RPC timed out: ' + cmd)), this.options.timeout ?? 30000)
|
||||
const timer = setTimeout(() => fail(new Error('Electron RPC timed out: ' + cmd)), RPC_TIMEOUT)
|
||||
this.rpcMap.set(invocationId, { resolve, reject, timer })
|
||||
try {
|
||||
ps.send({ type: 'request', invocationId, cmd, args }, error => {
|
||||
@ -164,11 +162,11 @@ export class ElectronCompiler {
|
||||
|
||||
stop(): Promise<void> {
|
||||
if (this.stopping) return this.stopping
|
||||
this.rejectPending(new Error('ElectronCompiler stopped'))
|
||||
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'), this.options.shutdownTimeout ?? 3000)
|
||||
const timer = setTimeout(() => ps.kill('SIGKILL'), SHUTDOWN_TIMEOUT)
|
||||
ps.once('close', () => {
|
||||
clearTimeout(timer)
|
||||
resolve()
|
||||
@ -1,29 +1,22 @@
|
||||
import path from 'node:path'
|
||||
import { randomBytes } from 'node:crypto'
|
||||
import { createRequire } from 'node:module'
|
||||
import colors from 'picocolors'
|
||||
import { type Plugin, type LibraryOptions, type Rolldown, normalizePath } from 'vite'
|
||||
import * as babel from '@babel/core'
|
||||
import MagicString from 'magic-string'
|
||||
import { getElectronPath } from '../electron'
|
||||
import { toRelativePath } from '../utils'
|
||||
import { ElectronCompiler } from './electronCompiler'
|
||||
import { type BytecodeBuild, getBytecodeBuild } from '../bytecode/build'
|
||||
import { type BytecodeTarget } from '../bytecode/compiler'
|
||||
|
||||
// Inspired by https://github.com/bytenode/bytenode
|
||||
|
||||
const _require = createRequire(import.meta.url)
|
||||
|
||||
function getBytecodeCompilerPath(): string {
|
||||
return path.join(path.dirname(_require.resolve('electron-vite/package.json')), 'bin', 'electron-bytecode-main.cjs')
|
||||
}
|
||||
|
||||
// Versioned envelope: magic/version (8), UTF-16 source length (4), module ID (16), V8 cache.
|
||||
// Keep our metadata separate from the private V8 cache header.
|
||||
async function compileToBytecode(compiler: ElectronCompiler, code: string): Promise<Buffer> {
|
||||
async function compileToBytecode(builder: BytecodeBuild, target: BytecodeTarget, code: string): Promise<Buffer> {
|
||||
const id = randomBytes(16)
|
||||
// Reserve enough source space for an untruncated, per-artifact placeholder identity.
|
||||
const source = code + '\n/*' + id.toString('hex') + '*/'
|
||||
const result = await compiler.compile(source)
|
||||
const result = await builder.compile(target, source)
|
||||
const header = Buffer.alloc(28)
|
||||
header.write('EVBC0001', 0, 'ascii')
|
||||
header.writeUInt32LE(result.sourceLength, 8)
|
||||
@ -189,13 +182,10 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
|
||||
|
||||
const bundles = Object.keys(output)
|
||||
|
||||
const compiler = new ElectronCompiler({
|
||||
path: getElectronPath(),
|
||||
args: [getBytecodeCompilerPath()],
|
||||
env: { ELECTRON_VITE_RENDERER: isPreload ? '1' : '0' }
|
||||
})
|
||||
try {
|
||||
await compiler.start()
|
||||
const compiler = getBytecodeBuild()
|
||||
if (!compiler) {
|
||||
throw new Error('Bytecode compilation requires an electron-vite build session')
|
||||
}
|
||||
for (const name of bundles) {
|
||||
const chunk = output[name]
|
||||
if (chunk.type === 'chunk') {
|
||||
@ -216,7 +206,7 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
|
||||
}
|
||||
}
|
||||
if (bytecodeChunks.includes(name)) {
|
||||
const bytecodeBuffer = await compileToBytecode(compiler, _code)
|
||||
const bytecodeBuffer = await compileToBytecode(compiler, isPreload ? 'preload' : 'main', _code)
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
fileName: name + 'c',
|
||||
@ -266,9 +256,6 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await compiler.stop()
|
||||
}
|
||||
|
||||
if (bytecodeChunkCount && !_chunks.some(ass => ass.type === 'asset' && ass.fileName === bytecodeModuleLoader)) {
|
||||
this.emitFile({
|
||||
|
||||
Loading…
Reference in New Issue
Block a user