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
JavaScript
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