UNPKG

vite-plugin-v8-bytecode

Version:

A Vite plugin for compiling JavaScript to V8 bytecode for Node.js and Electron applications

428 lines (424 loc) 14.6 kB
import path from 'path'; import MagicString from 'magic-string'; import vm from 'vm'; import v8 from 'v8'; import * as babel from '@babel/core'; // src/index.ts try { v8.setFlagsFromString("--no-lazy"); v8.setFlagsFromString("--no-flush-bytecode"); } catch (e) { console.warn("Warning: Could not set V8 flags for bytecode compilation:", e); } function wrapInModuleWrapper(code) { return `(function (exports, require, module, __filename, __dirname) { ${code} });`; } function compileToBytecode(code) { const wrappedCode = wrapInModuleWrapper(code); const script = new vm.Script(wrappedCode, { produceCachedData: true }); if (!script.cachedData) { throw new Error("Failed to generate bytecode: cachedData is undefined"); } const bytecode = script.cachedData; setFlagHashHeader(bytecode); return bytecode; } var dummyBytecode; function setFlagHashHeader(bytecodeBuffer) { const FLAG_HASH_OFFSET = 12; if (!dummyBytecode) { const script = new vm.Script("", { produceCachedData: true }); dummyBytecode = script.cachedData; } if (dummyBytecode && dummyBytecode.length > FLAG_HASH_OFFSET + 4) { dummyBytecode.subarray(FLAG_HASH_OFFSET, FLAG_HASH_OFFSET + 4).copy(bytecodeBuffer, FLAG_HASH_OFFSET); } } // src/loader.ts function getBytecodeLoaderCode() { return [ `"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.cachedData;`, ` }`, ` 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);`, `};` ].join("\n") + "\n"; } function protectStringsPlugin(api) { const { types: t } = api; function createFromCharCodeFunction(value) { const charCodes = Array.from(value).map((s) => s.charCodeAt(0)); const charCodeLiterals = charCodes.map((code) => t.numericLiteral(code)); const memberExpression = t.memberExpression( t.identifier("String"), t.identifier("fromCharCode") ); const callExpression = t.callExpression(memberExpression, [ t.spreadElement(t.identifier("arr")) ]); const returnStatement = t.returnStatement(callExpression); const functionExpression = t.functionExpression( null, [t.identifier("arr")], t.blockStatement([returnStatement]) ); return t.callExpression(functionExpression, [ t.arrayExpression(charCodeLiterals) ]); } return { name: "protect-strings-plugin", visitor: { StringLiteral(path3, state) { if (path3.parentPath.isMemberExpression({ property: path3.node, computed: true })) { return; } if (path3.parentPath.isObjectProperty({ key: path3.node, computed: false })) { return; } if (path3.parentPath.isCallExpression() && t.isIdentifier(path3.parentPath.node.callee) && path3.parentPath.node.callee.name === "require" && path3.parentPath.node.arguments[0] === path3.node) { return; } const { value } = path3.node; if (state.opts.protectedStrings.has(value)) { path3.replaceWith(createFromCharCodeFunction(value)); } }, TemplateLiteral(path3, state) { if (path3.node.expressions.length > 0 || path3.node.quasis.length !== 1) { return; } const value = path3.node.quasis[0].value.cooked; if (value && state.opts.protectedStrings.has(value)) { path3.replaceWith(createFromCharCodeFunction(value)); } } } }; } function templateLiteralToConcatPlugin(api) { const { types: t } = api; return { name: "template-literal-to-concat", visitor: { TemplateLiteral(path3) { const { quasis, expressions } = path3.node; let result = null; for (let i = 0; i < quasis.length; i++) { const quasi = quasis[i]; const expr = expressions[i]; if (quasi.value.cooked) { const stringLiteral = t.stringLiteral(quasi.value.cooked); result = result ? t.binaryExpression("+", result, stringLiteral) : stringLiteral; } if (expr && t.isExpression(expr)) { result = result ? t.binaryExpression("+", result, expr) : expr; } } if (result) { path3.replaceWith(result); } else { path3.replaceWith(t.stringLiteral("")); } } } }; } function transformCode(code, protectedStrings, sourceMaps = false) { const plugins = [ // ALWAYS convert template literals for bytecode compatibility templateLiteralToConcatPlugin ]; if (protectedStrings.length > 0) { plugins.push([ protectStringsPlugin, { protectedStrings: new Set(protectedStrings) } ]); } const result = babel.transformSync(code, { plugins, sourceMaps, configFile: false, babelrc: false }); return result ? { code: result.code || "", map: result.map || void 0 } : null; } function toRelativePath(from, to) { const relativePath = path.relative(path.dirname(to), from); return relativePath.startsWith(".") ? relativePath : `./${relativePath}`; } function resolveBuildOutputs(outputs, libOptions) { if (libOptions && !Array.isArray(outputs)) { const libFormats = libOptions.formats || []; return libFormats.map((format) => ({ ...outputs, format })); } return outputs; } function normalizePath(p) { return p.replace(/\\/g, "/"); } // src/index.ts var bytecodeChunkExtensionRE = /\.(jsc|cjsc)$/; function bytecodePlugin(options = {}) { if (process.env.NODE_ENV !== "production") { return null; } const { chunkAlias = [], removeBundleJS = true, protectedStrings = [] } = options; const _chunkAlias = Array.isArray(chunkAlias) ? chunkAlias : [chunkAlias]; const transformAllChunks = _chunkAlias.length === 0; const isBytecodeChunk = (chunkName) => { return transformAllChunks || _chunkAlias.some((alias) => alias === chunkName); }; const useStrict = '"use strict";'; const bytecodeModuleLoader = "bytecode-loader.cjs"; let logger; let supported = false; return { name: "vite:bytecode", apply: "build", enforce: "post", configResolved(config) { if (supported) { return; } logger = config.logger; const useInRenderer = config.plugins.some( (p) => p.name === "vite:electron-renderer-preset-config" ); if (useInRenderer) { config.logger.warn("bytecodePlugin does not support renderer."); return; } const build = config.build; const resolvedOutputs = resolveBuildOutputs( build.rollupOptions.output, build.lib ); if (resolvedOutputs) { const outputs = Array.isArray(resolvedOutputs) ? resolvedOutputs : [resolvedOutputs]; const output = outputs[0]; if (output.format === "es") { config.logger.warn( 'bytecodePlugin does not support ES module output format. Please set "build.rollupOptions.output.format" to "cjs".' ); } supported = output.format === "cjs" && !useInRenderer; } }, renderChunk(code, chunk, { sourcemap }) { if (supported && isBytecodeChunk(chunk.name)) { return transformCode(code, protectedStrings, !!sourcemap); } return null; }, async generateBundle(_, output) { if (!supported) { return; } const _chunks = Object.values(output); const chunks = _chunks.filter( (chunk) => chunk.type === "chunk" && isBytecodeChunk(chunk.name) ); if (chunks.length === 0) { return; } 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 getBytecodeLoaderBlock = (chunkFileName) => { return `require("${toRelativePath( bytecodeModuleLoader, normalizePath(chunkFileName) )}");`; }; let bytecodeChunkCount = 0; const bundles = Object.keys(output); await Promise.all( bundles.map(async (name) => { const chunk = output[name]; if (chunk.type === "chunk") { let _code = chunk.code; if (bytecodeRE) { let match; let s; 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 }); 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} ${bytecodeLoaderBlock} ${bytecodeModuleBlock} `; chunk.code = code; } else { delete output[chunk.fileName]; } bytecodeChunkCount += 1; } else { if (chunk.isEntry) { let hasBytecodeModule = false; const idsToHandle = /* @__PURE__ */ new Set([ ...chunk.imports, ...chunk.dynamicImports ]); for (const moduleId of idsToHandle) { if (bytecodeChunks.includes(moduleId)) { hasBytecodeModule = 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); } } _code = hasBytecodeModule ? _code.replace( /("use strict";)|('use strict';)/, `${useStrict} ${getBytecodeLoaderBlock(chunk.fileName)}` ) : _code; } chunk.code = _code; } } }) ); if (bytecodeChunkCount && !_chunks.some( (ass) => ass.type === "asset" && ass.fileName === bytecodeModuleLoader )) { this.emitFile({ type: "asset", source: getBytecodeLoaderCode(), name: "Bytecode Loader File", fileName: bytecodeModuleLoader }); } }, writeBundle(_, output) { if (supported) { const bytecodeChunkCount = Object.keys(output).filter( (chunk) => bytecodeChunkExtensionRE.test(chunk) ).length; logger.info(`\u2713 ${bytecodeChunkCount} chunks compiled into bytecode.`); } } }; } export { bytecodePlugin }; //# sourceMappingURL=index.js.map //# sourceMappingURL=index.js.map