Skip to content

Commit

Permalink
add experimental support for build time execution
Browse files Browse the repository at this point in the history
allow to execute a part of the module graph at build time
e. g. to generate code or other assets

loaders have access to that via `this.importModule(request, options)`
  • Loading branch information
sokra committed Apr 12, 2021
1 parent 57f0426 commit 065177d
Show file tree
Hide file tree
Showing 22 changed files with 766 additions and 76 deletions.
382 changes: 339 additions & 43 deletions lib/Compilation.js

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions lib/asset/AssetModulesPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,20 @@ class AssetModulesPlugin {

return result;
});

compilation.hooks.runModule.tap(
"AssetModulesPlugin",
(options, context) => {
const { codeGenerationResult } = options;
const source = codeGenerationResult.sources.get("asset");
if (source === undefined) return;
compilation.emitAsset(
codeGenerationResult.data.get("filename"),
source,
codeGenerationResult.data.get("assetInfo")
);
}
);
}
);
}
Expand Down
28 changes: 28 additions & 0 deletions lib/dependencies/LoaderImportDependency.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/

"use strict";

const ModuleDependency = require("./ModuleDependency");

class LoaderImportDependency extends ModuleDependency {
/**
* @param {string} request request string
*/
constructor(request) {
super(request);
this.weak = true;
}

get type() {
return "loader import";
}

get category() {
return "loaderImport";
}
}

module.exports = LoaderImportDependency;
92 changes: 90 additions & 2 deletions lib/dependencies/LoaderPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,37 @@
const NormalModule = require("../NormalModule");
const LazySet = require("../util/LazySet");
const LoaderDependency = require("./LoaderDependency");
const LoaderImportDependency = require("./LoaderImportDependency");

/** @typedef {import("../Compilation").DepConstructor} DepConstructor */
/** @typedef {import("../Compiler")} Compiler */
/** @typedef {import("../Module")} Module */

/**
* @callback LoadModuleCallback
* @param {Error=} err error object
* @param {string=} source source code
* @param {string | Buffer=} source source code
* @param {object=} map source map
* @param {Module=} module loaded module if successful
*/

/**
* @callback ImportModuleCallback
* @param {Error=} err error object
* @param {any=} exports exports of the evaluated module
*/

/**
* @typedef {Object} ImportModuleOptions
* @property {string=} layer the target layer
*/

