mirror of
https://github.com/alex8088/electron-vite.git
synced 2026-11-02 14:51:51 +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 || ''
|
let majorVer = process.env.ELECTRON_MAJOR_VER || ''
|
||||||
if (!majorVer) {
|
if (!majorVer) {
|
||||||
const pkg = _require.resolve('electron/package.json')
|
const pkg = _require.resolve('electron/package.json')
|
||||||
@ -49,18 +49,11 @@ export function supportImportMetaPaths(): boolean {
|
|||||||
export function getElectronPath(): string {
|
export function getElectronPath(): string {
|
||||||
let electronExecPath = process.env.ELECTRON_EXEC_PATH || ''
|
let electronExecPath = process.env.ELECTRON_EXEC_PATH || ''
|
||||||
if (!electronExecPath) {
|
if (!electronExecPath) {
|
||||||
const electronModulePath = path.dirname(_require.resolve('electron'))
|
electronExecPath = _require('electron')
|
||||||
const pathFile = path.join(electronModulePath, 'path.txt')
|
if (typeof electronExecPath !== 'string' || !fs.existsSync(electronExecPath)) {
|
||||||
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 {
|
|
||||||
throw new Error('Electron uninstall')
|
throw new Error('Electron uninstall')
|
||||||
}
|
}
|
||||||
|
process.env.ELECTRON_EXEC_PATH = electronExecPath
|
||||||
}
|
}
|
||||||
return electronExecPath
|
return electronExecPath
|
||||||
}
|
}
|
||||||
@ -69,6 +62,8 @@ export function getElectronNodeTarget(): string {
|
|||||||
const electronVer = getElectronMajorVer()
|
const electronVer = getElectronMajorVer()
|
||||||
|
|
||||||
const nodeVer = {
|
const nodeVer = {
|
||||||
|
'43': '24.18',
|
||||||
|
'42': '24.16',
|
||||||
'41': '24.14',
|
'41': '24.14',
|
||||||
'40': '24.14',
|
'40': '24.14',
|
||||||
'39': '22.20',
|
'39': '22.20',
|
||||||
@ -102,6 +97,8 @@ export function getElectronChromeTarget(): string {
|
|||||||
const electronVer = getElectronMajorVer()
|
const electronVer = getElectronMajorVer()
|
||||||
|
|
||||||
const chromeVer = {
|
const chromeVer = {
|
||||||
|
'43': '150',
|
||||||
|
'42': '148',
|
||||||
'41': '146',
|
'41': '146',
|
||||||
'40': '144',
|
'40': '144',
|
||||||
'39': '142',
|
'39': '142',
|
||||||
|
|||||||
@ -1,22 +1,24 @@
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import os from 'node:os'
|
||||||
import { spawn } from 'node:child_process'
|
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 LibraryOptions, type Rolldown, normalizePath } from 'vite'
|
import { type Plugin, type LibraryOptions, type Rolldown, normalizePath } from 'vite'
|
||||||
import * as babel from '@babel/core'
|
import * as babel from '@babel/core'
|
||||||
import MagicString from 'magic-string'
|
import MagicString from 'magic-string'
|
||||||
import { getElectronPath } from '../electron'
|
import { getElectronMajorVer, getElectronPath } from '../electron'
|
||||||
import { toRelativePath } from '../utils'
|
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)
|
||||||
|
|
||||||
function getBytecodeCompilerPath(): string {
|
function getBytecodeCompilerPath(filename = 'electron-bytecode.cjs'): string {
|
||||||
return path.join(path.dirname(_require.resolve('electron-vite/package.json')), 'bin', 'electron-bytecode.cjs')
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
let data = Buffer.from([])
|
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 = [
|
const bytecodeModuleLoaderCode = [
|
||||||
`"use strict";`,
|
`"use strict";`,
|
||||||
`const fs = require("fs");`,
|
`const fs = require("fs");`,
|
||||||
@ -249,83 +324,86 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
|
|||||||
return `require("${toRelativePath(bytecodeModuleLoader, normalizePath(chunkFileName))}");`
|
return `require("${toRelativePath(bytecodeModuleLoader, normalizePath(chunkFileName))}");`
|
||||||
}
|
}
|
||||||
|
|
||||||
let bytecodeChunkCount = 0
|
|
||||||
|
|
||||||
const bundles = Object.keys(output)
|
const bundles = Object.keys(output)
|
||||||
|
const bytecodeEntries: Array<{ name: string; fileName: string; code: string; source: string }> = []
|
||||||
|
|
||||||
await Promise.all(
|
for (const name of bundles) {
|
||||||
bundles.map(async name => {
|
const chunk = output[name]
|
||||||
const chunk = output[name]
|
if (chunk.type === 'chunk') {
|
||||||
if (chunk.type === 'chunk') {
|
let _code = chunk.code
|
||||||
let _code = chunk.code
|
if (bytecodeRE) {
|
||||||
if (bytecodeRE) {
|
let match: RegExpExecArray | null
|
||||||
let match: RegExpExecArray | null
|
let s: MagicString | undefined
|
||||||
let s: MagicString | undefined
|
while ((match = bytecodeRE.exec(_code))) {
|
||||||
while ((match = bytecodeRE.exec(_code))) {
|
s ||= new MagicString(_code)
|
||||||
s ||= new MagicString(_code)
|
const [prefix, chunkName] = match
|
||||||
const [prefix, chunkName] = match
|
const len = prefix.length + chunkName.length
|
||||||
const len = prefix.length + chunkName.length
|
s.overwrite(match.index, match.index + len, prefix + chunkName + 'c', {
|
||||||
s.overwrite(match.index, match.index + len, prefix + chunkName + 'c', {
|
contentOnly: true
|
||||||
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
|
|
||||||
})
|
})
|
||||||
if (!removeBundleJS) {
|
}
|
||||||
this.emitFile({
|
if (s) {
|
||||||
type: 'asset',
|
_code = s.toString()
|
||||||
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 (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({
|
this.emitFile({
|
||||||
type: 'asset',
|
type: 'asset',
|
||||||
source: bytecodeModuleLoaderCode.join('\n') + '\n',
|
source: bytecodeModuleLoaderCode.join('\n') + '\n',
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user