diff --git a/bin/electron-bytecode-main.cjs b/bin/electron-bytecode-main.cjs new file mode 100644 index 0000000..18dbaec --- /dev/null +++ b/bin/electron-bytecode-main.cjs @@ -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) + }) diff --git a/src/electron.ts b/src/electron.ts index 5704504..fb73270 100644 --- a/src/electron.ts +++ b/src/electron.ts @@ -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', diff --git a/src/plugins/bytecode.ts b/src/plugins/bytecode.ts index 31d864b..19a598e 100644 --- a/src/plugins/bytecode.ts +++ b/src/plugins/bytecode.ts @@ -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 { +function compileToBytecodeWithElectronNode(code: string): Promise { return new Promise((resolve, reject) => { let data = Buffer.from([]) @@ -64,6 +66,79 @@ function compileToBytecode(code: string): Promise { }) } +function compileToBytecodeInElectronMain(codes: string[]): Promise { + 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 { + 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',