mirror of
https://github.com/alex8088/electron-vite.git
synced 2026-09-04 15:22:44 +08:00
fix(bytecode): support Electron 42 and newer
Compile bytecode inside a real Electron main process on Electron 42+ so the V8 read-only snapshot checksum matches at runtime. Batch all chunks into one compiler process and retain the legacy run-as-node path for older Electron releases. Resolve Electron through its package entry to support lazy binary installation, and add Electron 42/43 build targets. Closes #911
This commit is contained in:
parent
608461c04c
commit
4847045063
32
bin/electron-bytecode-main.cjs
Normal file
32
bin/electron-bytecode-main.cjs
Normal file
@ -0,0 +1,32 @@
|
||||
const fs = require('node:fs')
|
||||
const Module = require('node:module')
|
||||
const v8 = require('node:v8')
|
||||
const vm = require('node:vm')
|
||||
const { app } = require('electron')
|
||||
|
||||
v8.setFlagsFromString('--no-lazy')
|
||||
v8.setFlagsFromString('--no-flush-bytecode')
|
||||
app.disableHardwareAcceleration()
|
||||
|
||||
async function compile() {
|
||||
const manifestPath = process.argv[2]
|
||||
if (!manifestPath) {
|
||||
throw new Error('Bytecode compiler manifest path is required')
|
||||
}
|
||||
|
||||
const jobs = JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
|
||||
for (const job of jobs) {
|
||||
const code = fs.readFileSync(job.input, 'utf8')
|
||||
const script = new vm.Script(Module.wrap(code), { produceCachedData: true })
|
||||
fs.writeFileSync(job.output, script.createCachedData())
|
||||
}
|
||||
}
|
||||
|
||||
app
|
||||
.whenReady()
|
||||
.then(compile)
|
||||
.then(() => app.exit(0))
|
||||
.catch(error => {
|
||||
process.stderr.write(`${error instanceof Error ? error.stack : String(error)}\n`)
|
||||
app.exit(1)
|
||||
})
|
||||
@ -23,7 +23,7 @@ const ensureElectronEntryFile = (root = process.cwd()): void => {
|
||||
}
|
||||
}
|
||||
|
||||
const getElectronMajorVer = (): string => {
|
||||
export const getElectronMajorVer = (): string => {
|
||||
let majorVer = process.env.ELECTRON_MAJOR_VER || ''
|
||||
if (!majorVer) {
|
||||
const pkg = _require.resolve('electron/package.json')
|
||||
@ -49,18 +49,11 @@ export function supportImportMetaPaths(): boolean {
|
||||
export function getElectronPath(): string {
|
||||
let electronExecPath = process.env.ELECTRON_EXEC_PATH || ''
|
||||
if (!electronExecPath) {
|
||||
const electronModulePath = path.dirname(_require.resolve('electron'))
|
||||
const pathFile = path.join(electronModulePath, 'path.txt')
|
||||
let executablePath
|
||||
if (fs.existsSync(pathFile)) {
|
||||
executablePath = fs.readFileSync(pathFile, 'utf-8')
|
||||
}
|
||||
if (executablePath) {
|
||||
electronExecPath = path.join(electronModulePath, 'dist', executablePath)
|
||||
process.env.ELECTRON_EXEC_PATH = electronExecPath
|
||||
} else {
|
||||
electronExecPath = _require('electron')
|
||||
if (typeof electronExecPath !== 'string' || !fs.existsSync(electronExecPath)) {
|
||||
throw new Error('Electron uninstall')
|
||||
}
|
||||
process.env.ELECTRON_EXEC_PATH = electronExecPath
|
||||
}
|
||||
return electronExecPath
|
||||
}
|
||||
@ -69,6 +62,8 @@ export function getElectronNodeTarget(): string {
|
||||
const electronVer = getElectronMajorVer()
|
||||
|
||||
const nodeVer = {
|
||||
'43': '24.18',
|
||||
'42': '24.16',
|
||||
'41': '24.14',
|
||||
'40': '24.14',
|
||||
'39': '22.20',
|
||||
@ -102,6 +97,8 @@ export function getElectronChromeTarget(): string {
|
||||
const electronVer = getElectronMajorVer()
|
||||
|
||||
const chromeVer = {
|
||||
'43': '150',
|
||||
'42': '148',
|
||||
'41': '146',
|
||||
'40': '144',
|
||||
'39': '142',
|
||||
|
||||
@ -1,22 +1,24 @@
|
||||
import path from 'node:path'
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import { spawn } from 'node:child_process'
|
||||
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 { getElectronMajorVer, getElectronPath } from '../electron'
|
||||
import { toRelativePath } from '../utils'
|
||||
|
||||
// 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.cjs')
|
||||
function getBytecodeCompilerPath(filename = 'electron-bytecode.cjs'): string {
|
||||
return path.join(path.dirname(_require.resolve('electron-vite/package.json')), 'bin', filename)
|
||||
}
|
||||
|
||||
function compileToBytecode(code: string): Promise<Buffer> {
|
||||
function compileToBytecodeWithElectronNode(code: string): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let data = Buffer.from([])
|
||||
|
||||
@ -64,6 +66,79 @@ function compileToBytecode(code: string): Promise<Buffer> {
|
||||
})
|
||||
}
|
||||
|
||||
function compileToBytecodeInElectronMain(codes: string[]): Promise<Buffer[]> {
|
||||
const electronPath = getElectronPath()
|
||||
const compilerPath = getBytecodeCompilerPath('electron-bytecode-main.cjs')
|
||||
const tempDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'electron-vite-bytecode-'))
|
||||
const manifestPath = path.join(tempDirectory, 'manifest.json')
|
||||
const jobs = codes.map((code, index) => {
|
||||
const input = path.join(tempDirectory, `${index}.js`)
|
||||
const output = path.join(tempDirectory, `${index}.jsc`)
|
||||
fs.writeFileSync(input, code)
|
||||
return { input, output }
|
||||
})
|
||||
fs.writeFileSync(manifestPath, JSON.stringify(jobs))
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const env = { ...process.env }
|
||||
delete env.ELECTRON_RUN_AS_NODE
|
||||
const args = [compilerPath, manifestPath]
|
||||
if (process.getuid?.() === 0) {
|
||||
args.push('--no-sandbox')
|
||||
}
|
||||
|
||||
const cleanup = (): void => {
|
||||
fs.rmSync(tempDirectory, { recursive: true, force: true })
|
||||
}
|
||||
const proc = spawn(electronPath, args, {
|
||||
env,
|
||||
stdio: ['ignore', 'ignore', 'pipe']
|
||||
})
|
||||
let stderr = ''
|
||||
let settled = false
|
||||
|
||||
const fail = (error: Error): void => {
|
||||
if (settled) return
|
||||
settled = true
|
||||
cleanup()
|
||||
reject(error)
|
||||
}
|
||||
|
||||
proc.stderr?.on('data', chunk => {
|
||||
stderr += chunk.toString()
|
||||
})
|
||||
proc.on('error', error => {
|
||||
fail(error)
|
||||
})
|
||||
proc.on('exit', (code, signal) => {
|
||||
if (settled) return
|
||||
if (code !== 0) {
|
||||
fail(
|
||||
new Error(
|
||||
`Electron main-process bytecode compilation failed (exit code ${code}, signal ${signal})${stderr ? `\n${stderr.trim()}` : ''}`
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const bytecode = jobs.map(job => fs.readFileSync(job.output))
|
||||
cleanup()
|
||||
settled = true
|
||||
resolve(bytecode)
|
||||
} catch (error) {
|
||||
fail(error instanceof Error ? error : new Error(String(error)))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function compileToBytecode(codes: string[]): Promise<Buffer[]> {
|
||||
return parseInt(getElectronMajorVer()) >= 42
|
||||
? compileToBytecodeInElectronMain(codes)
|
||||
: Promise.all(codes.map(compileToBytecodeWithElectronNode))
|
||||
}
|
||||
|
||||
const bytecodeModuleLoaderCode = [
|
||||
`"use strict";`,
|
||||
`const fs = require("fs");`,
|
||||
@ -249,83 +324,86 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
|
||||
return `require("${toRelativePath(bytecodeModuleLoader, normalizePath(chunkFileName))}");`
|
||||
}
|
||||
|
||||
let bytecodeChunkCount = 0
|
||||
|
||||
const bundles = Object.keys(output)
|
||||
const bytecodeEntries: Array<{ name: string; fileName: string; code: string; source: string }> = []
|
||||
|
||||
await Promise.all(
|
||||
bundles.map(async name => {
|
||||
const chunk = output[name]
|
||||
if (chunk.type === 'chunk') {
|
||||
let _code = chunk.code
|
||||
if (bytecodeRE) {
|
||||
let match: RegExpExecArray | null
|
||||
let s: MagicString | undefined
|
||||
while ((match = bytecodeRE.exec(_code))) {
|
||||
s ||= new MagicString(_code)
|
||||
const [prefix, chunkName] = match
|
||||
const len = prefix.length + chunkName.length
|
||||
s.overwrite(match.index, match.index + len, prefix + chunkName + 'c', {
|
||||
contentOnly: true
|
||||
})
|
||||
}
|
||||
if (s) {
|
||||
_code = s.toString()
|
||||
}
|
||||
}
|
||||
if (bytecodeChunks.includes(name)) {
|
||||
const bytecodeBuffer = await compileToBytecode(_code)
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
fileName: name + 'c',
|
||||
source: bytecodeBuffer
|
||||
for (const name of bundles) {
|
||||
const chunk = output[name]
|
||||
if (chunk.type === 'chunk') {
|
||||
let _code = chunk.code
|
||||
if (bytecodeRE) {
|
||||
let match: RegExpExecArray | null
|
||||
let s: MagicString | undefined
|
||||
while ((match = bytecodeRE.exec(_code))) {
|
||||
s ||= new MagicString(_code)
|
||||
const [prefix, chunkName] = match
|
||||
const len = prefix.length + chunkName.length
|
||||
s.overwrite(match.index, match.index + len, prefix + chunkName + 'c', {
|
||||
contentOnly: true
|
||||
})
|
||||
if (!removeBundleJS) {
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
fileName: '_' + chunk.fileName,
|
||||
source: chunk.code
|
||||
})
|
||||
}
|
||||
if (chunk.isEntry) {
|
||||
const bytecodeLoaderBlock = getBytecodeLoaderBlock(chunk.fileName)
|
||||
const bytecodeModuleBlock = `require("./${path.basename(name) + 'c'}");`
|
||||
const code = `${useStrict}\n${bytecodeLoaderBlock}\n${bytecodeModuleBlock}\n`
|
||||
chunk.code = code
|
||||
} else {
|
||||
delete output[chunk.fileName]
|
||||
}
|
||||
bytecodeChunkCount += 1
|
||||
} else {
|
||||
if (chunk.isEntry) {
|
||||
let hasBytecodeMoudle = false
|
||||
const idsToHandle = new Set([...chunk.imports, ...chunk.dynamicImports])
|
||||
for (const moduleId of idsToHandle) {
|
||||
if (bytecodeChunks.includes(moduleId)) {
|
||||
hasBytecodeMoudle = true
|
||||
break
|
||||
}
|
||||
const moduleInfo = this.getModuleInfo(moduleId)
|
||||
if (moduleInfo) {
|
||||
const { importers, dynamicImporters } = moduleInfo
|
||||
for (const importerId of importers) idsToHandle.add(importerId)
|
||||
for (const importerId of dynamicImporters) idsToHandle.add(importerId)
|
||||
}
|
||||
}
|
||||
_code = hasBytecodeMoudle
|
||||
? _code.replace(
|
||||
/("use strict";)|('use strict';)/,
|
||||
`${useStrict}\n${getBytecodeLoaderBlock(chunk.fileName)}`
|
||||
)
|
||||
: _code
|
||||
}
|
||||
chunk.code = _code
|
||||
}
|
||||
if (s) {
|
||||
_code = s.toString()
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
if (bytecodeChunks.includes(name)) {
|
||||
bytecodeEntries.push({ name, fileName: chunk.fileName, code: _code, source: chunk.code })
|
||||
if (chunk.isEntry) {
|
||||
const bytecodeLoaderBlock = getBytecodeLoaderBlock(chunk.fileName)
|
||||
const bytecodeModuleBlock = `require("./${path.basename(name) + 'c'}");`
|
||||
const code = `${useStrict}\n${bytecodeLoaderBlock}\n${bytecodeModuleBlock}\n`
|
||||
chunk.code = code
|
||||
} else {
|
||||
delete output[chunk.fileName]
|
||||
}
|
||||
} else {
|
||||
if (chunk.isEntry) {
|
||||
let hasBytecodeMoudle = false
|
||||
const idsToHandle = new Set([...chunk.imports, ...chunk.dynamicImports])
|
||||
for (const moduleId of idsToHandle) {
|
||||
if (bytecodeChunks.includes(moduleId)) {
|
||||
hasBytecodeMoudle = true
|
||||
break
|
||||
}
|
||||
const moduleInfo = this.getModuleInfo(moduleId)
|
||||
if (moduleInfo) {
|
||||
const { importers, dynamicImporters } = moduleInfo
|
||||
for (const importerId of importers) idsToHandle.add(importerId)
|
||||
for (const importerId of dynamicImporters) idsToHandle.add(importerId)
|
||||
}
|
||||
}
|
||||
_code = hasBytecodeMoudle
|
||||
? _code.replace(
|
||||
/("use strict";)|('use strict';)/,
|
||||
`${useStrict}\n${getBytecodeLoaderBlock(chunk.fileName)}`
|
||||
)
|
||||
: _code
|
||||
}
|
||||
chunk.code = _code
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bytecodeChunkCount && !_chunks.some(ass => ass.type === 'asset' && ass.fileName === bytecodeModuleLoader)) {
|
||||
const bytecodeBuffers = await compileToBytecode(bytecodeEntries.map(entry => entry.code))
|
||||
for (const [index, entry] of bytecodeEntries.entries()) {
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
fileName: entry.name + 'c',
|
||||
source: bytecodeBuffers[index]
|
||||
})
|
||||
if (!removeBundleJS) {
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
fileName: '_' + entry.fileName,
|
||||
source: entry.source
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
bytecodeEntries.length &&
|
||||
!_chunks.some(ass => ass.type === 'asset' && ass.fileName === bytecodeModuleLoader)
|
||||
) {
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
source: bytecodeModuleLoaderCode.join('\n') + '\n',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user