@herberttn/bytenode-webpack-plugin
Version:
Compile JavaScript into bytecode using bytenode
258 lines • 13.5 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BytenodeWebpackPlugin = void 0;
const path_1 = require("path");
const replace_string_1 = __importDefault(require("replace-string"));
const webpack_1 = require("webpack");
const webpack_virtual_modules_1 = __importDefault(require("webpack-virtual-modules"));
const loaders_1 = require("./loaders");
const sources_1 = require("./sources");
const utils_1 = require("./utils");
class BytenodeWebpackPlugin {
constructor(options = {}) {
this.name = 'BytenodeWebpackPlugin';
this.options = {
compileAsModule: true,
compileForElectron: false,
debugLifecycle: false,
keepSource: false,
preventSourceMaps: true,
...options,
};
}
apply(compiler) {
const logger = compiler.getInfrastructureLogger(this.name);
setupLifecycleLogging(compiler, this.name, this.options);
logger.debug('original webpack.options.entry', compiler.options.entry);
const { entries: { ignored, loaders, targets }, modules } = prepare(compiler);
logger.debug('prepared ignores', Object.fromEntries(ignored.entries()));
logger.debug('prepared loaders', Object.fromEntries(loaders.entries()));
logger.debug('prepared targets', Object.fromEntries(targets.entries()));
logger.debug('prepared modules', Object.fromEntries(modules.entries()));
compiler.options.entry = Object.fromEntries([
...ignored.entries(),
...loaders.entries(),
...targets.entries(),
]);
if (this.options.preventSourceMaps) {
logger.log('Preventing source maps from being generated by changing webpack.options.devtool to false.');
compiler.options.devtool = false;
}
if (this.options.compileForElectron) {
const target = compiler.options.target;
if (target) {
const targets = Array.isArray(target) ? target : [target];
if (!targets.some(target => target.startsWith('electron-'))) {
logger.warn(`Consider using an electron target instead of or in addition to [${targets.join(', ')}] when compiling for electron.`);
}
}
}
logger.debug('modified webpack.options.devtool', compiler.options.devtool);
logger.debug('modified webpack.options.entry', compiler.options.entry);
logger.debug('adding electron as external');
new webpack_1.ExternalsPlugin('commonjs', ['electron'])
.apply(compiler);
logger.debug('adding target imports from loader code as external');
new webpack_1.ExternalsPlugin('commonjs', ({ context, contextInfo, request }, callback) => {
if (context && contextInfo && request) {
const requestLocation = (0, path_1.resolve)(context, request);
if (contextInfo.issuer === (0, utils_1.toLoaderFileName)(requestLocation)) {
for (const target of Array.from(targets.values()).flatMap(target => target.import)) {
const targetLocation = (0, path_1.resolve)(compiler.context, target);
if (target === request || targetLocation === requestLocation) {
logger.debug('external: context', { context, contextInfo, request, requestLocation, target, targetLocation });
logger.debug('external: resolved to', target);
return callback(undefined, target);
}
}
}
}
return callback();
}).apply(compiler);
new webpack_virtual_modules_1.default(Object.fromEntries(modules.entries()))
.apply(compiler);
compiler.hooks.afterPlugins.tap(this.name, () => {
logger.debug('hook: after plugins');
const matches = (0, utils_1.createFileMatcher)(this.options.include, this.options.exclude);
compiler.hooks.compilation.tap(this.name, compilation => {
logger.debug('hook: compilation');
const stats = compilation.getLogger(this.name);
const loaderOutputFiles = [];
const targetOutputFiles = [];
compilation.hooks.processAssets.tap({ name: this.name, stage: webpack_1.Compilation.PROCESS_ASSETS_STAGE_PRE_PROCESS }, () => {
logger.debug('hook: process assets');
stats.time('collect asset names');
loaderOutputFiles.push(...collectOutputFiles(compilation, loaders));
logger.debug('collected: loader output files', loaderOutputFiles);
targetOutputFiles.push(...collectOutputFiles(compilation, targets));
logger.debug('collected: target output files', targetOutputFiles);
stats.timeEnd('collect asset names');
});
compilation.hooks.processAssets.tapPromise({ name: this.name, stage: webpack_1.Compilation.PROCESS_ASSETS_STAGE_DERIVED }, async (assets) => {
logger.debug('hook: process assets promise');
stats.time('process assets');
for (const [name, asset] of Object.entries(assets)) {
stats.group('asset', name);
stats.time('took');
if (loaderOutputFiles.includes(name)) {
await updateLoaderToRequireCompiledAssets(compilation, name, asset);
}
else if ((0, utils_1.isTargetExtension)(name) && matches(name)) {
await updateTargetWithCompiledCode(compilation, name, asset, this.options);
}
stats.timeEnd('took');
stats.groupEnd('asset', name);
}
stats.timeEnd('process assets');
});
async function updateLoaderToRequireCompiledAssets(compilation, name, asset) {
logger.debug('updating loader to require compiled assets', { name });
const source = await (0, sources_1.replaceSource)(asset, raw => {
logger.debug('initializing external target replacer');
logger.debug({ outputPath: compiler.outputPath });
for (let index = 0; index < targets.size; index++) {
const target = Array.from(targets.values())[index];
const fromLocation = loaderOutputFiles[index];
const toLocation = targetOutputFiles[index];
logger.debug('replacer', { name, target: { name: Array.from(targets.keys())[index], ...target }, fromLocation, toLocation });
let to = (0, path_1.relative)((0, path_1.dirname)(fromLocation), toLocation);
if (!to.startsWith('.')) {
to = (0, utils_1.toSiblingRelativeFileLocation)(to);
}
if (compiler.options.mode === 'development' && compiler.options.target === 'electron-renderer') {
to = (0, path_1.resolve)(compiler.outputPath, toLocation);
}
for (const from of target.import) {
raw = replaceImportPath(raw, name, from, to, {
permutations: [
utils_1.normalizeCodePath,
],
});
}
}
logger.debug('initializing compiled target replacer');
for (const file of targetOutputFiles) {
raw = replaceImportPath(raw, name, file, (0, utils_1.fromTargetToCompiledExtension)(file), {
permutations: [
utils_1.normalizeCodePathForUnix,
utils_1.normalizeCodePathForWindows,
],
});
}
return raw;
});
compilation.updateAsset(name, source);
function replaceImportPath(raw, name, from, to, options) {
const { permutations = [(identity) => identity] } = options ?? {};
for (const transform of permutations) {
const fromTransformed = `${transform(from)}"`;
const toTransformed = `${transform(to)}"`;
logger.debug('replacing within', name);
logger.debug(' from:', fromTransformed);
logger.debug(' to:', toTransformed);
raw = (0, replace_string_1.default)(raw, fromTransformed, toTransformed);
}
return raw;
}
}
});
});
async function updateTargetWithCompiledCode(compilation, name, asset, options) {
logger.debug('compiling asset source', { name });
const source = await (0, sources_1.compileSource)(asset, options);
logger.debug('updating asset source with the compiled content');
compilation.updateAsset(name, source);
const to = (0, utils_1.fromTargetToCompiledExtension)(name);
logger.debug(`renaming asset to ${to}`);
compilation.renameAsset(name, to);
if (options.keepSource) {
logger.debug('re-emitting decompiled asset due to plugin.options.keepSource being true');
compilation.emitAsset(name, asset);
}
else {
logger.debug('NOT re-emitting decompiled asset due to plugin.options.keepSource being false');
}
}
}
}
exports.BytenodeWebpackPlugin = BytenodeWebpackPlugin;
function collectOutputFiles(compilation, from) {
const files = [];
for (const name of from.keys()) {
const entrypoint = compilation.entrypoints.get(name);
if (entrypoint) {
files.push(...entrypoint.chunks.flatMap(chunk => Array.from(chunk.files.values())));
}
}
return files;
}
function prepare(compiler) {
const { entry, output } = compiler.options;
if (typeof entry === 'function') {
throw new Error('webpack.options.entry cannot be a function, use strings or objects');
}
if (typeof output.filename === 'string' && !/.*[[\]]+.*/.test(output.filename)) {
throw new Error('webpack.options.output.filename cannot be static, use a dynamic one like [name].js');
}
const entries = {
ignored: new Map(),
loaders: new Map(),
targets: new Map(),
};
const modules = new Map();
for (const [name, descriptor] of Object.entries(entry)) {
if (descriptor.filename) {
throw new Error('webpack.options.entry.filename is not supported, use webpack.options.output.filename');
}
const imports = descriptor.import;
entries.targets.set(name + '.compiled', { ...descriptor, import: imports });
entries.loaders.set(name, { import: imports.map(file => (0, utils_1.toLoaderFileName)(file)) });
for (const file of imports) {
const code = (0, loaders_1.createLoaderCode)({ imports: [(0, utils_1.toSiblingRelativeFileLocation)(file)] });
const location = (0, utils_1.toLoaderFileName)(file);
modules.set(location, code);
}
}
return {
entries,
modules,
};
}
function setupLifecycleLogging(compiler, name, options) {
if (!options.debugLifecycle) {
return;
}
const logger = compiler.getInfrastructureLogger(`${name}/lifecycle`);
setupHooksLogging(name, 'compiler', compiler.hooks);
compiler.hooks.compilation.tap(name, compilation => {
setupHooksLogging(name, 'compilation', compilation.hooks);
});
compiler.hooks.normalModuleFactory.tap(name, normalModuleFactory => {
setupHooksLogging(name, 'normalModuleFactory', normalModuleFactory.hooks);
});
function setupHooksLogging(pluginName, type, hooks) {
const deprecatedHooks = [
'additionalChunkAssets',
'afterOptimizeChunkAssets',
'normalModuleLoader',
'optimizeChunkAssets',
];
const recursiveHooks = ['infrastructureLog', 'log'];
for (const [name, hook] of Object.entries(hooks)) {
try {
if (deprecatedHooks.includes(name) || recursiveHooks.includes(name)) {
return;
}
hook.tap(pluginName, () => {
logger.debug(`${type} hook ${name} (${arguments.length} arguments)`);
});
}
catch (_) {
}
}
}
}
//# sourceMappingURL=plugin.js.map