webpack/lib/optimize/ModuleConcatenationPlugin.js

625 lines
19 KiB
JavaScript
Raw Normal View History

2017-05-10 19:15:14 +08:00
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
2018-07-30 23:08:51 +08:00
2017-05-10 19:15:14 +08:00
"use strict";
2019-12-11 05:58:26 +08:00
const asyncLib = require("neo-async");
const ChunkGraph = require("../ChunkGraph");
const ModuleGraph = require("../ModuleGraph");
2019-12-11 05:58:26 +08:00
const ModuleRestoreError = require("../ModuleRestoreError");
const ModuleStoreError = require("../ModuleStoreError");
const { STAGE_DEFAULT } = require("../OptimizationStages");
2018-07-30 23:08:51 +08:00
const HarmonyCompatibilityDependency = require("../dependencies/HarmonyCompatibilityDependency");
2017-05-10 19:15:14 +08:00
const HarmonyImportDependency = require("../dependencies/HarmonyImportDependency");
const ModuleHotAcceptDependency = require("../dependencies/ModuleHotAcceptDependency");
const ModuleHotDeclineDependency = require("../dependencies/ModuleHotDeclineDependency");
const StackedMap = require("../util/StackedMap");
2018-07-30 23:08:51 +08:00
const ConcatenatedModule = require("./ConcatenatedModule");
2017-05-10 19:15:14 +08:00
/** @typedef {import("../Compilation")} Compilation */
/** @typedef {import("../Compiler")} Compiler */
/** @typedef {import("../Module")} Module */
2017-11-08 18:32:05 +08:00
const formatBailoutReason = msg => {
return "ModuleConcatenation bailout: " + msg;
2017-11-08 18:32:05 +08:00
};
2017-05-10 19:15:14 +08:00
class ModuleConcatenationPlugin {
constructor(options) {
2018-02-25 09:00:20 +08:00
if (typeof options !== "object") options = {};
2017-05-10 19:15:14 +08:00
this.options = options;
}
/**
2020-04-23 16:48:36 +08:00
* Apply the plugin
* @param {Compiler} compiler the compiler instance
* @returns {void}
*/
2017-05-10 19:15:14 +08:00
apply(compiler) {
2018-02-25 09:00:20 +08:00
compiler.hooks.compilation.tap(
"ModuleConcatenationPlugin",
(compilation, { normalModuleFactory }) => {
const moduleGraph = compilation.moduleGraph;
2018-02-25 09:00:20 +08:00
const bailoutReasonMap = new Map();
const setBailoutReason = (module, reason) => {
setInnerBailoutReason(module, reason);
moduleGraph
.getOptimizationBailout(module)
.push(
typeof reason === "function"
? rs => formatBailoutReason(reason(rs))
: formatBailoutReason(reason)
);
2018-02-25 09:00:20 +08:00
};
const setInnerBailoutReason = (module, reason) => {
bailoutReasonMap.set(module, reason);
};
const getInnerBailoutReason = (module, requestShortener) => {
2018-02-25 09:00:20 +08:00
const reason = bailoutReasonMap.get(module);
if (typeof reason === "function") return reason(requestShortener);
return reason;
};
const formatBailoutWarning = (module, problem) => requestShortener => {
const reason = getInnerBailoutReason(module, requestShortener);
const reasonWithPrefix = reason ? ` (<- ${reason})` : "";
if (module === problem) {
return formatBailoutReason(
`Cannot concat with ${module.readableIdentifier(
requestShortener
)}${reasonWithPrefix}`
);
} else {
return formatBailoutReason(
`Cannot concat with ${module.readableIdentifier(
requestShortener
)} because of ${problem.readableIdentifier(
requestShortener
)}${reasonWithPrefix}`
);
}
};
2019-12-11 05:58:26 +08:00
compilation.hooks.optimizeChunkModules.tapAsync(
2018-12-09 19:54:17 +08:00
{
name: "ModuleConcatenationPlugin",
stage: STAGE_DEFAULT
2018-12-09 19:54:17 +08:00
},
(allChunks, modules, callback) => {
const logger = compilation.getLogger("ModuleConcatenationPlugin");
const chunkGraph = compilation.chunkGraph;
2018-02-25 09:00:20 +08:00
const relevantModules = [];
const possibleInners = new Set();
logger.time("select relevant modules");
2018-02-25 09:00:20 +08:00
for (const module of modules) {
// Only harmony modules are valid for optimization
if (
!module.buildMeta ||
module.buildMeta.exportsType !== "namespace" ||
module.presentationalDependencies === undefined ||
!module.presentationalDependencies.some(
2018-02-25 09:00:20 +08:00
d => d instanceof HarmonyCompatibilityDependency
)
) {
setBailoutReason(module, "Module is not an ECMAScript module");
continue;
}
2018-02-25 09:00:20 +08:00
// Some expressions are not compatible with module concatenation
// because they may produce unexpected results. The plugin bails out
// if some were detected upfront.
if (
2019-12-18 21:36:19 +08:00
module.buildInfo &&
module.buildInfo.moduleConcatenationBailout
2018-02-25 09:00:20 +08:00
) {
setBailoutReason(
module,
2019-12-18 21:36:19 +08:00
`Module uses ${module.buildInfo.moduleConcatenationBailout}`
2018-02-25 09:00:20 +08:00
);
continue;
}
2017-05-10 19:15:14 +08:00
2019-06-05 17:15:25 +08:00
// Must not be an async module
if (moduleGraph.isAsync(module)) {
setBailoutReason(module, `Module is async`);
continue;
}
2018-02-25 09:00:20 +08:00
// Exports must be known (and not dynamic)
const exportsInfo = moduleGraph.getExportsInfo(module);
if (!exportsInfo.hasStaticExportsList()) {
2018-02-25 09:00:20 +08:00
setBailoutReason(module, "Module exports are unknown");
continue;
}
2017-08-07 19:56:50 +08:00
2018-02-25 09:00:20 +08:00
// Hot Module Replacement need it's own module to work correctly
if (
module.dependencies.some(
dep =>
dep instanceof ModuleHotAcceptDependency ||
dep instanceof ModuleHotDeclineDependency
)
) {
setBailoutReason(module, "Module uses Hot Module Replacement");
continue;
}
// Module must be in any chunk (we don't want to do useless work)
if (chunkGraph.getNumberOfModuleChunks(module) === 0) {
setBailoutReason(module, "Module is not in any chunk");
continue;
}
2018-02-25 09:00:20 +08:00
relevantModules.push(module);
2018-02-25 09:00:20 +08:00
// Module must not be the entry points
if (chunkGraph.isEntryModule(module)) {
setInnerBailoutReason(module, "Module is an entry point");
2018-02-25 09:00:20 +08:00
continue;
}
2017-05-10 19:15:14 +08:00
const incomingConnections = Array.from(
moduleGraph.getIncomingConnections(module)
).filter(connection => connection.active);
2018-02-25 09:00:20 +08:00
// Module must only be used by Harmony Imports
const nonHarmonyConnections = incomingConnections.filter(
connection =>
!connection.dependency ||
!(connection.dependency instanceof HarmonyImportDependency)
);
if (nonHarmonyConnections.length > 0) {
setInnerBailoutReason(module, requestShortener => {
const importingModules = new Set(
nonHarmonyConnections
.map(c => c.originModule)
.filter(Boolean)
);
const importingExplanations = new Set(
nonHarmonyConnections
.map(c => c.explanation)
.filter(Boolean)
);
const importingModuleTypes = new Map(
Array.from(importingModules).map(
2020-03-29 06:10:15 +08:00
m =>
/** @type {[Module, Set<string>]} */ ([
m,
new Set(
nonHarmonyConnections
.filter(c => c.originModule === m)
.map(c => c.dependency.type)
.sort()
)
])
)
);
2018-02-25 09:00:20 +08:00
const names = Array.from(importingModules)
.map(
m =>
`${m.readableIdentifier(
requestShortener
)} (referenced with ${Array.from(
importingModuleTypes.get(m)
).join(", ")})`
)
.sort();
const explanations = Array.from(importingExplanations).sort();
if (names.length > 0 && explanations.length === 0) {
2018-02-25 09:00:20 +08:00
return `Module is referenced from these modules with unsupported syntax: ${names.join(
", "
)}`;
} else if (names.length === 0 && explanations.length > 0) {
2018-02-25 09:00:20 +08:00
return `Module is referenced by: ${explanations.join(
", "
)}`;
} else if (names.length > 0 && explanations.length > 0) {
2018-02-25 09:00:20 +08:00
return `Module is referenced from these modules with unsupported syntax: ${names.join(
", "
)} and by: ${explanations.join(", ")}`;
} else {
return "Module is referenced in a unsupported way";
}
2018-02-25 09:00:20 +08:00
});
continue;
}
2017-05-10 19:15:14 +08:00
// Module must be in the same chunks like the referencing module
const otherChunkConnections = incomingConnections.filter(
connection => {
return (
connection.originModule &&
!chunkGraph.haveModulesEqualChunks(
connection.originModule,
module
)
);
}
);
if (otherChunkConnections.length > 0) {
setInnerBailoutReason(module, requestShortener => {
const importingModules = new Set(
otherChunkConnections
.map(c => c.originModule)
.filter(Boolean)
);
const names = Array.from(importingModules)
.map(m => m.readableIdentifier(requestShortener))
.sort();
return `Module is referenced from different chunks by these modules: ${names.join(
", "
)}`;
});
continue;
}
2018-02-25 09:00:20 +08:00
possibleInners.add(module);
}
logger.timeEnd("select relevant modules");
logger.debug(
`${relevantModules.length} potential root modules, ${possibleInners.size} potential inner modules`
);
2018-02-25 09:00:20 +08:00
// sort by depth
// modules with lower depth are more likely suited as roots
// this improves performance, because modules already selected as inner are skipped
logger.time("sort relevant modules");
2018-02-25 09:00:20 +08:00
relevantModules.sort((a, b) => {
return moduleGraph.getDepth(a) - moduleGraph.getDepth(b);
});
logger.timeEnd("sort relevant modules");
logger.time("find modules to concatenate");
2018-02-25 09:00:20 +08:00
const concatConfigurations = [];
const usedAsInner = new Set();
for (const currentRoot of relevantModules) {
// when used by another configuration as inner:
// the other configuration is better and we can skip this one
if (usedAsInner.has(currentRoot)) continue;
// create a configuration with the root
const currentConfiguration = new ConcatConfiguration(currentRoot);
// cache failures to add modules
const failureCache = new Map();
2020-03-13 00:51:26 +08:00
// potential optional import candidates
/** @type {Set<Module>} */
const candidates = new Set();
2018-02-25 09:00:20 +08:00
// try to add all imports
for (const imp of this._getImports(compilation, currentRoot)) {
candidates.add(imp);
}
for (const imp of candidates) {
// _tryToAdd modifies the config even if it fails
// so make sure to only accept changes when it succeed
const backup = currentConfiguration.snapshot();
2020-03-13 00:51:26 +08:00
const impCandidates = new Set();
const problem = this._tryToAdd(
compilation,
2018-02-25 09:00:20 +08:00
currentConfiguration,
imp,
possibleInners,
2020-03-13 00:51:26 +08:00
impCandidates,
2018-02-25 09:00:20 +08:00
failureCache
);
if (problem) {
failureCache.set(imp, problem);
currentConfiguration.addWarning(imp, problem);
// roll back
currentConfiguration.rollback(backup);
} else {
2020-03-13 00:51:26 +08:00
for (const c of impCandidates) {
candidates.add(c);
}
2018-02-25 09:00:20 +08:00
}
}
if (!currentConfiguration.isEmpty()) {
concatConfigurations.push(currentConfiguration);
for (const module of currentConfiguration.getModules()) {
if (module !== currentConfiguration.rootModule) {
2018-02-25 09:00:20 +08:00
usedAsInner.add(module);
}
2018-02-25 09:00:20 +08:00
}
} else {
const optimizationBailouts = moduleGraph.getOptimizationBailout(
currentRoot
);
for (const warning of currentConfiguration.getWarningsSorted()) {
optimizationBailouts.push(
formatBailoutWarning(warning[0], warning[1])
);
}
2018-02-25 09:00:20 +08:00
}
2018-01-22 20:52:43 +08:00
}
logger.timeEnd("find modules to concatenate");
logger.debug(
`${concatConfigurations.length} concat configurations`
);
2018-02-25 09:00:20 +08:00
// HACK: Sort configurations by length and start with the longest one
2020-03-13 00:51:26 +08:00
// to get the biggest groups possible. Used modules are marked with usedModules
2018-02-25 09:00:20 +08:00
// TODO: Allow to reuse existing configuration while trying to add dependencies.
// This would improve performance. O(n^2) -> O(n)
logger.time(`sort concat configurations`);
2018-02-25 09:00:20 +08:00
concatConfigurations.sort((a, b) => {
return b.modules.size - a.modules.size;
});
logger.timeEnd(`sort concat configurations`);
2018-02-25 09:00:20 +08:00
const usedModules = new Set();
2019-12-11 05:58:26 +08:00
logger.time("create concatenated modules");
2019-12-11 05:58:26 +08:00
asyncLib.each(
concatConfigurations,
(concatConfiguration, callback) => {
2019-12-11 05:58:26 +08:00
const rootModule = concatConfiguration.rootModule;
// Avoid overlapping configurations
// TODO: remove this when todo above is fixed
if (usedModules.has(rootModule)) return callback();
const modules = concatConfiguration.getModules();
for (const m of modules) {
usedModules.add(m);
2018-08-22 21:33:16 +08:00
}
// Create a new ConcatenatedModule
let newModule = ConcatenatedModule.createFromModuleGraph(
rootModule,
modules,
moduleGraph,
compiler.root
);
// get name for caching
2019-12-11 05:58:26 +08:00
const identifier = newModule.identifier();
const cacheName = `${compilation.compilerPath}/ModuleConcatenationPlugin/${identifier}`;
const restore = () => {
compilation.cache.get(cacheName, null, (err, cacheModule) => {
if (err) {
return callback(new ModuleRestoreError(newModule, err));
}
if (cacheModule) {
cacheModule.updateCacheModule(newModule);
newModule = cacheModule;
}
2019-12-11 05:58:26 +08:00
build();
});
};
const build = () => {
newModule.build(
compiler.options,
compilation,
null,
null,
err => {
if (err) {
if (!err.module) {
err.module = newModule;
}
return callback(err);
}
integrateAndStore();
}
);
};
const integrateAndStore = () => {
2019-12-11 05:58:26 +08:00
ChunkGraph.setChunkGraphForModule(newModule, chunkGraph);
ModuleGraph.setModuleGraphForModule(newModule, moduleGraph);
for (const warning of concatConfiguration.getWarningsSorted()) {
moduleGraph
.getOptimizationBailout(newModule)
.push(formatBailoutWarning(warning[0], warning[1]));
}
moduleGraph.cloneModuleAttributes(rootModule, newModule);
for (const m of modules) {
compilation.modules.delete(m);
2019-12-11 05:58:26 +08:00
// add to builtModules when one of the included modules was built
if (compilation.builtModules.has(m)) {
compilation.builtModules.add(newModule);
}
// remove module from chunk
chunkGraph.replaceModule(m, newModule);
// replace module references with the concatenated module
moduleGraph.moveModuleConnections(m, newModule, c => {
return !(
c.dependency instanceof HarmonyImportDependency &&
modules.has(c.originModule) &&
modules.has(c.module)
);
});
}
// add concatenated module to the compilation
compilation.modules.add(newModule);
// TODO check if module needs build to avoid caching it without change
2019-12-11 05:58:26 +08:00
compilation.cache.store(cacheName, null, newModule, err => {
if (err) {
return callback(new ModuleStoreError(newModule, err));
}
2019-12-11 05:58:26 +08:00
callback();
2019-12-11 05:58:26 +08:00
});
};
restore();
2019-12-11 05:58:26 +08:00
},
err => {
logger.timeEnd("create concatenated modules");
process.nextTick(() => callback(err));
2018-02-25 09:00:20 +08:00
}
2019-12-11 05:58:26 +08:00
);
2018-01-22 20:52:43 +08:00
}
2018-02-25 09:00:20 +08:00
);
}
);
2017-05-10 19:15:14 +08:00
}
/**
* @param {Compilation} compilation the compilation
* @param {Module} module the module to be added
* @returns {Set<Module>} the imported modules
*/
_getImports(compilation, module) {
const moduleGraph = compilation.moduleGraph;
const set = new Set();
for (const dep of module.dependencies) {
// Get reference info only for harmony Dependencies
if (!(dep instanceof HarmonyImportDependency)) continue;
const connection = moduleGraph.getConnection(dep);
// Reference is valid and has a module
if (!connection || !connection.module || !connection.active) continue;
const importedNames = compilation.getDependencyReferencedExports(dep);
if (
2020-06-10 19:31:01 +08:00
importedNames.every(i =>
Array.isArray(i) ? i.length > 0 : i.name.length > 0
) ||
Array.isArray(moduleGraph.getProvidedExports(module))
) {
set.add(connection.module);
}
}
return set;
2017-05-10 19:15:14 +08:00
}
/**
* @param {Compilation} compilation webpack compilation
* @param {ConcatConfiguration} config concat configuration (will be modified when added)
* @param {Module} module the module to be added
* @param {Set<Module>} possibleModules modules that are candidates
* @param {Set<Module>} candidates list of potential candidates (will be added to)
* @param {Map<Module, Module>} failureCache cache for problematic modules to be more performant
* @returns {Module} the problematic module
*/
_tryToAdd(
compilation,
config,
module,
possibleModules,
candidates,
failureCache
) {
const cacheEntry = failureCache.get(module);
2018-02-25 09:00:20 +08:00
if (cacheEntry) {
return cacheEntry;
}
2017-05-10 19:15:14 +08:00
// Already added?
2018-02-25 09:00:20 +08:00
if (config.has(module)) {
return null;
2017-05-10 19:15:14 +08:00
}
// Not possible to add?
2018-02-25 09:00:20 +08:00
if (!possibleModules.has(module)) {
failureCache.set(module, module); // cache failures for performance
return module;
}
2017-05-10 19:15:14 +08:00
// Add the module
config.add(module);
2017-05-10 19:15:14 +08:00
const moduleGraph = compilation.moduleGraph;
2017-05-10 19:15:14 +08:00
// Every module which depends on the added module must be in the configuration too.
for (const connection of moduleGraph.getIncomingConnections(module)) {
2017-08-08 15:40:17 +08:00
// Modules that are not used can be ignored
if (!connection.active) continue;
2018-02-25 09:00:20 +08:00
const problem = this._tryToAdd(
compilation,
config,
connection.originModule,
2018-02-25 09:00:20 +08:00
possibleModules,
candidates,
2018-02-25 09:00:20 +08:00
failureCache
);
if (problem) {
failureCache.set(module, problem); // cache failures for performance
return problem;
2017-05-10 19:15:14 +08:00
}
}
2020-03-13 00:51:26 +08:00
// Add imports to possible candidates list
for (const imp of this._getImports(compilation, module)) {
candidates.add(imp);
}
return null;
2017-05-10 19:15:14 +08:00
}
}
class ConcatConfiguration {
/**
*
* @param {Module} rootModule the root module
*/
constructor(rootModule) {
2017-05-10 19:15:14 +08:00
this.rootModule = rootModule;
/** @type {StackedMap<Module, true>} */
this.modules = new StackedMap();
this.modules.set(rootModule, true);
/** @type {StackedMap<Module, Module>} */
this.warnings = new StackedMap();
2017-05-10 19:15:14 +08:00
}
add(module) {
this.modules.set(module, true);
2017-05-10 19:15:14 +08:00
}
has(module) {
return this.modules.has(module);
}
isEmpty() {
return this.modules.size === 1;
}
addWarning(module, problem) {
this.warnings.set(module, problem);
}
getWarningsSorted() {
2018-02-25 09:00:20 +08:00
return new Map(
this.warnings.asPairArray().sort((a, b) => {
const ai = a[0].identifier();
const bi = b[0].identifier();
if (ai < bi) return -1;
if (ai > bi) return 1;
return 0;
})
);
2017-06-15 05:20:30 +08:00
}
/**
* @returns {Set<Module>} modules as set
*/
getModules() {
return this.modules.asSet();
}
snapshot() {
const base = this.modules;
this.modules = this.modules.createChild();
return base;
2017-05-10 19:15:14 +08:00
}
rollback(snapshot) {
this.modules = snapshot;
2017-05-10 19:15:14 +08:00
}
}
module.exports = ModuleConcatenationPlugin;