"use strict";
const HarmonyImportDependency = require("../dependencies/HarmonyImportDependency");
const ModuleHotAcceptDependency = require("../dependencies/ModuleHotAcceptDependency");
const ModuleHotDeclineDependency = require("../dependencies/ModuleHotDeclineDependency");
const ConcatenatedModule = require("./ConcatenatedModule");
const HarmonyCompatibilityDependency = require("../dependencies/HarmonyCompatibilityDependency");
const StackedSetMap = require("../util/StackedSetMap");
const formatBailoutReason = msg => {
return "ModuleConcatenation bailout: " + msg;
};
class ModuleConcatenationPlugin {
constructor(options) {
if (typeof options !== "object") options = {};
this.options = options;
}
apply(compiler) {
compiler.hooks.compilation.tap(
"ModuleConcatenationPlugin",
(compilation, { normalModuleFactory }) => {
const handler = (parser, parserOptions) => {
parser.hooks.call.for("eval").tap("ModuleConcatenationPlugin", () => {
parser.state.module.buildMeta.moduleConcatenationBailout = "eval()";
});
};
normalModuleFactory.hooks.parser
.for("javascript/auto")
.tap("ModuleConcatenationPlugin", handler);
normalModuleFactory.hooks.parser
.for("javascript/dynamic")
.tap("ModuleConcatenationPlugin", handler);
normalModuleFactory.hooks.parser
.for("javascript/esm")
.tap("ModuleConcatenationPlugin", handler);
const bailoutReasonMap = new Map();
const setBailoutReason = (module, reason) => {
bailoutReasonMap.set(module, reason);
module.optimizationBailout.push(
typeof reason === "function"
? rs => formatBailoutReason(reason(rs))
: formatBailoutReason(reason)
);
};
const getBailoutReason = (module, requestShortener) => {
const reason = bailoutReasonMap.get(module);
if (typeof reason === "function") return reason(requestShortener);
return reason;
};
compilation.hooks.optimizeChunkModules.tap(
"ModuleConcatenationPlugin",
(allChunks, modules) => {
const relevantModules = [];
const possibleInners = new Set();
for (const module of modules) {
if (
!module.buildMeta ||
module.buildMeta.exportsType !== "namespace" ||
!module.dependencies.some(
d => d instanceof HarmonyCompatibilityDependency
)
) {
setBailoutReason(module, "Module is not an ECMAScript module");
continue;
}
if (
module.buildMeta &&
module.buildMeta.moduleConcatenationBailout
) {
setBailoutReason(
module,
`Module uses ${module.buildMeta.moduleConcatenationBailout}`
);
continue;
}
if (!Array.isArray(module.buildMeta.providedExports)) {
setBailoutReason(module, "Module exports are unknown");
continue;
}
if (module.variables.length > 0) {
setBailoutReason(
module,
`Module uses injected variables (${module.variables
.map(v => v.name)
.join(", ")})`
);
continue;
}
if (
module.dependencies.some(
dep =>
dep instanceof ModuleHotAcceptDependency ||
dep instanceof ModuleHotDeclineDependency
)
) {
setBailoutReason(module, "Module uses Hot Module Replacement");
continue;
}
relevantModules.push(module);
if (module.isEntryModule()) {
setBailoutReason(module, "Module is an entry point");
continue;
}
if (module.getNumberOfChunks() === 0) {
setBailoutReason(module, "Module is not in any chunk");
continue;
}
const nonHarmonyReasons = module.reasons.filter(
reason =>
!reason.dependency ||
!(reason.dependency instanceof HarmonyImportDependency)
);
if (nonHarmonyReasons.length > 0) {
const importingModules = new Set(
nonHarmonyReasons.map(r => r.module).filter(Boolean)
);
const importingExplanations = new Set(
nonHarmonyReasons.map(r => r.explanation).filter(Boolean)
);
const importingModuleTypes = new Map(
Array.from(importingModules).map(
m => ([
m,
new Set(
nonHarmonyReasons
.filter(r => r.module === m)
.map(r => r.dependency.type)
.sort()
)
])
)
);
setBailoutReason(module, requestShortener => {
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) {
return `Module is referenced from these modules with unsupported syntax: ${names.join(
", "
)}`;
} else if (names.length === 0 && explanations.length > 0) {
return `Module is referenced by: ${explanations.join(
", "
)}`;
} else if (names.length > 0 && explanations.length > 0) {
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";
}
});
continue;
}
possibleInners.add(module);
}
relevantModules.sort((a, b) => {
return a.depth - b.depth;
});
const concatConfigurations = [];
const usedAsInner = new Set();
for (const currentRoot of relevantModules) {
if (usedAsInner.has(currentRoot)) continue;
const currentConfiguration = new ConcatConfiguration(currentRoot);
const failureCache = new Map();
for (const imp of this._getImports(compilation, currentRoot)) {
const problem = this._tryToAdd(
compilation,
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.getModules()) {
if (module !== currentConfiguration.rootModule) {
usedAsInner.add(module);
}
}
}
}
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 modules = concatConfiguration.getModules();
const rootModule = concatConfiguration.rootModule;
const newModule = new ConcatenatedModule(
rootModule,
Array.from(modules),
ConcatenatedModule.createConcatenationList(
rootModule,
modules,
compilation
)
);
for (const warning of concatConfiguration.getWarningsSorted()) {
newModule.optimizationBailout.push(requestShortener => {
const reason = getBailoutReason(warning[0], requestShortener);
const reasonWithPrefix = reason ? ` (<- ${reason})` : "";
if (warning[0] === warning[1]) {
return formatBailoutReason(
`Cannot concat with ${warning[0].readableIdentifier(
requestShortener
)}${reasonWithPrefix}`
);
} else {
return formatBailoutReason(
`Cannot concat with ${warning[0].readableIdentifier(
requestShortener
)} because of ${warning[1].readableIdentifier(
requestShortener
)}${reasonWithPrefix}`
);
}
});
}
const chunks = concatConfiguration.rootModule.getChunks();
for (const m of modules) {
usedModules.add(m);
for (const chunk of chunks) {
chunk.removeModule(m);
}
}
for (const chunk of chunks) {
chunk.addModule(newModule);
newModule.addChunk(chunk);
}
for (const chunk of allChunks) {
if (chunk.entryModule === concatConfiguration.rootModule) {
chunk.entryModule = newModule;
}
}
compilation.modules.push(newModule);
for (const reason of newModule.reasons) {
if (reason.dependency.module === concatConfiguration.rootModule)
reason.dependency.module = newModule;
if (
reason.dependency.redirectedModule ===
concatConfiguration.rootModule
)
reason.dependency.redirectedModule = newModule;
}
for (let i = 0; i < newModule.dependencies.length; i++) {
let dep = newModule.dependencies[i];
if (dep.module) {
let reasons = dep.module.reasons;
for (let j = 0; j < reasons.length; j++) {
let reason = reasons[j];
if (reason.dependency === dep) {
reason.module = newModule;
}
}
}
}
}
compilation.modules = compilation.modules.filter(
m => !usedModules.has(m)
);
}
);
}
);
}
_getImports(compilation, module) {
return new Set(
module.dependencies
.map(dep => {
if (!(dep instanceof HarmonyImportDependency)) return null;
if (!compilation) return dep.getReference();
return compilation.getDependencyReference(module, dep);
})
.filter(
ref =>
ref &&
ref.module &&
(Array.isArray(ref.importedNames) ||
Array.isArray(ref.module.buildMeta.providedExports))
)
.map(ref => ref.module)
);
}
_tryToAdd(compilation, config, module, possibleModules, failureCache) {
const cacheEntry = failureCache.get(module);
if (cacheEntry) {
return cacheEntry;
}
if (config.has(module)) {
return null;
}
if (!possibleModules.has(module)) {
failureCache.set(module, module); return module;
}
if (!config.rootModule.hasEqualsChunks(module)) {
failureCache.set(module, module); return module;
}
const testConfig = config.clone();
testConfig.add(module);
for (const reason of module.reasons) {
if (
reason.module.factoryMeta.sideEffectFree &&
reason.module.used === false
)
continue;
const problem = this._tryToAdd(
compilation,
testConfig,
reason.module,
possibleModules,
failureCache
);
if (problem) {
failureCache.set(module, problem); return problem;
}
}
config.set(testConfig);
for (const imp of this._getImports(compilation, module)) {
const problem = this._tryToAdd(
compilation,
config,
imp,
possibleModules,
failureCache
);
if (problem) {
config.addWarning(imp, problem);
}
}
return null;
}
}
class ConcatConfiguration {
constructor(rootModule, cloneFrom) {
this.rootModule = rootModule;
if (cloneFrom) {
this.modules = cloneFrom.modules.createChild(5);
this.warnings = cloneFrom.warnings.createChild(5);
} else {
this.modules = new StackedSetMap();
this.modules.add(rootModule);
this.warnings = new StackedSetMap();
}
}
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);
}
getWarningsSorted() {
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;
})
);
}
getModules() {
return this.modules.asSet();
}
clone() {
return new ConcatConfiguration(this.rootModule, this);
}
set(config) {
this.rootModule = config.rootModule;
this.modules = config.modules;
this.warnings = config.warnings;
}
}
module.exports = ModuleConcatenationPlugin;