2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
8 const asyncLib = require("neo-async");
9 const ChunkGraph = require("../ChunkGraph");
10 const ModuleGraph = require("../ModuleGraph");
11 const { STAGE_DEFAULT } = require("../OptimizationStages");
12 const HarmonyImportDependency = require("../dependencies/HarmonyImportDependency");
13 const { compareModulesByIdentifier } = require("../util/comparators");
20 } = require("../util/runtime");
21 const ConcatenatedModule = require("./ConcatenatedModule");
23 /** @typedef {import("../Compilation")} Compilation */
24 /** @typedef {import("../Compiler")} Compiler */
25 /** @typedef {import("../Module")} Module */
26 /** @typedef {import("../RequestShortener")} RequestShortener */
27 /** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */
30 * @typedef {Object} Statistics
31 * @property {number} cached
32 * @property {number} alreadyInConfig
33 * @property {number} invalidModule
34 * @property {number} incorrectChunks
35 * @property {number} incorrectDependency
36 * @property {number} incorrectModuleDependency
37 * @property {number} incorrectChunksOfImporter
38 * @property {number} incorrectRuntimeCondition
39 * @property {number} importerFailed
40 * @property {number} added
43 const formatBailoutReason = msg => {
44 return "ModuleConcatenation bailout: " + msg;
47 class ModuleConcatenationPlugin {
48 constructor(options) {
49 if (typeof options !== "object") options = {};
50 this.options = options;
55 * @param {Compiler} compiler the compiler instance
59 const { _backCompat: backCompat } = compiler;
60 compiler.hooks.compilation.tap("ModuleConcatenationPlugin", compilation => {
61 if (compilation.moduleMemCaches) {
63 "optimization.concatenateModules can't be used with cacheUnaffected as module concatenation is a global effect"
66 const moduleGraph = compilation.moduleGraph;
67 const bailoutReasonMap = new Map();
69 const setBailoutReason = (module, reason) => {
70 setInnerBailoutReason(module, reason);
72 .getOptimizationBailout(module)
74 typeof reason === "function"
75 ? rs => formatBailoutReason(reason(rs))
76 : formatBailoutReason(reason)
80 const setInnerBailoutReason = (module, reason) => {
81 bailoutReasonMap.set(module, reason);
84 const getInnerBailoutReason = (module, requestShortener) => {
85 const reason = bailoutReasonMap.get(module);
86 if (typeof reason === "function") return reason(requestShortener);
90 const formatBailoutWarning = (module, problem) => requestShortener => {
91 if (typeof problem === "function") {
92 return formatBailoutReason(
93 `Cannot concat with ${module.readableIdentifier(
95 )}: ${problem(requestShortener)}`
98 const reason = getInnerBailoutReason(module, requestShortener);
99 const reasonWithPrefix = reason ? `: ${reason}` : "";
100 if (module === problem) {
101 return formatBailoutReason(
102 `Cannot concat with ${module.readableIdentifier(
104 )}${reasonWithPrefix}`
107 return formatBailoutReason(
108 `Cannot concat with ${module.readableIdentifier(
110 )} because of ${problem.readableIdentifier(
112 )}${reasonWithPrefix}`
117 compilation.hooks.optimizeChunkModules.tapAsync(
119 name: "ModuleConcatenationPlugin",
122 (allChunks, modules, callback) => {
123 const logger = compilation.getLogger(
124 "webpack.ModuleConcatenationPlugin"
126 const { chunkGraph, moduleGraph } = compilation;
127 const relevantModules = [];
128 const possibleInners = new Set();
133 logger.time("select relevant modules");
134 for (const module of modules) {
135 let canBeRoot = true;
136 let canBeInner = true;
138 const bailoutReason = module.getConcatenationBailoutReason(context);
140 setBailoutReason(module, bailoutReason);
144 // Must not be an async module
145 if (moduleGraph.isAsync(module)) {
146 setBailoutReason(module, `Module is async`);
150 // Must be in strict mode
151 if (!module.buildInfo.strict) {
152 setBailoutReason(module, `Module is not in strict mode`);
156 // Module must be in any chunk (we don't want to do useless work)
157 if (chunkGraph.getNumberOfModuleChunks(module) === 0) {
158 setBailoutReason(module, "Module is not in any chunk");
162 // Exports must be known (and not dynamic)
163 const exportsInfo = moduleGraph.getExportsInfo(module);
164 const relevantExports = exportsInfo.getRelevantExports(undefined);
165 const unknownReexports = relevantExports.filter(exportInfo => {
167 exportInfo.isReexport() && !exportInfo.getTarget(moduleGraph)
170 if (unknownReexports.length > 0) {
173 `Reexports in this module do not have a static target (${Array.from(
177 exportInfo.name || "other exports"
178 }: ${exportInfo.getUsedInfo()}`
184 // Root modules must have a static list of exports
185 const unknownProvidedExports = relevantExports.filter(
187 return exportInfo.provided !== true;
190 if (unknownProvidedExports.length > 0) {
193 `List of module exports is dynamic (${Array.from(
194 unknownProvidedExports,
197 exportInfo.name || "other exports"
198 }: ${exportInfo.getProvidedInfo()} and ${exportInfo.getUsedInfo()}`
204 // Module must not be an entry point
205 if (chunkGraph.isEntryModule(module)) {
206 setInnerBailoutReason(module, "Module is an entry point");
210 if (canBeRoot) relevantModules.push(module);
211 if (canBeInner) possibleInners.add(module);
213 logger.timeEnd("select relevant modules");
215 `${relevantModules.length} potential root modules, ${possibleInners.size} potential inner modules`
218 // modules with lower depth are more likely suited as roots
219 // this improves performance, because modules already selected as inner are skipped
220 logger.time("sort relevant modules");
221 relevantModules.sort((a, b) => {
222 return moduleGraph.getDepth(a) - moduleGraph.getDepth(b);
224 logger.timeEnd("sort relevant modules");
226 /** @type {Statistics} */
232 incorrectDependency: 0,
233 incorrectModuleDependency: 0,
234 incorrectChunksOfImporter: 0,
235 incorrectRuntimeCondition: 0,
239 let statsCandidates = 0;
240 let statsSizeSum = 0;
241 let statsEmptyConfigurations = 0;
243 logger.time("find modules to concatenate");
244 const concatConfigurations = [];
245 const usedAsInner = new Set();
246 for (const currentRoot of relevantModules) {
247 // when used by another configuration as inner:
248 // the other configuration is better and we can skip this one
249 // TODO reconsider that when it's only used in a different runtime
250 if (usedAsInner.has(currentRoot)) continue;
252 let chunkRuntime = undefined;
253 for (const r of chunkGraph.getModuleRuntimes(currentRoot)) {
254 chunkRuntime = mergeRuntimeOwned(chunkRuntime, r);
256 const exportsInfo = moduleGraph.getExportsInfo(currentRoot);
257 const filteredRuntime = filterRuntime(chunkRuntime, r =>
258 exportsInfo.isModuleUsed(r)
260 const activeRuntime =
261 filteredRuntime === true
263 : filteredRuntime === false
267 // create a configuration with the root
268 const currentConfiguration = new ConcatConfiguration(
273 // cache failures to add modules
274 const failureCache = new Map();
276 // potential optional import candidates
277 /** @type {Set<Module>} */
278 const candidates = new Set();
280 // try to add all imports
281 for (const imp of this._getImports(
289 for (const imp of candidates) {
290 const impCandidates = new Set();
291 const problem = this._tryToAdd(
293 currentConfiguration,
305 failureCache.set(imp, problem);
306 currentConfiguration.addWarning(imp, problem);
308 for (const c of impCandidates) {
313 statsCandidates += candidates.size;
314 if (!currentConfiguration.isEmpty()) {
315 const modules = currentConfiguration.getModules();
316 statsSizeSum += modules.size;
317 concatConfigurations.push(currentConfiguration);
318 for (const module of modules) {
319 if (module !== currentConfiguration.rootModule) {
320 usedAsInner.add(module);
324 statsEmptyConfigurations++;
325 const optimizationBailouts =
326 moduleGraph.getOptimizationBailout(currentRoot);
327 for (const warning of currentConfiguration.getWarningsSorted()) {
328 optimizationBailouts.push(
329 formatBailoutWarning(warning[0], warning[1])
334 logger.timeEnd("find modules to concatenate");
337 concatConfigurations.length
338 } successful concat configurations (avg size: ${
339 statsSizeSum / concatConfigurations.length
340 }), ${statsEmptyConfigurations} bailed out completely`
343 `${statsCandidates} candidates were considered for adding (${stats.cached} cached failure, ${stats.alreadyInConfig} already in config, ${stats.invalidModule} invalid module, ${stats.incorrectChunks} incorrect chunks, ${stats.incorrectDependency} incorrect dependency, ${stats.incorrectChunksOfImporter} incorrect chunks of importer, ${stats.incorrectModuleDependency} incorrect module dependency, ${stats.incorrectRuntimeCondition} incorrect runtime condition, ${stats.importerFailed} importer failed, ${stats.added} added)`
345 // HACK: Sort configurations by length and start with the longest one
346 // to get the biggest groups possible. Used modules are marked with usedModules
347 // TODO: Allow to reuse existing configuration while trying to add dependencies.
348 // This would improve performance. O(n^2) -> O(n)
349 logger.time(`sort concat configurations`);
350 concatConfigurations.sort((a, b) => {
351 return b.modules.size - a.modules.size;
353 logger.timeEnd(`sort concat configurations`);
354 const usedModules = new Set();
356 logger.time("create concatenated modules");
358 concatConfigurations,
359 (concatConfiguration, callback) => {
360 const rootModule = concatConfiguration.rootModule;
362 // Avoid overlapping configurations
363 // TODO: remove this when todo above is fixed
364 if (usedModules.has(rootModule)) return callback();
365 const modules = concatConfiguration.getModules();
366 for (const m of modules) {
370 // Create a new ConcatenatedModule
371 let newModule = ConcatenatedModule.create(
374 concatConfiguration.runtime,
376 compilation.outputOptions.hashFunction
379 const build = () => {
388 err.module = newModule;
390 return callback(err);
397 const integrate = () => {
399 ChunkGraph.setChunkGraphForModule(newModule, chunkGraph);
400 ModuleGraph.setModuleGraphForModule(newModule, moduleGraph);
403 for (const warning of concatConfiguration.getWarningsSorted()) {
405 .getOptimizationBailout(newModule)
406 .push(formatBailoutWarning(warning[0], warning[1]));
408 moduleGraph.cloneModuleAttributes(rootModule, newModule);
409 for (const m of modules) {
410 // add to builtModules when one of the included modules was built
411 if (compilation.builtModules.has(m)) {
412 compilation.builtModules.add(newModule);
414 if (m !== rootModule) {
415 // attach external references to the concatenated module too
416 moduleGraph.copyOutgoingModuleConnections(
421 c.originModule === m &&
423 c.dependency instanceof HarmonyImportDependency &&
424 modules.has(c.module)
429 // remove module from chunk
430 for (const chunk of chunkGraph.getModuleChunksIterable(
433 const sourceTypes = chunkGraph.getChunkModuleSourceTypes(
437 if (sourceTypes.size === 1) {
438 chunkGraph.disconnectChunkAndModule(chunk, m);
440 const newSourceTypes = new Set(sourceTypes);
441 newSourceTypes.delete("javascript");
442 chunkGraph.setChunkModuleSourceTypes(
451 compilation.modules.delete(rootModule);
452 ChunkGraph.clearChunkGraphForModule(rootModule);
453 ModuleGraph.clearModuleGraphForModule(rootModule);
455 // remove module from chunk
456 chunkGraph.replaceModule(rootModule, newModule);
457 // replace module references with the concatenated module
458 moduleGraph.moveModuleConnections(rootModule, newModule, c => {
460 c.module === rootModule ? c.originModule : c.module;
461 const innerConnection =
462 c.dependency instanceof HarmonyImportDependency &&
463 modules.has(otherModule);
464 return !innerConnection;
466 // add concatenated module to the compilation
467 compilation.modules.add(newModule);
475 logger.timeEnd("create concatenated modules");
476 process.nextTick(callback.bind(null, err));
485 * @param {Compilation} compilation the compilation
486 * @param {Module} module the module to be added
487 * @param {RuntimeSpec} runtime the runtime scope
488 * @returns {Set<Module>} the imported modules
490 _getImports(compilation, module, runtime) {
491 const moduleGraph = compilation.moduleGraph;
492 const set = new Set();
493 for (const dep of module.dependencies) {
494 // Get reference info only for harmony Dependencies
495 if (!(dep instanceof HarmonyImportDependency)) continue;
497 const connection = moduleGraph.getConnection(dep);
498 // Reference is valid and has a module
501 !connection.module ||
502 !connection.isTargetActive(runtime)
507 const importedNames = compilation.getDependencyReferencedExports(
513 importedNames.every(i =>
514 Array.isArray(i) ? i.length > 0 : i.name.length > 0
516 Array.isArray(moduleGraph.getProvidedExports(module))
518 set.add(connection.module);
525 * @param {Compilation} compilation webpack compilation
526 * @param {ConcatConfiguration} config concat configuration (will be modified when added)
527 * @param {Module} module the module to be added
528 * @param {RuntimeSpec} runtime the runtime scope of the generated code
529 * @param {RuntimeSpec} activeRuntime the runtime scope of the root module
530 * @param {Set<Module>} possibleModules modules that are candidates
531 * @param {Set<Module>} candidates list of potential candidates (will be added to)
532 * @param {Map<Module, Module | function(RequestShortener): string>} failureCache cache for problematic modules to be more performant
533 * @param {ChunkGraph} chunkGraph the chunk graph
534 * @param {boolean} avoidMutateOnFailure avoid mutating the config when adding fails
535 * @param {Statistics} statistics gathering metrics
536 * @returns {Module | function(RequestShortener): string} the problematic module
548 avoidMutateOnFailure,
551 const cacheEntry = failureCache.get(module);
558 if (config.has(module)) {
559 statistics.alreadyInConfig++;
563 // Not possible to add?
564 if (!possibleModules.has(module)) {
565 statistics.invalidModule++;
566 failureCache.set(module, module); // cache failures for performance
570 // Module must be in the correct chunks
571 const missingChunks = Array.from(
572 chunkGraph.getModuleChunksIterable(config.rootModule)
573 ).filter(chunk => !chunkGraph.isModuleInChunk(module, chunk));
574 if (missingChunks.length > 0) {
575 const problem = requestShortener => {
576 const missingChunksList = Array.from(
577 new Set(missingChunks.map(chunk => chunk.name || "unnamed chunk(s)"))
579 const chunks = Array.from(
581 Array.from(chunkGraph.getModuleChunksIterable(module)).map(
582 chunk => chunk.name || "unnamed chunk(s)"
586 return `Module ${module.readableIdentifier(
588 )} is not in the same chunk(s) (expected in chunk(s) ${missingChunksList.join(
590 )}, module is in chunk(s) ${chunks.join(", ")})`;
592 statistics.incorrectChunks++;
593 failureCache.set(module, problem); // cache failures for performance
597 const moduleGraph = compilation.moduleGraph;
599 const incomingConnections =
600 moduleGraph.getIncomingConnectionsByOriginModule(module);
602 const incomingConnectionsFromNonModules =
603 incomingConnections.get(null) || incomingConnections.get(undefined);
604 if (incomingConnectionsFromNonModules) {
605 const activeNonModulesConnections =
606 incomingConnectionsFromNonModules.filter(connection => {
607 // We are not interested in inactive connections
608 // or connections without dependency
609 return connection.isActive(runtime);
611 if (activeNonModulesConnections.length > 0) {
612 const problem = requestShortener => {
613 const importingExplanations = new Set(
614 activeNonModulesConnections.map(c => c.explanation).filter(Boolean)
616 const explanations = Array.from(importingExplanations).sort();
617 return `Module ${module.readableIdentifier(
620 explanations.length > 0
621 ? `by: ${explanations.join(", ")}`
622 : "in an unsupported way"
625 statistics.incorrectDependency++;
626 failureCache.set(module, problem); // cache failures for performance
631 /** @type {Map<Module, readonly ModuleGraph.ModuleGraphConnection[]>} */
632 const incomingConnectionsFromModules = new Map();
633 for (const [originModule, connections] of incomingConnections) {
635 // Ignore connection from orphan modules
636 if (chunkGraph.getNumberOfModuleChunks(originModule) === 0) continue;
638 // We don't care for connections from other runtimes
639 let originRuntime = undefined;
640 for (const r of chunkGraph.getModuleRuntimes(originModule)) {
641 originRuntime = mergeRuntimeOwned(originRuntime, r);
644 if (!intersectRuntime(runtime, originRuntime)) continue;
646 // We are not interested in inactive connections
647 const activeConnections = connections.filter(connection =>
648 connection.isActive(runtime)
650 if (activeConnections.length > 0)
651 incomingConnectionsFromModules.set(originModule, activeConnections);
655 const incomingModules = Array.from(incomingConnectionsFromModules.keys());
657 // Module must be in the same chunks like the referencing module
658 const otherChunkModules = incomingModules.filter(originModule => {
659 for (const chunk of chunkGraph.getModuleChunksIterable(
662 if (!chunkGraph.isModuleInChunk(originModule, chunk)) {
668 if (otherChunkModules.length > 0) {
669 const problem = requestShortener => {
670 const names = otherChunkModules
671 .map(m => m.readableIdentifier(requestShortener))
673 return `Module ${module.readableIdentifier(
675 )} is referenced from different chunks by these modules: ${names.join(
679 statistics.incorrectChunksOfImporter++;
680 failureCache.set(module, problem); // cache failures for performance
684 /** @type {Map<Module, readonly ModuleGraph.ModuleGraphConnection[]>} */
685 const nonHarmonyConnections = new Map();
686 for (const [originModule, connections] of incomingConnectionsFromModules) {
687 const selected = connections.filter(
689 !connection.dependency ||
690 !(connection.dependency instanceof HarmonyImportDependency)
692 if (selected.length > 0)
693 nonHarmonyConnections.set(originModule, connections);
695 if (nonHarmonyConnections.size > 0) {
696 const problem = requestShortener => {
697 const names = Array.from(nonHarmonyConnections)
698 .map(([originModule, connections]) => {
699 return `${originModule.readableIdentifier(
701 )} (referenced with ${Array.from(
704 .map(c => c.dependency && c.dependency.type)
712 return `Module ${module.readableIdentifier(
714 )} is referenced from these modules with unsupported syntax: ${names.join(
718 statistics.incorrectModuleDependency++;
719 failureCache.set(module, problem); // cache failures for performance
723 if (runtime !== undefined && typeof runtime !== "string") {
724 // Module must be consistently referenced in the same runtimes
725 /** @type {{ originModule: Module, runtimeCondition: RuntimeSpec }[]} */
726 const otherRuntimeConnections = [];
730 ] of incomingConnectionsFromModules) {
731 /** @type {false | RuntimeSpec} */
732 let currentRuntimeCondition = false;
733 for (const connection of connections) {
734 const runtimeCondition = filterRuntime(runtime, runtime => {
735 return connection.isTargetActive(runtime);
737 if (runtimeCondition === false) continue;
738 if (runtimeCondition === true) continue outer;
739 if (currentRuntimeCondition !== false) {
740 currentRuntimeCondition = mergeRuntime(
741 currentRuntimeCondition,
745 currentRuntimeCondition = runtimeCondition;
748 if (currentRuntimeCondition !== false) {
749 otherRuntimeConnections.push({
751 runtimeCondition: currentRuntimeCondition
755 if (otherRuntimeConnections.length > 0) {
756 const problem = requestShortener => {
757 return `Module ${module.readableIdentifier(
759 )} is runtime-dependent referenced by these modules: ${Array.from(
760 otherRuntimeConnections,
761 ({ originModule, runtimeCondition }) =>
762 `${originModule.readableIdentifier(
764 )} (expected runtime ${runtimeToString(
766 )}, module is only referenced in ${runtimeToString(
767 /** @type {RuntimeSpec} */ (runtimeCondition)
771 statistics.incorrectRuntimeCondition++;
772 failureCache.set(module, problem); // cache failures for performance
778 if (avoidMutateOnFailure) {
779 backup = config.snapshot();
785 incomingModules.sort(compareModulesByIdentifier);
787 // Every module which depends on the added module must be in the configuration too.
788 for (const originModule of incomingModules) {
789 const problem = this._tryToAdd(
803 if (backup !== undefined) config.rollback(backup);
804 statistics.importerFailed++;
805 failureCache.set(module, problem); // cache failures for performance
810 // Add imports to possible candidates list
811 for (const imp of this._getImports(compilation, module, runtime)) {
819 class ConcatConfiguration {
821 * @param {Module} rootModule the root module
822 * @param {RuntimeSpec} runtime the runtime
824 constructor(rootModule, runtime) {
825 this.rootModule = rootModule;
826 this.runtime = runtime;
827 /** @type {Set<Module>} */
828 this.modules = new Set();
829 this.modules.add(rootModule);
830 /** @type {Map<Module, Module | function(RequestShortener): string>} */
831 this.warnings = new Map();
835 this.modules.add(module);
839 return this.modules.has(module);
843 return this.modules.size === 1;
846 addWarning(module, problem) {
847 this.warnings.set(module, problem);
850 getWarningsSorted() {
852 Array.from(this.warnings).sort((a, b) => {
853 const ai = a[0].identifier();
854 const bi = b[0].identifier();
855 if (ai < bi) return -1;
856 if (ai > bi) return 1;
863 * @returns {Set<Module>} modules as set
870 return this.modules.size;
874 const modules = this.modules;
875 for (const m of modules) {
876 if (snapshot === 0) {
885 module.exports = ModuleConcatenationPlugin;