diff --git a/lib/Compilation.js b/lib/Compilation.js index 7a7d40075..2503b9654 100644 --- a/lib/Compilation.js +++ b/lib/Compilation.js @@ -32,7 +32,10 @@ const { connectChunkGroupAndChunk, connectChunkGroupParentAndChild } = require("./GraphHelpers"); -const { makeWebpackError } = require("./HookWebpackError"); +const { + makeWebpackError, + tryRunOrWebpackError +} = require("./HookWebpackError"); const MainTemplate = require("./MainTemplate"); const Module = require("./Module"); const ModuleDependencyError = require("./ModuleDependencyError"); @@ -71,6 +74,7 @@ const { soonFrozenObjectDeprecation, createFakeHook } = require("./util/deprecation"); +const processAsyncTree = require("./util/processAsyncTree"); const { getRuntimeKey } = require("./util/runtime"); const { isSourceEqual } = require("./util/source"); @@ -119,6 +123,13 @@ const { isSourceEqual } = require("./util/source"); * @returns {void} */ +/** + * @callback ExportsCallback + * @param {WebpackError=} err + * @param {any=} exports + * @returns {void} + */ + /** * @callback DepBlockVarDependenciesCallback * @param {Dependency} dependency @@ -158,6 +169,31 @@ const { isSourceEqual } = require("./util/source"); * @property {ChunkGraph} chunkGraph the chunk graph */ +/** + * @typedef {Object} RuntimeRequirementsContext + * @property {ChunkGraph} chunkGraph the chunk graph + * @property {CodeGenerationResults} codeGenerationResults the code generation results + */ + +/** + * @typedef {Object} RunModuleOptions + * @property {EntryOptions=} entryOptions + */ + +/** + * @typedef {Object} RunModuleArgument + * @property {Module} module + * @property {object} moduleObject + * @property {CodeGenerationResult} codeGenerationResult + */ + +/** + * @typedef {Object} RunModuleContext + * @property {Chunk} chunk + * @property {ChunkGraph} chunkGraph + * @property {Function} __webpack_require__ + */ + /** * @typedef {Object} EntryData * @property {Dependency[]} dependencies dependencies of the entrypoint that should be evaluated at startup @@ -525,6 +561,9 @@ BREAKING CHANGE: Asset processing hooks in Compilation has been merged into a si "runtime" ]), + /** @type {SyncHook<[RunModuleArgument, RunModuleContext]>} */ + runModule: new SyncHook(["options", "context"]), + /** @type {AsyncSeriesHook<[Iterable]>} */ finishModules: new AsyncSeriesHook(["modules"]), /** @type {AsyncSeriesHook<[Module]>} */ @@ -568,32 +607,35 @@ BREAKING CHANGE: Asset processing hooks in Compilation has been merged into a si /** @type {SyncBailHook<[], boolean>} */ shouldRecord: new SyncBailHook([]), - /** @type {SyncHook<[Chunk, Set]>} */ + /** @type {SyncHook<[Chunk, Set, RuntimeRequirementsContext]>} */ additionalChunkRuntimeRequirements: new SyncHook([ "chunk", - "runtimeRequirements" + "runtimeRequirements", + "context" ]), - /** @type {HookMap]>>} */ + /** @type {HookMap, RuntimeRequirementsContext]>>} */ runtimeRequirementInChunk: new HookMap( - () => new SyncBailHook(["chunk", "runtimeRequirements"]) + () => new SyncBailHook(["chunk", "runtimeRequirements", "context"]) ), - /** @type {SyncHook<[Module, Set]>} */ + /** @type {SyncHook<[Module, Set, RuntimeRequirementsContext]>} */ additionalModuleRuntimeRequirements: new SyncHook([ "module", - "runtimeRequirements" + "runtimeRequirements", + "context" ]), - /** @type {HookMap]>>} */ + /** @type {HookMap, RuntimeRequirementsContext]>>} */ runtimeRequirementInModule: new HookMap( - () => new SyncBailHook(["module", "runtimeRequirements"]) + () => new SyncBailHook(["module", "runtimeRequirements", "context"]) ), - /** @type {SyncHook<[Chunk, Set]>} */ + /** @type {SyncHook<[Chunk, Set, RuntimeRequirementsContext]>} */ additionalTreeRuntimeRequirements: new SyncHook([ "chunk", - "runtimeRequirements" + "runtimeRequirements", + "context" ]), - /** @type {HookMap]>>} */ + /** @type {HookMap, RuntimeRequirementsContext]>>} */ runtimeRequirementInTree: new HookMap( - () => new SyncBailHook(["chunk", "runtimeRequirements"]) + () => new SyncBailHook(["chunk", "runtimeRequirements", "context"]) ), /** @type {SyncHook<[RuntimeModule, Chunk]>} */ @@ -928,6 +970,8 @@ BREAKING CHANGE: Asset processing hooks in Compilation has been merged into a si this.builtModules = new WeakSet(); /** @type {WeakSet} */ this.codeGeneratedModules = new WeakSet(); + /** @type {WeakSet} */ + this.buildTimeExecutedModules = new WeakSet(); /** @private @type {Map} */ this._rebuildingModules = new Map(); /** @type {Set} */ @@ -2644,59 +2688,7 @@ Or do you want to use the entrypoints '${name}' and '${runtime}' independently o }); } - /** - * @returns {void} - */ - processRuntimeRequirements() { - const { chunkGraph } = this; - - const additionalModuleRuntimeRequirements = this.hooks - .additionalModuleRuntimeRequirements; - const runtimeRequirementInModule = this.hooks.runtimeRequirementInModule; - for (const module of this.modules) { - if (chunkGraph.getNumberOfModuleChunks(module) > 0) { - for (const runtime of chunkGraph.getModuleRuntimes(module)) { - let set; - const runtimeRequirements = this.codeGenerationResults.getRuntimeRequirements( - module, - runtime - ); - if (runtimeRequirements && runtimeRequirements.size > 0) { - set = new Set(runtimeRequirements); - } else if (additionalModuleRuntimeRequirements.isUsed()) { - set = new Set(); - } else { - continue; - } - additionalModuleRuntimeRequirements.call(module, set); - - for (const r of set) { - const hook = runtimeRequirementInModule.get(r); - if (hook !== undefined) hook.call(module, set); - } - chunkGraph.addModuleRuntimeRequirements(module, runtime, set); - } - } - } - - for (const chunk of this.chunks) { - const set = new Set(); - for (const module of chunkGraph.getChunkModulesIterable(chunk)) { - const runtimeRequirements = chunkGraph.getModuleRuntimeRequirements( - module, - chunk.runtime - ); - for (const r of runtimeRequirements) set.add(r); - } - this.hooks.additionalChunkRuntimeRequirements.call(chunk, set); - - for (const r of set) { - this.hooks.runtimeRequirementInChunk.for(r).call(chunk, set); - } - - chunkGraph.addChunkRuntimeRequirements(chunk, set); - } - + _getChunkGraphEntries() { /** @type {Set} */ const treeEntries = new Set(); for (const ep of this.entrypoints.values()) { @@ -2707,8 +2699,74 @@ Or do you want to use the entrypoints '${name}' and '${runtime}' independently o const chunk = ep.getRuntimeChunk(); if (chunk) treeEntries.add(chunk); } + return treeEntries; + } - for (const treeEntry of treeEntries) { + /** + * @param {Object} options options + * @param {ChunkGraph=} options.chunkGraph the chunk graph + * @param {Iterable=} options.modules modules + * @param {Iterable=} options.chunks chunks + * @param {CodeGenerationResults=} options.codeGenerationResults codeGenerationResults + * @param {Iterable=} options.chunkGraphEntries chunkGraphEntries + * @returns {void} + */ + processRuntimeRequirements({ + chunkGraph = this.chunkGraph, + modules = this.modules, + chunks = this.chunks, + codeGenerationResults = this.codeGenerationResults, + chunkGraphEntries = this._getChunkGraphEntries() + } = {}) { + const context = { chunkGraph, codeGenerationResults }; + const additionalModuleRuntimeRequirements = this.hooks + .additionalModuleRuntimeRequirements; + const runtimeRequirementInModule = this.hooks.runtimeRequirementInModule; + for (const module of modules) { + if (chunkGraph.getNumberOfModuleChunks(module) > 0) { + for (const runtime of chunkGraph.getModuleRuntimes(module)) { + let set; + const runtimeRequirements = codeGenerationResults.getRuntimeRequirements( + module, + runtime + ); + if (runtimeRequirements && runtimeRequirements.size > 0) { + set = new Set(runtimeRequirements); + } else if (additionalModuleRuntimeRequirements.isUsed()) { + set = new Set(); + } else { + continue; + } + additionalModuleRuntimeRequirements.call(module, set, context); + + for (const r of set) { + const hook = runtimeRequirementInModule.get(r); + if (hook !== undefined) hook.call(module, set, context); + } + chunkGraph.addModuleRuntimeRequirements(module, runtime, set); + } + } + } + + for (const chunk of chunks) { + const set = new Set(); + for (const module of chunkGraph.getChunkModulesIterable(chunk)) { + const runtimeRequirements = chunkGraph.getModuleRuntimeRequirements( + module, + chunk.runtime + ); + for (const r of runtimeRequirements) set.add(r); + } + this.hooks.additionalChunkRuntimeRequirements.call(chunk, set, context); + + for (const r of set) { + this.hooks.runtimeRequirementInChunk.for(r).call(chunk, set, context); + } + + chunkGraph.addChunkRuntimeRequirements(chunk, set); + } + + for (const treeEntry of chunkGraphEntries) { const set = new Set(); for (const chunk of treeEntry.getAllReferencedChunks()) { const runtimeRequirements = chunkGraph.getChunkRuntimeRequirements( @@ -2717,22 +2775,30 @@ Or do you want to use the entrypoints '${name}' and '${runtime}' independently o for (const r of runtimeRequirements) set.add(r); } - this.hooks.additionalTreeRuntimeRequirements.call(treeEntry, set); + this.hooks.additionalTreeRuntimeRequirements.call( + treeEntry, + set, + context + ); for (const r of set) { - this.hooks.runtimeRequirementInTree.for(r).call(treeEntry, set); + this.hooks.runtimeRequirementInTree + .for(r) + .call(treeEntry, set, context); } chunkGraph.addTreeRuntimeRequirements(treeEntry, set); } } + // TODO webpack 6 make chunkGraph argument non-optional /** * @param {Chunk} chunk target chunk * @param {RuntimeModule} module runtime module + * @param {ChunkGraph} chunkGraph the chunk graph * @returns {void} */ - addRuntimeModule(chunk, module) { + addRuntimeModule(chunk, module, chunkGraph = this.chunkGraph) { // Deprecated ModuleGraph association ModuleGraph.setModuleGraphForModule(module, this.moduleGraph); @@ -2741,10 +2807,10 @@ Or do you want to use the entrypoints '${name}' and '${runtime}' independently o this._modules.set(module.identifier(), module); // connect to the chunk graph - this.chunkGraph.connectChunkAndModule(chunk, module); - this.chunkGraph.connectChunkAndRuntimeModule(chunk, module); + chunkGraph.connectChunkAndModule(chunk, module); + chunkGraph.connectChunkAndRuntimeModule(chunk, module); if (module.fullHash) { - this.chunkGraph.addFullHashModuleToChunk(chunk, module); + chunkGraph.addFullHashModuleToChunk(chunk, module); } // attach runtime module @@ -2762,14 +2828,14 @@ Or do you want to use the entrypoints '${name}' and '${runtime}' independently o exportsInfo.setUsedForSideEffectsOnly(runtime); } } - this.chunkGraph.addModuleRuntimeRequirements( + chunkGraph.addModuleRuntimeRequirements( module, chunk.runtime, new Set([RuntimeGlobals.requireScope]) ); // runtime modules don't need ids - this.chunkGraph.setModuleId(module, ""); + chunkGraph.setModuleId(module, ""); // Call hook this.hooks.runtimeModule.call(module, chunk); @@ -3889,6 +3955,236 @@ This prevents using hashes of each other and should be avoided.`); ); } + /** + * @param {Module} module the module + * @param {RunModuleOptions} options options + * @param {ExportsCallback} callback callback + */ + runModule(module, options, callback) { + // Aggregate all referenced modules and ensure they are ready + const modules = new Set([module]); + processAsyncTree( + modules, + 10, + (module, push, callback) => { + this.addModuleQueue.waitFor(module, err => { + if (err) return callback(err); + this.buildQueue.waitFor(module, err => { + if (err) return callback(err); + this.processDependenciesQueue.waitFor(module, err => { + if (err) return callback(err); + for (const { + module: m + } of this.moduleGraph.getOutgoingConnections(module)) { + const size = modules.size; + modules.add(m); + if (modules.size !== size) push(m); + } + callback(); + }); + }); + }); + }, + err => { + if (err) return callback(makeWebpackError(err, "runModule")); + + // Create new chunk graph, chunk and entrypoint for the build time execution + const chunkGraph = new ChunkGraph(this.moduleGraph); + const runtime = "build time"; + const { + hashFunction, + hashDigest, + hashDigestLength + } = this.outputOptions; + const runtimeTemplate = this.runtimeTemplate; + + const chunk = new Chunk("build time chunk"); + chunk.id = chunk.name; + chunk.ids = [chunk.id]; + chunk.runtime = runtime; + + const entrypoint = new Entrypoint({ + runtime, + ...options.entryOptions + }); + chunkGraph.connectChunkAndEntryModule(chunk, module, entrypoint); + connectChunkGroupAndChunk(entrypoint, chunk); + entrypoint.setRuntimeChunk(chunk); + entrypoint.setEntrypointChunk(chunk); + + const chunks = new Set([chunk]); + + const modulesById = new Map(); + + // Assign ids to modules and modules to the chunk + for (const module of modules) { + const id = module.identifier(); + modulesById.set(id, module); + chunkGraph.setModuleId(module, id); + chunkGraph.connectChunkAndModule(chunk, module); + } + + // Hash modules + for (const module of modules) { + this._createModuleHash( + module, + chunkGraph, + runtime, + hashFunction, + runtimeTemplate, + hashDigest, + hashDigestLength + ); + } + + const codeGenerationResults = new CodeGenerationResults(); + /** @type {WebpackError[]} */ + const errors = []; + const codeGen = (module, callback) => { + this._codeGenerationModule( + module, + runtime, + [runtime], + chunkGraph.getModuleHash(module, runtime), + this.dependencyTemplates, + chunkGraph, + this.moduleGraph, + runtimeTemplate, + errors, + codeGenerationResults, + (err, codeGenerated) => { + callback(err); + } + ); + }; + + // Generate code for all aggregated modules + asyncLib.eachLimit(modules, 10, codeGen, err => { + if (err) return callback(makeWebpackError(err, "runModule")); + + // for backward-compat temporary set the chunk graph + // TODO webpack 6 + const old = this.chunkGraph; + this.chunkGraph = chunkGraph; + this.processRuntimeRequirements({ + chunkGraph, + modules, + chunks, + codeGenerationResults, + chunkGraphEntries: chunks + }); + this.chunkGraph = old; + + const runtimeModules = chunkGraph.getChunkRuntimeModulesIterable( + chunk + ); + + // Hash runtime modules + for (const module of runtimeModules) { + this._createModuleHash( + module, + chunkGraph, + runtime, + hashFunction, + runtimeTemplate, + hashDigest, + hashDigestLength + ); + } + + // Generate code for all runtime modules + asyncLib.eachLimit(runtimeModules, 10, codeGen, err => { + if (err) return callback(makeWebpackError(err, "runModule")); + + let result; + try { + const { + strictModuleErrorHandling, + strictModuleExceptionHandling + } = this.outputOptions; + const moduleCache = new Map(); + const __webpack_require__ = id => { + const module = modulesById.get(id); + return __webpack_require_module__(module, id); + }; + const __webpack_require_module__ = (module, id) => { + const cached = moduleCache.get(module); + if (cached !== undefined) { + if (cached.error) throw cached.error; + return cached.exports; + } + const moduleObject = { + id, + exports: {}, + loaded: false, + error: undefined + }; + this.buildTimeExecutedModules.add(module); + try { + moduleCache.set(module, moduleObject); + const codeGenerationResult = codeGenerationResults.get( + module, + runtime + ); + tryRunOrWebpackError( + () => + this.hooks.runModule.call( + { + codeGenerationResult, + module, + moduleObject + }, + context + ), + "Compilation.hooks.runModule" + ); + moduleObject.loaded = true; + return moduleObject.exports; + } catch (e) { + if (strictModuleExceptionHandling) { + moduleCache.delete(module); + } else if (strictModuleErrorHandling) { + moduleObject.error = e; + } + if (!e.module) e.module = module; + throw e; + } + }; + + /** @type {RunModuleContext} */ + const context = { + __webpack_require__, + chunk, + chunkGraph + }; + + for (const runtimeModule of chunkGraph.getChunkRuntimeModulesInOrder( + chunk + )) { + __webpack_require_module__( + runtimeModule, + runtimeModule.identifier() + ); + } + result = __webpack_require__(module.identifier()); + } catch (e) { + const err = new WebpackError( + `Execution of module code from module graph (${module.readableIdentifier( + this.requestShortener + )}) failed: ${e.message}` + ); + err.stack = e.stack; + err.module = e.module; + return callback(err); + } + + callback(null, result); + }); + }); + } + ); + } + checkConstraints() { const chunkGraph = this.chunkGraph; diff --git a/lib/asset/AssetModulesPlugin.js b/lib/asset/AssetModulesPlugin.js index 0ab8da31e..353a48cf1 100644 --- a/lib/asset/AssetModulesPlugin.js +++ b/lib/asset/AssetModulesPlugin.js @@ -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") + ); + } + ); } ); } diff --git a/lib/dependencies/LoaderImportDependency.js b/lib/dependencies/LoaderImportDependency.js new file mode 100644 index 000000000..e740cc918 --- /dev/null +++ b/lib/dependencies/LoaderImportDependency.js @@ -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; diff --git a/lib/dependencies/LoaderPlugin.js b/lib/dependencies/LoaderPlugin.js index 1a82dba5d..98da9e186 100644 --- a/lib/dependencies/LoaderPlugin.js +++ b/lib/dependencies/LoaderPlugin.js @@ -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", @@ -28,6 +47,10 @@ class LoaderPlugin { LoaderDependency, normalModuleFactory ); + compilation.dependencyFactories.set( + LoaderImportDependency, + normalModuleFactory + ); } ); @@ -47,7 +70,7 @@ class LoaderPlugin { name: request }; const factory = compilation.dependencyFactories.get( - dep.constructor + /** @type {DepConstructor} */ (dep.constructor) ); if (factory === undefined) { return callback( @@ -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 | 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); + }; } ); }); diff --git a/lib/javascript/ArrayPushCallbackChunkFormatPlugin.js b/lib/javascript/ArrayPushCallbackChunkFormatPlugin.js index 2d2643870..9ccb9847b 100644 --- a/lib/javascript/ArrayPushCallbackChunkFormatPlugin.js +++ b/lib/javascript/ArrayPushCallbackChunkFormatPlugin.js @@ -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); } diff --git a/lib/javascript/CommonJsChunkFormatPlugin.js b/lib/javascript/CommonJsChunkFormatPlugin.js index e6a32a097..9c9caa245 100644 --- a/lib/javascript/CommonJsChunkFormatPlugin.js +++ b/lib/javascript/CommonJsChunkFormatPlugin.js @@ -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); diff --git a/lib/javascript/JavascriptModulesPlugin.js b/lib/javascript/JavascriptModulesPlugin.js index c26bd6604..b46db9c7f 100644 --- a/lib/javascript/JavascriptModulesPlugin.js +++ b/lib/javascript/JavascriptModulesPlugin.js @@ -6,6 +6,7 @@ "use strict"; const { SyncWaterfallHook, SyncHook, SyncBailHook } = require("tapable"); +const vm = require("vm"); const { ConcatSource, OriginalSource, @@ -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__); + } + ); } ); } diff --git a/lib/library/AbstractLibraryPlugin.js b/lib/library/AbstractLibraryPlugin.js index b8f0ab24f..70a4f9c6d 100644 --- a/lib/library/AbstractLibraryPlugin.js +++ b/lib/library/AbstractLibraryPlugin.js @@ -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 */ @@ -27,6 +28,7 @@ const COMMON_LIBRARY_NAME_MESSAGE = * @template T * @typedef {Object} LibraryContext * @property {Compilation} compilation + * @property {ChunkGraph} chunkGraph * @property {T} options */ @@ -75,7 +77,8 @@ class AbstractLibraryPlugin { if (module) { this.finishEntryModule(module, name, { options, - compilation + compilation, + chunkGraph: compilation.chunkGraph }); } } @@ -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 + }); } } ); @@ -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 + }); }); } @@ -131,7 +142,8 @@ class AbstractLibraryPlugin { if (options === false) return; return this.embedInRuntimeBailout(module, renderContext, { options, - compilation + compilation, + chunkGraph: compilation.chunkGraph }); } ); @@ -146,7 +158,8 @@ class AbstractLibraryPlugin { if (options === false) return; return this.strictRuntimeBailout(renderContext, { options, - compilation + compilation, + chunkGraph: compilation.chunkGraph }); }); } @@ -161,7 +174,8 @@ class AbstractLibraryPlugin { if (options === false) return source; return this.renderStartup(source, module, renderContext, { options, - compilation + compilation, + chunkGraph: compilation.chunkGraph }); } ); @@ -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 + }); }); }); } diff --git a/lib/prefetch/ChunkPrefetchPreloadPlugin.js b/lib/prefetch/ChunkPrefetchPreloadPlugin.js index 181b60545..2bcb8b423 100644 --- a/lib/prefetch/ChunkPrefetchPreloadPlugin.js +++ b/lib/prefetch/ChunkPrefetchPreloadPlugin.js @@ -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, @@ -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) { diff --git a/lib/runtime/StartupChunkDependenciesPlugin.js b/lib/runtime/StartupChunkDependenciesPlugin.js index ff393471c..325c2a72e 100644 --- a/lib/runtime/StartupChunkDependenciesPlugin.js +++ b/lib/runtime/StartupChunkDependenciesPlugin.js @@ -37,9 +37,9 @@ class StartupChunkDependenciesPlugin { }; compilation.hooks.additionalTreeRuntimeRequirements.tap( "StartupChunkDependenciesPlugin", - (chunk, set) => { + (chunk, set, { chunkGraph }) => { if (!isEnabledForChunk(chunk)) return; - if (compilation.chunkGraph.hasChunkEntryDependentChunks(chunk)) { + if (chunkGraph.hasChunkEntryDependentChunks(chunk)) { set.add(RuntimeGlobals.startup); set.add(RuntimeGlobals.ensureChunk); set.add(RuntimeGlobals.ensureChunkIncludeEntries); diff --git a/lib/stats/DefaultStatsFactoryPlugin.js b/lib/stats/DefaultStatsFactoryPlugin.js index 287e90ed4..6bdd35675 100644 --- a/lib/stats/DefaultStatsFactoryPlugin.js +++ b/lib/stats/DefaultStatsFactoryPlugin.js @@ -148,6 +148,7 @@ const { makePathsRelative, parseResource } = require("../util/identifier"); * @property {boolean=} cacheable * @property {boolean=} built * @property {boolean=} codeGenerated + * @property {boolean=} buildTimeExecuted * @property {boolean=} cached * @property {boolean=} optional * @property {boolean=} orphan @@ -1077,6 +1078,9 @@ const SIMPLE_EXTRACTORS = { const { compilation, type } = context; const built = compilation.builtModules.has(module); const codeGenerated = compilation.codeGeneratedModules.has(module); + const buildTimeExecuted = compilation.buildTimeExecutedModules.has( + module + ); /** @type {{[x: string]: number}} */ const sizes = {}; for (const sourceType of module.getSourceTypes()) { @@ -1091,6 +1095,7 @@ const SIMPLE_EXTRACTORS = { sizes, built, codeGenerated, + buildTimeExecuted, cached: !built && !codeGenerated }; Object.assign(object, statsModule); diff --git a/lib/stats/DefaultStatsPrinterPlugin.js b/lib/stats/DefaultStatsPrinterPlugin.js index 399bd1845..5f8dcad97 100644 --- a/lib/stats/DefaultStatsPrinterPlugin.js +++ b/lib/stats/DefaultStatsPrinterPlugin.js @@ -306,6 +306,8 @@ const SIMPLE_PRINTERS = { built ? yellow(formatFlag("built")) : undefined, "module.codeGenerated": (codeGenerated, { formatFlag, yellow }) => codeGenerated ? yellow(formatFlag("code generated")) : undefined, + "module.buildTimeExecuted": (buildTimeExecuted, { formatFlag, green }) => + buildTimeExecuted ? green(formatFlag("build time executed")) : undefined, "module.cached": (cached, { formatFlag, green }) => cached ? green(formatFlag("cached")) : undefined, "module.assets": (assets, { formatFlag, magenta }) => diff --git a/lib/util/AsyncQueue.js b/lib/util/AsyncQueue.js index f87031dd0..4e3b963b3 100644 --- a/lib/util/AsyncQueue.js +++ b/lib/util/AsyncQueue.js @@ -99,7 +99,7 @@ class AsyncQueue { } /** - * @param {T} item a item + * @param {T} item an item * @param {Callback} callback callback function * @returns {void} */ @@ -144,7 +144,7 @@ class AsyncQueue { } /** - * @param {T} item a item + * @param {T} item an item * @returns {void} */ invalidate(item) { @@ -156,6 +156,29 @@ class AsyncQueue { } } + /** + * Waits for an already started item + * @param {T} item an item + * @param {Callback} callback callback function + * @returns {void} + */ + waitFor(item, callback) { + const key = this._getKey(item); + const entry = this._entries.get(key); + if (entry === undefined) { + return callback( + new Error("waitFor can only be called for an already started item") + ); + } + if (entry.state === DONE_STATE) { + process.nextTick(() => callback(entry.error, entry.result)); + } else if (entry.callbacks === undefined) { + entry.callbacks = [callback]; + } else { + entry.callbacks.push(callback); + } + } + /** * @returns {void} */ diff --git a/test/Defaults.unittest.js b/test/Defaults.unittest.js index afb8cffd9..cb3a626ec 100644 --- a/test/Defaults.unittest.js +++ b/test/Defaults.unittest.js @@ -421,6 +421,26 @@ describe("Defaults", () => { "...", ], }, + "loaderImport": Object { + "aliasFields": Array [ + "browser", + ], + "conditionNames": Array [ + "import", + "module", + "...", + ], + "extensions": Array [ + ".js", + ".json", + ".wasm", + ], + "mainFields": Array [ + "browser", + "module", + "...", + ], + }, "undefined": Object { "aliasFields": Array [ "browser", @@ -1089,6 +1109,13 @@ describe("Defaults", () => { @@ ... @@ - "browser", @@ ... @@ + - "aliasFields": Array [ + - "browser", + - ], + + "aliasFields": Array [], + @@ ... @@ + - "browser", + @@ ... @@ - "browser", + "node", @@ ... @@ @@ -1223,6 +1250,13 @@ describe("Defaults", () => { @@ ... @@ - "browser", @@ ... @@ + - "aliasFields": Array [ + - "browser", + - ], + + "aliasFields": Array [], + @@ ... @@ + - "browser", + @@ ... @@ - "browser", + "node", + "electron", @@ -1339,6 +1373,13 @@ describe("Defaults", () => { @@ ... @@ - "browser", @@ ... @@ + - "aliasFields": Array [ + - "browser", + - ], + + "aliasFields": Array [], + @@ ... @@ + - "browser", + @@ ... @@ + "node", @@ ... @@ + "electron", diff --git a/test/configCases/loader-import-module/css/colors.js b/test/configCases/loader-import-module/css/colors.js new file mode 100644 index 000000000..d3da74ec7 --- /dev/null +++ b/test/configCases/loader-import-module/css/colors.js @@ -0,0 +1,2 @@ +export const red = "#f00"; +export const green = "#0f0"; diff --git a/test/configCases/loader-import-module/css/file.png b/test/configCases/loader-import-module/css/file.png new file mode 100644 index 000000000..fb53b9ded Binary files /dev/null and b/test/configCases/loader-import-module/css/file.png differ diff --git a/test/configCases/loader-import-module/css/index.js b/test/configCases/loader-import-module/css/index.js new file mode 100644 index 000000000..3dcc2b046 --- /dev/null +++ b/test/configCases/loader-import-module/css/index.js @@ -0,0 +1,13 @@ +import stylesheet from "./stylesheet"; +import stylesheet1 from "./stylesheet?1"; +import otherStylesheet from "./other-stylesheet"; + +it("should be able to use build-time code", () => { + expect(stylesheet).toBe( + 'body { background: url("/public/assets/file.png"); color: #f00; }' + ); + expect(stylesheet1).toBe( + 'body { background: url("/public/assets/file.png?1"); color: #f00; }' + ); + expect(otherStylesheet).toBe("body { background-color: #0f0; }"); +}); diff --git a/test/configCases/loader-import-module/css/loader.js b/test/configCases/loader-import-module/css/loader.js new file mode 100644 index 000000000..aee770b13 --- /dev/null +++ b/test/configCases/loader-import-module/css/loader.js @@ -0,0 +1,6 @@ +exports.pitch = async function (remaining) { + const result = await this.importModule( + this.resourcePath + ".webpack[javascript/auto]" + "!=!" + remaining + ); + return result.default || result; +}; diff --git a/test/configCases/loader-import-module/css/other-stylesheet.js b/test/configCases/loader-import-module/css/other-stylesheet.js new file mode 100644 index 000000000..b8e16318b --- /dev/null +++ b/test/configCases/loader-import-module/css/other-stylesheet.js @@ -0,0 +1,2 @@ +import { green } from "./colors.js"; +export default `body { background-color: ${green}; }`; diff --git a/test/configCases/loader-import-module/css/stylesheet.js b/test/configCases/loader-import-module/css/stylesheet.js new file mode 100644 index 000000000..c056edfa0 --- /dev/null +++ b/test/configCases/loader-import-module/css/stylesheet.js @@ -0,0 +1,4 @@ +import { red } from "./colors.js"; +export default `body { background: url("${ + new URL("./file.png", import.meta.url).href + __resourceQuery +}"); color: ${red}; }`; diff --git a/test/configCases/loader-import-module/css/webpack.config.js b/test/configCases/loader-import-module/css/webpack.config.js new file mode 100644 index 000000000..4b849f26e --- /dev/null +++ b/test/configCases/loader-import-module/css/webpack.config.js @@ -0,0 +1,34 @@ +/** @type {import("../../../../").Configuration} */ +module.exports = { + output: { + publicPath: "/public/" + }, + module: { + parser: { + javascript: { + url: "relative" + } + }, + generator: { + asset: { + filename: "assets/[name][ext][query]" + } + }, + rules: [ + { + test: /stylesheet\.js$/, + use: "./loader", + type: "asset/source" + } + ] + }, + plugins: [ + compiler => + compiler.hooks.done.tap("test case", stats => + expect(stats.compilation.getAsset("assets/file.png")).toHaveProperty( + "info", + expect.objectContaining({ sourceFilename: "file.png" }) + ) + ) + ] +}; diff --git a/types.d.ts b/types.d.ts index 9673a143e..b118f0e5a 100644 --- a/types.d.ts +++ b/types.d.ts @@ -345,6 +345,11 @@ declare abstract class AsyncQueue { }; add(item: T, callback: CallbackFunction): void; invalidate(item: T): void; + + /** + * Waits for an already started item + */ + waitFor(item: T, callback: CallbackFunction): void; stop(): void; increaseParallelism(): void; decreaseParallelism(): void; @@ -1242,6 +1247,7 @@ declare class Compilation { dependencyReferencedExports: SyncWaterfallHook< [(string[] | ReferencedExport)[], Dependency, RuntimeSpec] >; + runModule: SyncHook<[RunModuleArgument, RunModuleContext]>; finishModules: AsyncSeriesHook<[Iterable]>; finishRebuildingModule: AsyncSeriesHook<[Module]>; unseal: SyncHook<[]>; @@ -1263,14 +1269,24 @@ declare class Compilation { >; afterOptimizeChunkModules: SyncHook<[Iterable, Iterable]>; shouldRecord: SyncBailHook<[], boolean>; - additionalChunkRuntimeRequirements: SyncHook<[Chunk, Set]>; - runtimeRequirementInChunk: HookMap], any>>; - additionalModuleRuntimeRequirements: SyncHook<[Module, Set]>; - runtimeRequirementInModule: HookMap< - SyncBailHook<[Module, Set], any> + additionalChunkRuntimeRequirements: SyncHook< + [Chunk, Set, RuntimeRequirementsContext] + >; + runtimeRequirementInChunk: HookMap< + SyncBailHook<[Chunk, Set, RuntimeRequirementsContext], any> + >; + additionalModuleRuntimeRequirements: SyncHook< + [Module, Set, RuntimeRequirementsContext] + >; + runtimeRequirementInModule: HookMap< + SyncBailHook<[Module, Set, RuntimeRequirementsContext], any> + >; + additionalTreeRuntimeRequirements: SyncHook< + [Chunk, Set, RuntimeRequirementsContext] + >; + runtimeRequirementInTree: HookMap< + SyncBailHook<[Chunk, Set, RuntimeRequirementsContext], any> >; - additionalTreeRuntimeRequirements: SyncHook<[Chunk, Set]>; - runtimeRequirementInTree: HookMap], any>>; runtimeModule: SyncHook<[RuntimeModule, Chunk]>; reviveModules: SyncHook<[Iterable, any]>; beforeModuleIds: SyncHook<[Iterable]>; @@ -1376,7 +1392,7 @@ declare class Compilation { runtimeTemplate: RuntimeTemplate; moduleTemplates: { javascript: ModuleTemplate }; moduleGraph: ModuleGraph; - chunkGraph?: ChunkGraph; + chunkGraph: any; codeGenerationResults: CodeGenerationResults; processDependenciesQueue: AsyncQueue; addModuleQueue: AsyncQueue; @@ -1415,6 +1431,7 @@ declare class Compilation { needAdditionalPass: boolean; builtModules: WeakSet; codeGeneratedModules: WeakSet; + buildTimeExecutedModules: WeakSet; emittedAssets: Set; comparedForEmitAssets: Set; fileDependencies: LazySet; @@ -1512,8 +1529,33 @@ declare class Compilation { blocks: DependenciesBlock[] ): void; codeGeneration(callback?: any): void; - processRuntimeRequirements(): void; - addRuntimeModule(chunk: Chunk, module: RuntimeModule): void; + processRuntimeRequirements(__0?: { + /** + * the chunk graph + */ + chunkGraph?: ChunkGraph; + /** + * modules + */ + modules?: Iterable; + /** + * chunks + */ + chunks?: Iterable; + /** + * codeGenerationResults + */ + codeGenerationResults?: CodeGenerationResults; + /** + * chunkGraphEntries + */ + chunkGraphEntries?: Iterable; + }): void; + addRuntimeModule( + chunk: Chunk, + module: RuntimeModule, + chunkGraph?: ChunkGraph + ): void; addChunkInGroup( groupOptions: string | ChunkGroupOptions, module: Module, @@ -1601,6 +1643,11 @@ declare class Compilation { | WebpackPluginInstance )[] ): Compiler; + runModule( + module: Module, + options: RunModuleOptions, + callback: (err?: WebpackError, exports?: any) => void + ): void; checkConstraints(): void; /** @@ -5348,6 +5395,7 @@ declare interface KnownStatsModule { cacheable?: boolean; built?: boolean; codeGenerated?: boolean; + buildTimeExecuted?: boolean; cached?: boolean; optional?: boolean; orphan?: boolean; @@ -5484,6 +5532,7 @@ declare class LibManifestPlugin { } declare interface LibraryContext { compilation: Compilation; + chunkGraph: ChunkGraph; options: T; } @@ -9253,6 +9302,19 @@ type RuleSetUseItem = options?: string | { [index: string]: any }; } | __TypeWebpackOptions; +declare interface RunModuleArgument { + module: Module; + moduleObject: object; + codeGenerationResult: CodeGenerationResult; +} +declare interface RunModuleContext { + chunk: Chunk; + chunkGraph: ChunkGraph; + __webpack_require__: Function; +} +declare interface RunModuleOptions { + entryOptions?: EntryOptions; +} declare class RuntimeChunkPlugin { constructor(options?: any); options: any; @@ -9294,6 +9356,17 @@ declare class RuntimeModule extends Module { */ static STAGE_TRIGGER: number; } +declare interface RuntimeRequirementsContext { + /** + * the chunk graph + */ + chunkGraph: ChunkGraph; + + /** + * the code generation results + */ + codeGenerationResults: CodeGenerationResults; +} type RuntimeSpec = undefined | string | SortableSet; declare abstract class RuntimeSpecMap { get(runtime: RuntimeSpec): T;