webpack/lib/optimize/ModuleConcatenationPlugin.js

294 lines
9.5 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
*/
"use strict";
const HarmonyImportDependency = require("../dependencies/HarmonyImportDependency");
const ConcatenatedModule = require("./ConcatenatedModule");
const HarmonyExportImportedSpecifierDependency = require("../dependencies/HarmonyExportImportedSpecifierDependency");
class ModuleConcatenationPlugin {
constructor(options) {
if(typeof options !== "object") options = {};
this.options = options;
}
apply(compiler) {
compiler.plugin("compilation", (compilation, params) => {
params.normalModuleFactory.plugin("parser", (parser, parserOptions) => {
parser.plugin("call eval", () => {
parser.state.module.meta.hasEval = true;
});
});
const bailoutReasonMap = new Map();
2017-05-29 05:31:58 +08:00
function setBailoutReason(module, prefix, reason) {
bailoutReasonMap.set(module, reason);
module.optimizationBailout.push(typeof reason === "function" ? (rs) => `${prefix}: ${reason(rs)}` : `${prefix}: ${reason}`);
}
2017-05-29 05:31:58 +08:00
function getBailoutReason(module, requestShortener) {
const reason = bailoutReasonMap.get(module);
if(typeof reason === "function") return reason(requestShortener);
return reason;
}
2017-05-29 05:31:58 +08:00
2017-05-10 19:15:14 +08:00
compilation.plugin("optimize-chunk-modules", (chunks, modules) => {
const relevantModules = [];
const possibleInners = new Set();
for(const module of modules) {
// Only harmony modules are valid for optimization
if(!module.meta || !module.meta.harmonyModule) {
continue;
}
// Because of variable renaming we can't use modules with eval
if(module.meta && module.meta.hasEval) {
setBailoutReason(module, "ModuleConcatenation", "eval is used in the module");
continue;
}
2017-05-10 19:15:14 +08:00
relevantModules.push(module);
// Module must not be the entry points
if(module.getChunks().some(chunk => chunk.entryModule === module)) {
setBailoutReason(module, "ModuleConcatenation (inner)", "module is an entrypoint");
continue;
}
2017-05-10 19:15:14 +08:00
// Exports must be known (and not dynamic)
if(!Array.isArray(module.providedExports)) {
setBailoutReason(module, "ModuleConcatenation (inner)", "exports are not known");
continue;
}
2017-05-10 19:15:14 +08:00
// Using dependency variables is not possible as this wraps the code in a function
if(module.variables.length > 0) {
setBailoutReason(module, "ModuleConcatenation (inner)", "dependency variables are used (i. e. ProvidePlugin)");
continue;
}
2017-05-10 19:15:14 +08:00
// Module must only be used by Harmony Imports
const nonHarmonyReasons = module.reasons.filter(reason => !(reason.dependency instanceof HarmonyImportDependency));
if(nonHarmonyReasons.length > 0) {
const importingModules = new Set(nonHarmonyReasons.map(r => r.module));
setBailoutReason(module, "ModuleConcatenation (inner)", (requestShortener) => {
const names = Array.from(importingModules).map(m => m.readableIdentifier(requestShortener));
return `module is used with non-harmony imports from ${names.join(", ")}`;
});
continue;
}
2017-05-10 19:15:14 +08:00
possibleInners.add(module);
}
// sort by depth
// modules with lower depth are more likly suited as roots
// this improves performance, because modules already selected as inner are skipped
relevantModules.sort((a, b) => {
return a.depth - b.depth;
});
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();
// try to add all imports
for(const imp of this.getImports(currentRoot)) {
const problem = this.tryToAdd(currentConfiguration, imp, possibleInners, failureCache);
if(problem) {
failureCache.set(imp, problem);
currentConfiguration.addWarning(imp, problem);
}
}
if(!currentConfiguration.isEmpty()) {
concatConfigurations.push(currentConfiguration);
for(const module of currentConfiguration.modules) {
if(module !== currentConfiguration.rootModule)
usedAsInner.add(module);
}
2017-05-10 19:15:14 +08:00
}
}
// HACK: Sort configurations by length and start with the longest one
// to get the biggers groups possible. Used modules are marked with usedModules
// TODO: Allow to reuse existing configuration while trying to add dependencies.
// This would improve performance. O(n^2) -> O(n)
concatConfigurations.sort((a, b) => {
return b.modules.size - a.modules.size;
});
const usedModules = new Set();
for(const concatConfiguration of concatConfigurations) {
if(usedModules.has(concatConfiguration.rootModule))
continue;
const orderedModules = new Set();
this.addInOrder(concatConfiguration.rootModule, concatConfiguration.modules, orderedModules);
const newModule = new ConcatenatedModule(concatConfiguration.rootModule, Array.from(orderedModules));
for(const warning of concatConfiguration.warnings) {
newModule.optimizationBailout.push((requestShortener) => {
const reason = getBailoutReason(warning[0], requestShortener);
const reasonPrefix = reason ? `: ${reason}` : "";
if(warning[0] === warning[1])
return `ModuleConcatenation: Cannot concat with ${warning[0].readableIdentifier(requestShortener)}${reasonPrefix}`;
else
return `ModuleConcatenation: Cannot concat with ${warning[0].readableIdentifier(requestShortener)} because of ${warning[1].readableIdentifier(requestShortener)}${reasonPrefix}`;
});
}
const chunks = concatConfiguration.rootModule.getChunks();
for(const m of orderedModules) {
usedModules.add(m);
chunks.forEach(chunk => chunk.removeModule(m));
}
chunks.forEach(chunk => {
2017-05-10 19:15:14 +08:00
chunk.addModule(newModule);
2017-05-21 13:14:10 +08:00
if(chunk.entryModule === concatConfiguration.rootModule)
2017-05-10 19:15:14 +08:00
chunk.entryModule = newModule;
});
compilation.modules.push(newModule);
newModule.reasons.forEach(reason => reason.dependency.module = newModule);
newModule.dependencies.forEach(dep => {
if(dep.module) {
dep.module.reasons.forEach(reason => {
if(reason.dependency === dep)
reason.module = newModule;
});
}
});
}
compilation.modules = compilation.modules.filter(m => !usedModules.has(m));
2017-05-10 19:15:14 +08:00
});
});
}
getImports(module) {
return Array.from(new Set(module.dependencies
// Only harmony Dependencies
.filter(dep => dep instanceof HarmonyImportDependency && dep.module)
// Dependencies are simple enough to concat them
.filter(dep => {
return !module.dependencies.some(d =>
d instanceof HarmonyExportImportedSpecifierDependency &&
d.importDependency === dep &&
!d.id &&
!Array.isArray(dep.module.providedExports)
);
})
// Take the imported module
.map(dep => dep.module)
));
}
tryToAdd(config, module, possibleModules, failureCache) {
const cacheEntry = failureCache.get(module);
if(cacheEntry) {
return cacheEntry;
}
2017-05-10 19:15:14 +08:00
// Already added?
if(config.has(module)) {
return null;
2017-05-10 19:15:14 +08:00
}
// Not possible to add?
if(!possibleModules.has(module)) {
failureCache.set(module, module); // cache failures for performance
return module;
}
// module must be in the same chunks
if(!config.rootModule.hasEqualsChunks(module)) {
failureCache.set(module, module); // cache failures for performance
return module;
2017-05-10 19:15:14 +08:00
}
// Clone config to make experimental changes
const testConfig = config.clone();
// Add the module
testConfig.add(module);
// Every module which depends on the added module must be in the configuration too.
for(const reason of module.reasons) {
const problem = this.tryToAdd(testConfig, reason.module, possibleModules, failureCache);
if(problem) {
failureCache.set(module, problem); // cache failures for performance
return problem;
2017-05-10 19:15:14 +08:00
}
}
// Eagerly try to add imports too if possible
for(const imp of this.getImports(module)) {
const problem = this.tryToAdd(testConfig, imp, possibleModules, failureCache);
if(problem) {
config.addWarning(module, problem);
}
}
2017-05-10 19:15:14 +08:00
// Commit experimental changes
config.set(testConfig);
return null;
2017-05-10 19:15:14 +08:00
}
addInOrder(module, unorderedSet, orderedSet) {
if(orderedSet.has(module)) return;
if(!unorderedSet.has(module)) return;
orderedSet.add(module);
for(const imp of this.getImports(module))
this.addInOrder(imp, unorderedSet, orderedSet);
orderedSet.delete(module);
orderedSet.add(module);
}
}
class ConcatConfiguration {
constructor(rootModule) {
this.rootModule = rootModule;
this.modules = new Set([rootModule]);
this.warnings = new Map();
2017-05-10 19:15:14 +08:00
}
add(module) {
this.modules.add(module);
}
has(module) {
return this.modules.has(module);
}
isEmpty() {
return this.modules.size === 1;
}
addWarning(module, problem) {
this.warnings.set(module, problem);
}
2017-05-10 19:15:14 +08:00
clone() {
const clone = new ConcatConfiguration(this.rootModule);
for(const module of this.modules)
clone.add(module);
for(const pair of this.warnings)
clone.addWarning(pair[0], pair[1]);
2017-05-10 19:15:14 +08:00
return clone;
}
set(config) {
this.rootModule = config.rootModule;
this.modules = new Set(config.modules);
this.warnings = new Map(config.warnings);
2017-05-10 19:15:14 +08:00
}
}
module.exports = ModuleConcatenationPlugin;