UNPKG

electron-vite

Version:

Electron build tooling based on Vite

467 lines (462 loc) 19.1 kB
import { createFilter, normalizePath, mergeConfig } from 'vite'; export { createLogger, defineConfig as defineViteConfig, mergeConfig } from 'vite'; import { g as getElectronPath, t as toRelativePath, l as loadPackageData } from './chunks/lib-DI8hz120.js'; export { d as defineConfig, b as loadConfigFromFile, a as loadEnv, r as resolveConfig } from './chunks/lib-DI8hz120.js'; export { createServer } from './chunks/lib-BWtXCQ6O.js'; export { build } from './chunks/lib-CPRxxvGV.js'; export { preview } from './chunks/lib-BkRrYQUU.js'; import path from 'node:path'; import fs from 'node:fs'; import { spawn } from 'node:child_process'; import { createRequire } from 'node:module'; import colors from 'picocolors'; import * as babel from '@babel/core'; import MagicString from 'magic-string'; import 'node:url'; import 'esbuild'; import 'node:crypto'; import 'node:fs/promises'; // Inspired by https://github.com/bytenode/bytenode const _require = createRequire(import.meta.url); function getBytecodeCompilerPath() { return path.join(path.dirname(_require.resolve('electron-vite/package.json')), 'bin', 'electron-bytecode.cjs'); } function compileToBytecode(code) { return new Promise((resolve, reject) => { let data = Buffer.from([]); const electronPath = getElectronPath(); const bytecodePath = getBytecodeCompilerPath(); const proc = spawn(electronPath, [bytecodePath], { env: { ELECTRON_RUN_AS_NODE: '1' }, stdio: ['pipe', 'pipe', 'pipe', 'ipc'] }); 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); }); } if (proc.stderr) { proc.stderr.on('data', chunk => { console.error('Error: ', chunk.toString()); }); proc.stderr.on('error', err => { console.error('Error: ', err); }); } 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); }); }); } const bytecodeModuleLoaderCode = [ `"use strict";`, `const fs = require("fs");`, `const path = require("path");`, `const vm = require("vm");`, `const v8 = require("v8");`, `const Module = require("module");`, `v8.setFlagsFromString("--no-lazy");`, `v8.setFlagsFromString("--no-flush-bytecode");`, `const FLAG_HASH_OFFSET = 12;`, `const SOURCE_HASH_OFFSET = 8;`, `let dummyBytecode;`, `function setFlagHashHeader(bytecodeBuffer) {`, ` if (!dummyBytecode) {`, ` const script = new vm.Script("", {`, ` produceCachedData: true`, ` });`, ` dummyBytecode = script.createCachedData();`, ` }`, ` 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;`, `};`, `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, {`, ` filename: filename,`, ` lineOffset: 0,`, ` displayErrors: true,`, ` cachedData: bytecodeBuffer`, ` });`, ` if (script.cachedDataRejected) {`, ` throw new Error("Invalid or incompatible cached data (cachedDataRejected)");`, ` }`, ` const require = function (id) {`, ` return module.require(id);`, ` };`, ` require.resolve = function (request, options) {`, ` return Module._resolveFilename(request, module, false, options);`, ` };`, ` if (process.mainModule) {`, ` require.main = process.mainModule;`, ` }`, ` 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);`, `};` ]; /** * Compile to v8 bytecode to protect source code. */ function bytecodePlugin(options = {}) { if (process.env.NODE_ENV_ELECTRON_VITE !== 'production') { return null; } const { chunkAlias = [], transformArrowFunctions = true, removeBundleJS = true, protectedStrings = [] } = options; const _chunkAlias = Array.isArray(chunkAlias) ? chunkAlias : [chunkAlias]; const filter = createFilter(/\.(m?[jt]s|[jt]sx)$/); const escapeRegExpString = (str) => { return str .replace(/\\/g, '\\\\\\\\') .replace(/[|{}()[\]^$+*?.]/g, '\\$&') .replace(/-/g, '\\u002d'); }; const transformAllChunks = _chunkAlias.length === 0; const isBytecodeChunk = (chunkName) => { return transformAllChunks || _chunkAlias.some(alias => alias === chunkName); }; const _transform = (code) => { const re = babel.transform(code, { plugins: ['@babel/plugin-transform-arrow-functions'] }); return re.code || ''; }; const useStrict = '"use strict";'; const bytecodeModuleLoader = 'bytecode-loader.cjs'; let config; let useInRenderer = false; let bytecodeRequired = false; let bytecodeFiles = []; return { name: 'vite:bytecode', apply: 'build', enforce: 'post', configResolved(resolvedConfig) { config = resolvedConfig; useInRenderer = config.plugins.some(p => p.name === 'vite:electron-renderer-preset-config'); if (useInRenderer) { config.logger.warn(colors.yellow('bytecodePlugin does not support renderer.')); } if (resolvedConfig.build.minify && protectedStrings.length > 0) { config.logger.warn(colors.yellow('Strings cannot be protected when minification is enabled.')); } }, transform(code, id) { if (config.build.minify || protectedStrings.length === 0 || !filter(id)) return; let match; let s; protectedStrings.forEach(str => { const escapedStr = escapeRegExpString(str); const re = new RegExp(`\\u0027${escapedStr}\\u0027|\\u0022${escapedStr}\\u0022`, 'g'); const charCodes = Array.from(str).map(s => s.charCodeAt(0)); const replacement = `String.fromCharCode(${charCodes.toString()})`; while ((match = re.exec(code))) { s ||= new MagicString(code); const [full] = match; s.overwrite(match.index, match.index + full.length, replacement, { contentOnly: true }); } }); if (s) { return { code: s.toString(), map: config.build.sourcemap ? s.generateMap({ hires: 'boundary' }) : null }; } }, renderChunk(code, chunk, options) { if (options.format === 'es') { config.logger.warn(colors.yellow('bytecodePlugin does not support ES module, please remove "type": "module" ' + 'in package.json or set the "build.rollupOptions.output.format" option to "cjs".')); return null; } if (useInRenderer) { return null; } if (chunk.type === 'chunk' && isBytecodeChunk(chunk.name)) { bytecodeRequired = true; if (transformArrowFunctions) { return { code: _transform(code) }; } } return null; }, generateBundle(options) { if (options.format !== 'es' && !useInRenderer && bytecodeRequired) { this.emitFile({ type: 'asset', source: bytecodeModuleLoaderCode.join('\n') + '\n', name: 'Bytecode Loader File', fileName: bytecodeModuleLoader }); } }, async writeBundle(options, output) { if (options.format === 'es' || useInRenderer || !bytecodeRequired) { return; } const outDir = options.dir; bytecodeFiles = []; const bundles = Object.keys(output); const chunks = Object.values(output).filter(chunk => chunk.type === 'chunk' && isBytecodeChunk(chunk.name) && chunk.fileName !== bytecodeModuleLoader); const bytecodeChunks = chunks.map(chunk => chunk.fileName); const nonEntryChunks = chunks.filter(chunk => !chunk.isEntry).map(chunk => path.basename(chunk.fileName)); const pattern = nonEntryChunks.map(chunk => `(${chunk})`).join('|'); const bytecodeRE = pattern ? new RegExp(`require\\(\\S*(?=(${pattern})\\S*\\))`, 'g') : null; const keepBundle = (chunkFileName) => { const newFileName = path.resolve(path.dirname(chunkFileName), `_${path.basename(chunkFileName)}`); fs.renameSync(chunkFileName, newFileName); }; const getBytecodeLoaderBlock = (chunkFileName) => { return `require("${toRelativePath(bytecodeModuleLoader, normalizePath(chunkFileName))}");`; }; await Promise.all(bundles.map(async (name) => { const chunk = output[name]; if (chunk.type === 'chunk') { let _code = chunk.code; if (bytecodeRE && _code.match(bytecodeRE)) { let match; const s = new MagicString(_code); while ((match = bytecodeRE.exec(_code))) { const [prefix, chunkName] = match; const len = prefix.length + chunkName.length; s.overwrite(match.index, match.index + len, prefix + chunkName + 'c', { contentOnly: true }); } _code = s.toString(); } const chunkFileName = path.resolve(outDir, name); if (bytecodeChunks.includes(name)) { const bytecodeBuffer = await compileToBytecode(_code); fs.writeFileSync(path.resolve(outDir, name + 'c'), bytecodeBuffer); if (chunk.isEntry) { if (!removeBundleJS) { keepBundle(chunkFileName); } const bytecodeLoaderBlock = getBytecodeLoaderBlock(chunk.fileName); const bytecodeModuleBlock = `require("./${path.basename(name) + 'c'}");`; const code = `${useStrict}\n${bytecodeLoaderBlock}\n${bytecodeModuleBlock}\n`; fs.writeFileSync(chunkFileName, code); } else { if (removeBundleJS) { fs.unlinkSync(chunkFileName); } else { keepBundle(chunkFileName); } } bytecodeFiles.push({ name: name + 'c', size: bytecodeBuffer.length }); } 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 && !moduleInfo.isExternal) { const { importers, dynamicImporters } = moduleInfo; for (const importerId of importers) idsToHandle.add(importerId); for (const importerId of dynamicImporters) idsToHandle.add(importerId); } } const bytecodeLoaderBlock = getBytecodeLoaderBlock(chunk.fileName); _code = hasBytecodeMoudle ? _code.replace(/("use strict";)|('use strict';)/, `${useStrict}\n${bytecodeLoaderBlock}`) : _code; } fs.writeFileSync(chunkFileName, _code); } } })); }, closeBundle() { if (!useInRenderer) { const chunkLimit = config.build.chunkSizeWarningLimit; const outDir = normalizePath(path.relative(config.root, path.resolve(config.root, config.build.outDir))) + '/'; config.logger.info(`${colors.green(`✓`)} ${bytecodeFiles.length} bundles compiled into bytecode.`); let longest = 0; bytecodeFiles.forEach(file => { const len = file.name.length; if (len > longest) longest = len; }); bytecodeFiles.forEach(file => { const kbs = file.size / 1000; config.logger.info(`${colors.gray(colors.white(colors.dim(outDir)))}${colors.green(file.name.padEnd(longest + 2))} ${kbs > chunkLimit ? colors.yellow(`${kbs.toFixed(2)} kB`) : colors.dim(`${kbs.toFixed(2)} kB`)}`); }); bytecodeFiles = []; } } }; } /** * Automatically externalize dependencies */ function externalizeDepsPlugin(options = {}) { const { exclude = [], include = [] } = options; const pkg = loadPackageData() || {}; let deps = Object.keys(pkg.dependencies || {}); if (include.length) { deps = deps.concat(include.filter(dep => dep.trim() !== '')); } if (exclude.length) { deps = deps.filter(dep => !exclude.includes(dep)); } deps = [...new Set(deps)]; return { name: 'vite:externalize-deps', enforce: 'pre', config(config) { const defaultConfig = { build: { rollupOptions: { external: deps.length > 0 ? [...deps, new RegExp(`^(${deps.join('|')})/.+`)] : [] } } }; const buildConfig = mergeConfig(defaultConfig.build, config.build || {}); config.build = buildConfig; } }; } async function transformWithSWC(code, id, options) { const { sourcemap = false, minify = false } = options; delete options.sourcemap; delete options.minify; const isTs = /\.tsx?$/.test(id); const require = createRequire(import.meta.url); let swc; try { swc = require('@swc/core'); } catch { throw new Error('swc plugin require @swc/core, you need to install it.'); } const jsc = { parser: { syntax: isTs ? 'typescript' : 'ecmascript', decorators: true }, transform: { legacyDecorator: true, decoratorMetadata: true, ...options }, keepClassNames: true, target: 'es2022', minify: { format: { comments: false } } }; const result = await swc.transform(code, { jsc, sourceMaps: sourcemap, minify, configFile: false, swcrc: false }); const map = sourcemap && result.map ? JSON.parse(result.map) : { mappings: '' }; return { code: result.code, map }; } /** * Use SWC to support for emitting type metadata for decorators. * When using `swcPlugin`, you need to install `@swc/core`. */ function swcPlugin(options = {}) { const filter = createFilter(options.include || /\.(m?ts|[jt]sx)$/, options.exclude || /\.js$/); let sourcemap = false; let minify = false; return { name: 'vite:swc', config() { return { esbuild: false }; }, async configResolved(resolvedConfig) { sourcemap = resolvedConfig.build?.sourcemap === 'inline' ? 'inline' : !!resolvedConfig.build?.sourcemap; minify = resolvedConfig.build?.minify; }, async transform(code, id) { if (filter(id)) { const result = await transformWithSWC(code, id, { sourcemap, ...(options.transformOptions || {}) }); return { code: result.code, map: result.map }; } }, async renderChunk(code, chunk) { if (!minify || minify === 'terser') { return null; } const result = await transformWithSWC(code, chunk.fileName, { sourcemap, minify: true, ...(options.transformOptions || {}) }); return { code: result.code, map: result.map }; } }; } export { bytecodePlugin, externalizeDepsPlugin, swcPlugin };