class LoaderPlugin {
/**
* Apply the plugin
* @param {Compiler} compiler the compiler instance
* @returns {void}
*/
apply(compiler) {
compiler.hooks.compilation.tap(
"LoaderPlugin",
Expand All @@ -28,6 +47,10 @@ class LoaderPlugin {
LoaderDependency,
normalModuleFactory
);
compilation.dependencyFactories.set(
LoaderImportDependency,
normalModuleFactory
);
}
);

Expand All @@ -47,7 +70,7 @@ class LoaderPlugin {
name: request
};
const factory = compilation.dependencyFactories.get(
dep.constructor
/** @type {DepConstructor} */ (dep.constructor)
);
if (factory === undefined) {
return callback(
Expand Down Expand Up @@ -123,6 +146,71 @@ class LoaderPlugin {
}
);
};

/**
* @param {string} request the request string to load the module from
* @param {ImportModuleOptions=} options options
* @param {ImportModuleCallback=} callback callback returning the exports
* @returns {void}
*/
const importModule = (request, options, callback) => {
const dep = new LoaderImportDependency(request);
dep.loc = {
name: request
};
const factory = compilation.dependencyFactories.get(
/** @type {DepConstructor} */ (dep.constructor)
);
if (factory === undefined) {
return callback(
new Error(
`No module factory available for dependency type: ${dep.constructor.name}`
)
);
}
compilation.buildQueue.increaseParallelism();
compilation.handleModuleCreation(
{
factory,
dependencies: [dep],
originModule: loaderContext._module,
contextInfo: {
issuerLayer: options.layer
},
context: loaderContext.context,
recursive: true
},
err => {
compilation.buildQueue.decreaseParallelism();
if (err) {
return callback(err);
}
const referencedModule = moduleGraph.getModule(dep);
if (!referencedModule) {
return callback(new Error("Cannot load the module"));
}
compilation.runModule(referencedModule, {}, callback);
}
);
};

/**
* @param {string} request the request string to load the module from
* @param {ImportModuleOptions} options options
* @param {ImportModuleCallback=} callback callback returning the exports
* @returns {Promise<any> | void} exports
*/
loaderContext.importModule = (request, options, callback) => {
if (!callback) {
return new Promise((resolve, reject) => {
importModule(request, options || {}, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
}
return importModule(request, options || {}, callback);
};
}
);
});
Expand Down
4 changes: 2 additions & 2 deletions lib/javascript/ArrayPushCallbackChunkFormatPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ class ArrayPushCallbackChunkFormatPlugin {
compilation => {
compilation.hooks.additionalChunkRuntimeRequirements.tap(
"ArrayPushCallbackChunkFormatPlugin",
(chunk, set) => {
(chunk, set, { chunkGraph }) => {
if (chunk.hasRuntime()) return;
if (compilation.chunkGraph.getNumberOfEntryModules(chunk) > 0) {
if (chunkGraph.getNumberOfEntryModules(chunk) > 0) {
set.add(RuntimeGlobals.onChunksLoaded);
set.add(RuntimeGlobals.require);
}
Expand Down
4 changes: 2 additions & 2 deletions lib/javascript/CommonJsChunkFormatPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ class CommonJsChunkFormatPlugin {
compilation => {
compilation.hooks.additionalChunkRuntimeRequirements.tap(
"CommonJsChunkLoadingPlugin",
(chunk, set) => {
(chunk, set, { chunkGraph }) => {
if (chunk.hasRuntime()) return;
if (compilation.chunkGraph.getNumberOfEntryModules(chunk) > 0) {
if (chunkGraph.getNumberOfEntryModules(chunk) > 0) {
set.add(RuntimeGlobals.require);
set.add(RuntimeGlobals.startupEntrypoint);
set.add(RuntimeGlobals.externalInstallChunk);
Expand Down
47 changes: 45 additions & 2 deletions lib/javascript/JavascriptModulesPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"use strict";

const { SyncWaterfallHook, SyncHook, SyncBailHook } = require("tapable");
const vm = require("vm");
const {
ConcatSource,
OriginalSource,
Expand Down Expand Up @@ -369,16 +370,58 @@ class JavascriptModulesPlugin {
});
compilation.hooks.additionalTreeRuntimeRequirements.tap(
"JavascriptModulesPlugin",
(chunk, set) => {
(chunk, set, { chunkGraph }) => {
if (
!set.has(RuntimeGlobals.startupNoDefault) &&
compilation.chunkGraph.hasChunkEntryDependentChunks(chunk)
chunkGraph.hasChunkEntryDependentChunks(chunk)
) {
set.add(RuntimeGlobals.onChunksLoaded);
set.add(RuntimeGlobals.require);
}
}
);
compilation.hooks.runModule.tap(
"JavascriptModulesPlugin",
(options, context) => {
const source = options.codeGenerationResult.sources.get(
"javascript"
);
if (source === undefined) return;
const { module, moduleObject } = options;
const code = source.source();

const fn = vm.runInThisContext(
`(function(${module.moduleArgument}, ${module.exportsArgument}, __webpack_require__) {\n${code}\n/**/})`,
{
filename: module.identifier(),
lineOffset: -1
}
);
fn.call(
moduleObject.exports,
moduleObject,
moduleObject.exports,
context.__webpack_require__
);
}
);
compilation.hooks.runModule.tap(
"JavascriptModulesPlugin",
(options, context) => {
const source = options.codeGenerationResult.sources.get("runtime");
if (source === undefined) return;
const code = source.source();

const fn = vm.runInThisContext(
`(function(__webpack_require__) {\n${code}\n/**/})`,
{
filename: options.module.identifier(),
lineOffset: -1
}
);
fn.call(null, context.__webpack_require__);
}
);
}
);
}
Expand Down
34 changes: 26 additions & 8 deletions lib/library/AbstractLibraryPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const JavascriptModulesPlugin = require("../javascript/JavascriptModulesPlugin")
/** @typedef {import("../../declarations/WebpackOptions").LibraryOptions} LibraryOptions */
/** @typedef {import("../../declarations/WebpackOptions").LibraryType} LibraryType */
/** @typedef {import("../Chunk")} Chunk */
/** @typedef {import("../ChunkGraph")} ChunkGraph */
/** @typedef {import("../Compilation")} Compilation */
/** @typedef {import("../Compilation").ChunkHashContext} ChunkHashContext */
/** @typedef {import("../Compiler")} Compiler */
Expand All @@ -27,6 +28,7 @@ const COMMON_LIBRARY_NAME_MESSAGE =
* @template T
* @typedef {Object} LibraryContext
* @property {Compilation} compilation
* @property {ChunkGraph} chunkGraph
* @property {T} options
*/

Expand Down Expand Up @@ -75,7 +77,8 @@ class AbstractLibraryPlugin {
if (module) {
this.finishEntryModule(module, name, {
options,
compilation
compilation,
chunkGraph: compilation.chunkGraph
});
}
}
Expand All @@ -101,10 +104,14 @@ class AbstractLibraryPlugin {
) {
compilation.hooks.additionalChunkRuntimeRequirements.tap(
_pluginName,
(chunk, set) => {
(chunk, set, { chunkGraph }) => {
const options = getOptionsForChunk(chunk);
if (options !== false) {
this.runtimeRequirements(chunk, set, { options, compilation });
this.runtimeRequirements(chunk, set, {
options,
compilation,
chunkGraph
});
}
}
);
Expand All @@ -116,7 +123,11 @@ class AbstractLibraryPlugin {
hooks.render.tap(_pluginName, (source, renderContext) => {
const options = getOptionsForChunk(renderContext.chunk);
if (options === false) return source;
return this.render(source, renderContext, { options, compilation });
return this.render(source, renderContext, {
options,
compilation,
chunkGraph: compilation.chunkGraph
});
});
}

Expand All @@ -131,7 +142,8 @@ class AbstractLibraryPlugin {
if (options === false) return;
return this.embedInRuntimeBailout(module, renderContext, {
options,
compilation
compilation,
chunkGraph: compilation.chunkGraph
});
}
);
Expand All @@ -146,7 +158,8 @@ class AbstractLibraryPlugin {
if (options === false) return;
return this.strictRuntimeBailout(renderContext, {
options,
compilation
compilation,
chunkGraph: compilation.chunkGraph
});
});
}
Expand All @@ -161,7 +174,8 @@ class AbstractLibraryPlugin {
if (options === false) return source;
return this.renderStartup(source, module, renderContext, {
options,
compilation
compilation,
chunkGraph: compilation.chunkGraph
});
}
);
Expand All @@ -170,7 +184,11 @@ class AbstractLibraryPlugin {
hooks.chunkHash.tap(_pluginName, (chunk, hash, context) => {
const options = getOptionsForChunk(chunk);
if (options === false) return;
this.chunkHash(chunk, hash, context, { options, compilation });
this.chunkHash(chunk, hash, context, {
options,
compilation,
chunkGraph: compilation.chunkGraph
});
});
});
}
Expand Down
6 changes: 2 additions & 4 deletions lib/prefetch/ChunkPrefetchPreloadPlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ class ChunkPrefetchPreloadPlugin {
compilation => {
compilation.hooks.additionalChunkRuntimeRequirements.tap(
"ChunkPrefetchPreloadPlugin",
(chunk, set) => {
const { chunkGraph } = compilation;
(chunk, set, { chunkGraph }) => {
if (chunkGraph.getNumberOfEntryModules(chunk) === 0) return;
const startupChildChunks = chunk.getChildrenOfTypeInOrder(
chunkGraph,
Expand All @@ -43,8 +42,7 @@ class ChunkPrefetchPreloadPlugin {
);
compilation.hooks.additionalTreeRuntimeRequirements.tap(
"ChunkPrefetchPreloadPlugin",
(chunk, set) => {
const { chunkGraph } = compilation;
(chunk, set, { chunkGraph }) => {
const chunkMap = chunk.getChildIdsByOrdersMap(chunkGraph, false);

if (chunkMap.prefetch) {
Expand Down

0 comments on commit 065177d

Please sign in to comment.