mirror of
https://github.com/alex8088/electron-vite.git
synced 2026-06-28 22:48:11 +08:00
Merge cc591ff7cb288d9bf64655885d45f8e08d4b1d25 into 31965d29724313f84d505e5ba0e493279a895d7b
This commit is contained in:
commit
bd8eafab78
21
bin/electron-bytecode-preload.cjs
Normal file
21
bin/electron-bytecode-preload.cjs
Normal file
@ -0,0 +1,21 @@
|
||||
// Compiler preload: runs in a renderer process (sandbox:false) so the produced code
|
||||
// cache matches the V8 isolate (snapshot + flag hash) of the app's runtime preload,
|
||||
// which the browser/main process cache does not on Electron 42+ / V8 14.8.
|
||||
const fs = require('fs')
|
||||
const vm = require('vm')
|
||||
const v8 = require('v8')
|
||||
const { ipcRenderer } = require('electron')
|
||||
|
||||
v8.setFlagsFromString('--no-lazy')
|
||||
v8.setFlagsFromString('--no-flush-bytecode')
|
||||
|
||||
const params = ['exports', 'require', 'module', '__filename', '__dirname']
|
||||
|
||||
try {
|
||||
const code = fs.readFileSync(process.env.ELECTRON_VITE_BYTECODE_IN, 'utf-8')
|
||||
const fn = vm.compileFunction(code, params, { produceCachedData: true })
|
||||
fs.writeFileSync(process.env.ELECTRON_VITE_BYTECODE_OUT, fn.cachedData)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
ipcRenderer.send('electron-vite:bytecode-done')
|
||||
@ -1,32 +1,50 @@
|
||||
const { app } = require('electron')
|
||||
const vm = require('vm')
|
||||
const v8 = require('v8')
|
||||
const wrap = require('module').wrap
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
v8.setFlagsFromString('--no-lazy')
|
||||
v8.setFlagsFromString('--no-flush-bytecode')
|
||||
|
||||
let code = ''
|
||||
// Compile a chunk to a V8 code cache in the SAME process type that will consume it.
|
||||
// On Electron 42+/V8 14.8 the cache is bound to a per-process-type snapshot AND flag
|
||||
// hash, so a cache built in the wrong process type is rejected (and forcing acceptance
|
||||
// corrupts execution). Spawned WITHOUT ELECTRON_RUN_AS_NODE (whose isolate matches
|
||||
// neither). Code in / cache out go through ELECTRON_VITE_BYTECODE_IN / _OUT temp files.
|
||||
// - main chunks -> compiled here, in the browser (main) process.
|
||||
// - preload chunks -> ELECTRON_VITE_BYTECODE_RENDERER=1: a hidden window whose
|
||||
// sandbox:false preload compiles it in a renderer isolate.
|
||||
app.disableHardwareAcceleration()
|
||||
|
||||
process.stdin.setEncoding('utf-8')
|
||||
const params = ['exports', 'require', 'module', '__filename', '__dirname']
|
||||
const inFile = process.env.ELECTRON_VITE_BYTECODE_IN
|
||||
const outFile = process.env.ELECTRON_VITE_BYTECODE_OUT
|
||||
|
||||
process.stdin.on('readable', () => {
|
||||
const data = process.stdin.read()
|
||||
if (data !== null) {
|
||||
code += data
|
||||
}
|
||||
})
|
||||
|
||||
process.stdin.on('end', () => {
|
||||
try {
|
||||
if (typeof code !== 'string') {
|
||||
throw new Error(`javascript code must be string. ${typeof code} was given.`)
|
||||
if (process.env.ELECTRON_VITE_BYTECODE_RENDERER) {
|
||||
const { BrowserWindow, ipcMain } = require('electron')
|
||||
app.whenReady().then(() => {
|
||||
ipcMain.once('electron-vite:bytecode-done', () => app.quit())
|
||||
const win = new BrowserWindow({
|
||||
show: false,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'electron-bytecode-preload.cjs'),
|
||||
sandbox: false,
|
||||
contextIsolation: true
|
||||
}
|
||||
})
|
||||
win.loadURL('data:text/html,<!doctype html><html></html>')
|
||||
})
|
||||
} else {
|
||||
app.whenReady().then(() => {
|
||||
try {
|
||||
const code = fs.readFileSync(inFile, 'utf-8')
|
||||
const fn = vm.compileFunction(code, params, { produceCachedData: true })
|
||||
fs.writeFileSync(outFile, fn.cachedData)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
process.exitCode = 1
|
||||
}
|
||||
|
||||
const script = new vm.Script(wrap(code), { produceCachedData: true })
|
||||
const bytecodeBuffer = script.createCachedData()
|
||||
|
||||
process.stdout.write(bytecodeBuffer)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
app.quit()
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import fs from 'node:fs'
|
||||
import os from 'node:os'
|
||||
import path from 'node:path'
|
||||
import { spawn } from 'node:child_process'
|
||||
import { createRequire } from 'node:module'
|
||||
@ -16,50 +18,54 @@ function getBytecodeCompilerPath(): string {
|
||||
return path.join(path.dirname(_require.resolve('electron-vite/package.json')), 'bin', 'electron-bytecode.cjs')
|
||||
}
|
||||
|
||||
function compileToBytecode(code: string): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let data = Buffer.from([])
|
||||
let bytecodeId = 0
|
||||
|
||||
function compileToBytecode(code: string, renderer: boolean): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const electronPath = getElectronPath()
|
||||
const bytecodePath = getBytecodeCompilerPath()
|
||||
const id = `${process.pid}-${bytecodeId++}`
|
||||
const inFile = path.join(os.tmpdir(), `electron-vite-bytecode-${id}.in.js`)
|
||||
const outFile = path.join(os.tmpdir(), `electron-vite-bytecode-${id}.jsc`)
|
||||
fs.writeFileSync(inFile, code)
|
||||
|
||||
// Compile in a real Electron process whose V8 isolate matches the one that will
|
||||
// consume the cache. On V8 14.8+ (Electron 42+) the code cache is bound to a
|
||||
// snapshot/isolate checksum AND a flag hash that differ per process type, so a cache
|
||||
// built in the wrong process type is rejected (and forcing acceptance corrupts):
|
||||
// - main chunks -> the Electron browser (main) process
|
||||
// - preload chunks -> a renderer process (a hidden window whose sandbox:false
|
||||
// preload does the compile)
|
||||
// Never ELECTRON_RUN_AS_NODE (its isolate matches neither on Electron 42+). Code in
|
||||
// / cache out go through temp files (a GUI-subsystem process doesn't pipe stdio).
|
||||
const env = { ...process.env, ELECTRON_VITE_BYTECODE_IN: inFile, ELECTRON_VITE_BYTECODE_OUT: outFile }
|
||||
delete env.ELECTRON_RUN_AS_NODE
|
||||
if (renderer) {
|
||||
env.ELECTRON_VITE_BYTECODE_RENDERER = '1'
|
||||
}
|
||||
|
||||
const proc = spawn(electronPath, [bytecodePath], {
|
||||
env: { ELECTRON_RUN_AS_NODE: '1' },
|
||||
stdio: ['pipe', 'pipe', 'pipe', 'ipc']
|
||||
env,
|
||||
stdio: ['ignore', 'ignore', 'pipe']
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
let stderr = ''
|
||||
if (proc.stderr) {
|
||||
proc.stderr.on('data', chunk => {
|
||||
console.error('Error: ', chunk.toString())
|
||||
})
|
||||
proc.stderr.on('error', err => {
|
||||
console.error('Error: ', err)
|
||||
stderr += chunk.toString()
|
||||
})
|
||||
}
|
||||
|
||||
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)
|
||||
proc.on('exit', exitCode => {
|
||||
fs.rmSync(inFile, { force: true })
|
||||
try {
|
||||
const data = fs.readFileSync(outFile)
|
||||
fs.rmSync(outFile, { force: true })
|
||||
resolve(data)
|
||||
} catch {
|
||||
reject(new Error(`bytecode compilation failed (exit code ${exitCode})${stderr ? `:\n${stderr}` : ''}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -73,47 +79,37 @@ const bytecodeModuleLoaderCode = [
|
||||
`const Module = require("module");`,
|
||||
`v8.setFlagsFromString("--no-lazy");`,
|
||||
`v8.setFlagsFromString("--no-flush-bytecode");`,
|
||||
`const FLAG_HASH_OFFSET = 12;`,
|
||||
`const COMPILE_PARAMS = ["exports", "require", "module", "__filename", "__dirname"];`,
|
||||
`const SOURCE_HASH_OFFSET = 8;`,
|
||||
`let dummyBytecode;`,
|
||||
`function setFlagHashHeader(bytecodeBuffer) {`,
|
||||
` if (!dummyBytecode) {`,
|
||||
` const script = new vm.Script("", {`,
|
||||
` produceCachedData: true`,
|
||||
` });`,
|
||||
` dummyBytecode = script.createCachedData();`,
|
||||
`function sourceLength(bytecodeBuffer) {`,
|
||||
` // The low 28 bits of the source hash hold the source length; the high bits are`,
|
||||
` // V8 source-hash flags (e.g. the "wrapped" bit set by vm.compileFunction).`,
|
||||
` return bytecodeBuffer.readUInt32LE(SOURCE_HASH_OFFSET) & 0x0fffffff;`,
|
||||
`};`,
|
||||
`function placeholderBody(len, filename) {`,
|
||||
` // A same-length body so the source hash matches. Its CONTENT is ignored (V8 runs`,
|
||||
` // the cached bytecode) but it must be UNIQUE per file, otherwise V8's in-isolate`,
|
||||
` // compilation cache returns a previously-compiled function for the same source.`,
|
||||
` const tag = "/*" + filename + " ";`,
|
||||
` if (tag.length + 2 <= len) {`,
|
||||
` return tag + " ".repeat(len - tag.length - 2) + "*/";`,
|
||||
` }`,
|
||||
` dummyBytecode.slice(FLAG_HASH_OFFSET, FLAG_HASH_OFFSET + 4).copy(bytecodeBuffer, FLAG_HASH_OFFSET);`,
|
||||
`};`,
|
||||
`function getSourceHashHeader(bytecodeBuffer) {`,
|
||||
` return bytecodeBuffer.slice(SOURCE_HASH_OFFSET, SOURCE_HASH_OFFSET + 4);`,
|
||||
`};`,
|
||||
`function buffer2Number(buffer) {`,
|
||||
` let ret = 0;`,
|
||||
` ret |= buffer[3] << 24;`,
|
||||
` ret |= buffer[2] << 16;`,
|
||||
` ret |= buffer[1] << 8;`,
|
||||
` ret |= buffer[0];`,
|
||||
` return ret;`,
|
||||
` if (len >= 4) {`,
|
||||
` return "/*" + (filename + " ").slice(0, len - 4).padEnd(len - 4, " ") + "*/";`,
|
||||
` }`,
|
||||
` return " ".repeat(len);`,
|
||||
`};`,
|
||||
`Module._extensions[".jsc"] = Module._extensions[".cjsc"] = function (module, filename) {`,
|
||||
` const bytecodeBuffer = fs.readFileSync(filename);`,
|
||||
` if (!Buffer.isBuffer(bytecodeBuffer)) {`,
|
||||
` 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, {`,
|
||||
` const placeholder = placeholderBody(sourceLength(bytecodeBuffer), filename);`,
|
||||
` const compiledWrapper = vm.compileFunction(placeholder, COMPILE_PARAMS, {`,
|
||||
` filename: filename,`,
|
||||
` lineOffset: 0,`,
|
||||
` displayErrors: true,`,
|
||||
` cachedData: bytecodeBuffer`,
|
||||
` });`,
|
||||
` if (script.cachedDataRejected) {`,
|
||||
` if (compiledWrapper.cachedDataRejected) {`,
|
||||
` throw new Error("Invalid or incompatible cached data (cachedDataRejected)");`,
|
||||
` }`,
|
||||
` const require = function (id) {`,
|
||||
@ -127,15 +123,8 @@ const bytecodeModuleLoaderCode = [
|
||||
` }`,
|
||||
` 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);`,
|
||||
` return compiledWrapper.call(module.exports, module.exports, require, module, filename, dirname);`,
|
||||
`};`
|
||||
]
|
||||
|
||||
@ -190,6 +179,9 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
|
||||
const bytecodeModuleLoader = 'bytecode-loader.cjs'
|
||||
|
||||
let supported = false
|
||||
// Preload runs in a renderer-type V8 isolate (different snapshot/flags than the
|
||||
// browser/main process), so its bytecode must be compiled in a renderer process.
|
||||
let isPreload = false
|
||||
|
||||
return {
|
||||
name: 'vite:bytecode',
|
||||
@ -199,6 +191,7 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
|
||||
if (supported) {
|
||||
return
|
||||
}
|
||||
isPreload = config.plugins.some(p => p.name === 'vite:electron-preload-config-preset')
|
||||
const useInRenderer = config.plugins.some(p => p.name === 'vite:electron-renderer-preset-config')
|
||||
if (useInRenderer) {
|
||||
config.logger.warn(colors.yellow('bytecodePlugin does not support renderer.'))
|
||||
@ -274,7 +267,7 @@ export function bytecodePlugin(options: BytecodeOptions = {}): Plugin | null {
|
||||
}
|
||||
}
|
||||
if (bytecodeChunks.includes(name)) {
|
||||
const bytecodeBuffer = await compileToBytecode(_code)
|
||||
const bytecodeBuffer = await compileToBytecode(_code, isPreload)
|
||||
this.emitFile({
|
||||
type: 'asset',
|
||||
fileName: name + 'c',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user