943 lines
28 KiB
JavaScript
943 lines
28 KiB
JavaScript
|
/*
|
||
|
MIT License http://www.opensource.org/licenses/mit-license.php
|
||
|
Author Tobias Koppers @sokra
|
||
|
*/
|
||
|
|
||
|
"use strict";
|
||
|
|
||
|
const asyncLib = require("neo-async");
|
||
|
const ChunkGraph = require("../ChunkGraph");
|
||
|
const ModuleGraph = require("../ModuleGraph");
|
||
|
const { STAGE_DEFAULT } = require("../OptimizationStages");
|
||
|
const HarmonyImportDependency = require("../dependencies/HarmonyImportDependency");
|
||
|
const { compareModulesByIdentifier } = require("../util/comparators");
|
||
|
const {
|
||
|
intersectRuntime,
|
||
|
mergeRuntimeOwned,
|
||
|
filterRuntime,
|
||
|
runtimeToString,
|
||
|
mergeRuntime
|
||
|
} = require("../util/runtime");
|
||
|
const ConcatenatedModule = require("./ConcatenatedModule");
|
||
|
|
||
|
/** @typedef {import("../Compilation")} Compilation */
|
||
|
/** @typedef {import("../Compiler")} Compiler */
|
||
|
/** @typedef {import("../Module")} Module */
|
||
|
/** @typedef {import("../RequestShortener")} RequestShortener */
|
||
|
/** @typedef {import("../util/runtime").RuntimeSpec} RuntimeSpec */
|
||
|
|
||
|
/**
|
||
|
* @typedef {Object} Statistics
|
||
|
* @property {number} cached
|
||
|
* @property {number} alreadyInConfig
|
||
|
* @property {number} invalidModule
|
||
|
* @property {number} incorrectChunks
|
||
|
* @property {number} incorrectDependency
|
||
|
* @property {number} incorrectModuleDependency
|
||
|
* @property {number} incorrectChunksOfImporter
|
||
|
* @property {number} incorrectRuntimeCondition
|
||
|
* @property {number} importerFailed
|
||
|
* @property {number} added
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @param {string} msg message
|
||
|
* @returns {string} formatted message
|
||
|
*/
|
||
|
const formatBailoutReason = msg => {
|
||
|
return "ModuleConcatenation bailout: " + msg;
|
||
|
};
|
||
|
|
||
|
class ModuleConcatenationPlugin {
|
||
|
constructor(options) {
|
||
|
if (typeof options !== "object") options = {};
|
||
|
this.options = options;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Apply the plugin
|
||
|
* @param {Compiler} compiler the compiler instance
|
||
|
* @returns {void}
|
||
|
*/
|
||
|
apply(compiler) {
|
||
|
const { _backCompat: backCompat } = compiler;
|
||
|
compiler.hooks.compilation.tap("ModuleConcatenationPlugin", compilation => {
|
||
|
if (compilation.moduleMemCaches) {
|
||
|
throw new Error(
|
||
|
"optimization.concatenateModules can't be used with cacheUnaffected as module concatenation is a global effect"
|
||
|
);
|
||
|
}
|
||
|
const moduleGraph = compilation.moduleGraph;
|
||
|
/** @type {Map<Module, string | ((requestShortener: RequestShortener) => string)>} */
|
||
|
const bailoutReasonMap = new Map();
|
||
|
|
||
|
/**
|
||
|
* @param {Module} module the module
|
||
|
* @param {string | ((requestShortener: RequestShortener) => string)} reason the reason
|
||
|
*/
|
||
|
const setBailoutReason = (module, reason) => {
|
||
|
setInnerBailoutReason(module, reason);
|
||
|
moduleGraph
|
||
|
.getOptimizationBailout(module)
|
||
|
.push(
|
||
|
typeof reason === "function"
|
||
|
? rs => formatBailoutReason(reason(rs))
|
||
|
: formatBailoutReason(reason)
|
||
|
);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* @param {Module} module the module
|
||
|
* @param {string | ((requestShortener: RequestShortener) => string)} reason the reason
|
||
|
*/
|
||
|
const setInnerBailoutReason = (module, reason) => {
|
||
|
bailoutReasonMap.set(module, reason);
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* @param {Module} module the module
|
||
|
* @param {RequestShortener} requestShortener the request shortener
|
||
|
* @returns {string | ((requestShortener: RequestShortener) => string) | undefined} the reason
|
||
|
*/
|
||
|
const getInnerBailoutReason = (module, requestShortener) => {
|
||
|
const reason = bailoutReasonMap.get(module);
|
||
|
if (typeof reason === "function") return reason(requestShortener);
|
||
|
return reason;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* @param {Module} module the module
|
||
|
* @param {Module | function(RequestShortener): string} problem the problem
|
||
|
* @returns {(requestShortener: RequestShortener) => string} the reason
|
||
|
*/
|
||
|
const formatBailoutWarning = (module, problem) => requestShortener => {
|
||
|
if (typeof problem === "function") {
|
||
|
return formatBailoutReason(
|
||
|
`Cannot concat with ${module.readableIdentifier(
|
||
|
requestShortener
|
||
|
)}: ${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}`
|
||
|
);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
compilation.hooks.optimizeChunkModules.tapAsync(
|
||
|
{
|
||
|
name: "ModuleConcatenationPlugin",
|
||
|
stage: STAGE_DEFAULT
|
||
|
},
|
||
|
(allChunks, modules, callback) => {
|
||
|
const logger = compilation.getLogger(
|
||
|
"webpack.ModuleConcatenationPlugin"
|
||
|
);
|
||
|
const { chunkGraph, moduleGraph } = compilation;
|
||
|
const relevantModules = [];
|
||
|
const possibleInners = new Set();
|
||
|
const context = {
|
||
|
chunkGraph,
|
||
|
moduleGraph
|
||
|
};
|
||
|
logger.time("select relevant modules");
|
||
|
for (const module of modules) {
|
||
|
let canBeRoot = true;
|
||
|
let canBeInner = true;
|
||
|
|
||
|
const bailoutReason = module.getConcatenationBailoutReason(context);
|
||
|
if (bailoutReason) {
|
||
|
setBailoutReason(module, bailoutReason);
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Must not be an async module
|
||
|
if (moduleGraph.isAsync(module)) {
|
||
|
setBailoutReason(module, `Module is async`);
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Must be in strict mode
|
||
|
if (!module.buildInfo.strict) {
|
||
|
setBailoutReason(module, `Module is not in strict mode`);
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
// Exports must be known (and not dynamic)
|
||
|
const exportsInfo = moduleGraph.getExportsInfo(module);
|
||
|
const relevantExports = exportsInfo.getRelevantExports(undefined);
|
||
|
const unknownReexports = relevantExports.filter(exportInfo => {
|
||
|
return (
|
||
|
exportInfo.isReexport() && !exportInfo.getTarget(moduleGraph)
|
||
|
);
|
||
|
});
|
||
|
if (unknownReexports.length > 0) {
|
||
|
setBailoutReason(
|
||
|
module,
|
||
|
`Reexports in this module do not have a static target (${Array.from(
|
||
|
unknownReexports,
|
||
|
exportInfo =>
|
||
|
`${
|
||
|
exportInfo.name || "other exports"
|
||
|
}: ${exportInfo.getUsedInfo()}`
|
||
|
).join(", ")})`
|
||
|
);
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Root modules must have a static list of exports
|
||
|
const unknownProvidedExports = relevantExports.filter(
|
||
|
exportInfo => {
|
||
|
return exportInfo.provided !== true;
|
||
|
}
|
||
|
);
|
||
|
if (unknownProvidedExports.length > 0) {
|
||
|
setBailoutReason(
|
||
|
module,
|
||
|
`List of module exports is dynamic (${Array.from(
|
||
|
unknownProvidedExports,
|
||
|
exportInfo =>
|
||
|
`${
|
||
|
exportInfo.name || "other exports"
|
||
|
}: ${exportInfo.getProvidedInfo()} and ${exportInfo.getUsedInfo()}`
|
||
|
).join(", ")})`
|
||
|
);
|
||
|
canBeRoot = false;
|
||
|
}
|
||
|
|
||
|
// Module must not be an entry point
|
||
|
if (chunkGraph.isEntryModule(module)) {
|
||
|
setInnerBailoutReason(module, "Module is an entry point");
|
||
|
canBeInner = false;
|
||
|
}
|
||
|
|
||
|
if (canBeRoot) relevantModules.push(module);
|
||
|
if (canBeInner) possibleInners.add(module);
|
||
|
}
|
||
|
logger.timeEnd("select relevant modules");
|
||
|
logger.debug(
|
||
|
`${relevantModules.length} potential root modules, ${possibleInners.size} potential inner modules`
|
||
|
);
|
||
|
// 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");
|
||
|
relevantModules.sort((a, b) => {
|
||
|
return moduleGraph.getDepth(a) - moduleGraph.getDepth(b);
|
||
|
});
|
||
|
logger.timeEnd("sort relevant modules");
|
||
|
|
||
|
/** @type {Statistics} */
|
||
|
const stats = {
|
||
|
cached: 0,
|
||
|
alreadyInConfig: 0,
|
||
|
invalidModule: 0,
|
||
|
incorrectChunks: 0,
|
||
|
incorrectDependency: 0,
|
||
|
incorrectModuleDependency: 0,
|
||
|
incorrectChunksOfImporter: 0,
|
||
|
incorrectRuntimeCondition: 0,
|
||
|
importerFailed: 0,
|
||
|
added: 0
|
||
|
};
|
||
|
let statsCandidates = 0;
|
||
|
let statsSizeSum = 0;
|
||
|
let statsEmptyConfigurations = 0;
|
||
|
|
||
|
logger.time("find modules to concatenate");
|
||
|
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
|
||
|
// TODO reconsider that when it's only used in a different runtime
|
||
|
if (usedAsInner.has(currentRoot)) continue;
|
||
|
|
||
|
let chunkRuntime = undefined;
|
||
|
for (const r of chunkGraph.getModuleRuntimes(currentRoot)) {
|
||
|
chunkRuntime = mergeRuntimeOwned(chunkRuntime, r);
|
||
|
}
|
||
|
const exportsInfo = moduleGraph.getExportsInfo(currentRoot);
|
||
|
const filteredRuntime = filterRuntime(chunkRuntime, r =>
|
||
|
exportsInfo.isModuleUsed(r)
|
||
|
);
|
||
|
const activeRuntime =
|
||
|
filteredRuntime === true
|
||
|
? chunkRuntime
|
||
|
: filteredRuntime === false
|
||
|
? undefined
|
||
|
: filteredRuntime;
|
||
|
|
||
|
// create a configuration with the root
|
||
|
const currentConfiguration = new ConcatConfiguration(
|
||
|
currentRoot,
|
||
|
activeRuntime
|
||
|
);
|
||
|
|
||
|
// cache failures to add modules
|
||
|
const failureCache = new Map();
|
||
|
|
||
|
// potential optional import candidates
|
||
|
/** @type {Set<Module>} */
|
||
|
const candidates = new Set();
|
||
|
|
||
|
// try to add all imports
|
||
|
for (const imp of this._getImports(
|
||
|
compilation,
|
||
|
currentRoot,
|
||
|
activeRuntime
|
||
|
)) {
|
||
|
candidates.add(imp);
|
||
|
}
|
||
|
|
||
|
for (const imp of candidates) {
|
||
|
const impCandidates = new Set();
|
||
|
const problem = this._tryToAdd(
|
||
|
compilation,
|
||
|
currentConfiguration,
|
||
|
imp,
|
||
|
chunkRuntime,
|
||
|
activeRuntime,
|
||
|
possibleInners,
|
||
|
impCandidates,
|
||
|
failureCache,
|
||
|
chunkGraph,
|
||
|
true,
|
||
|
stats
|
||
|
);
|
||
|
if (problem) {
|
||
|
failureCache.set(imp, problem);
|
||
|
currentConfiguration.addWarning(imp, problem);
|
||
|
} else {
|
||
|
for (const c of impCandidates) {
|
||
|
candidates.add(c);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
statsCandidates += candidates.size;
|
||
|
if (!currentConfiguration.isEmpty()) {
|
||
|
const modules = currentConfiguration.getModules();
|
||
|
statsSizeSum += modules.size;
|
||
|
concatConfigurations.push(currentConfiguration);
|
||
|
for (const module of modules) {
|
||
|
if (module !== currentConfiguration.rootModule) {
|
||
|
usedAsInner.add(module);
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
statsEmptyConfigurations++;
|
||
|
const optimizationBailouts =
|
||
|
moduleGraph.getOptimizationBailout(currentRoot);
|
||
|
for (const warning of currentConfiguration.getWarningsSorted()) {
|
||
|
optimizationBailouts.push(
|
||
|
formatBailoutWarning(warning[0], warning[1])
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
logger.timeEnd("find modules to concatenate");
|
||
|
logger.debug(
|
||
|
`${
|
||
|
concatConfigurations.length
|
||
|
} successful concat configurations (avg size: ${
|
||
|
statsSizeSum / concatConfigurations.length
|
||
|
}), ${statsEmptyConfigurations} bailed out completely`
|
||
|
);
|
||
|
logger.debug(
|
||
|
`${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)`
|
||
|
);
|
||
|
// HACK: Sort configurations by length and start with the longest one
|
||
|
// to get the biggest 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)
|
||
|
logger.time(`sort concat configurations`);
|
||
|
concatConfigurations.sort((a, b) => {
|
||
|
return b.modules.size - a.modules.size;
|
||
|
});
|
||
|
logger.timeEnd(`sort concat configurations`);
|
||
|
const usedModules = new Set();
|
||
|
|
||
|
logger.time("create concatenated modules");
|
||
|
asyncLib.each(
|
||
|
concatConfigurations,
|
||
|
(concatConfiguration, callback) => {
|
||
|
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);
|
||
|
}
|
||
|
|
||
|
// Create a new ConcatenatedModule
|
||
|
let newModule = ConcatenatedModule.create(
|
||
|
rootModule,
|
||
|
modules,
|
||
|
concatConfiguration.runtime,
|
||
|
compiler.root,
|
||
|
compilation.outputOptions.hashFunction
|
||
|
);
|
||
|
|
||
|
const build = () => {
|
||
|
newModule.build(
|
||
|
compiler.options,
|
||
|
compilation,
|
||
|
null,
|
||
|
null,
|
||
|
err => {
|
||
|
if (err) {
|
||
|
if (!err.module) {
|
||
|
err.module = newModule;
|
||
|
}
|
||
|
return callback(err);
|
||
|
}
|
||
|
integrate();
|
||
|
}
|
||
|
);
|
||
|
};
|
||
|
|
||
|
const integrate = () => {
|
||
|
if (backCompat) {
|
||
|
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) {
|
||
|
// add to builtModules when one of the included modules was built
|
||
|
if (compilation.builtModules.has(m)) {
|
||
|
compilation.builtModules.add(newModule);
|
||
|
}
|
||
|
if (m !== rootModule) {
|
||
|
// attach external references to the concatenated module too
|
||
|
moduleGraph.copyOutgoingModuleConnections(
|
||
|
m,
|
||
|
newModule,
|
||
|
c => {
|
||
|
return (
|
||
|
c.originModule === m &&
|
||
|
!(
|
||
|
c.dependency instanceof HarmonyImportDependency &&
|
||
|
modules.has(c.module)
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
);
|
||
|
// remove module from chunk
|
||
|
for (const chunk of chunkGraph.getModuleChunksIterable(
|
||
|
rootModule
|
||
|
)) {
|
||
|
const sourceTypes = chunkGraph.getChunkModuleSourceTypes(
|
||
|
chunk,
|
||
|
m
|
||
|
);
|
||
|
if (sourceTypes.size === 1) {
|
||
|
chunkGraph.disconnectChunkAndModule(chunk, m);
|
||
|
} else {
|
||
|
const newSourceTypes = new Set(sourceTypes);
|
||
|
newSourceTypes.delete("javascript");
|
||
|
chunkGraph.setChunkModuleSourceTypes(
|
||
|
chunk,
|
||
|
m,
|
||
|
newSourceTypes
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
compilation.modules.delete(rootModule);
|
||
|
ChunkGraph.clearChunkGraphForModule(rootModule);
|
||
|
ModuleGraph.clearModuleGraphForModule(rootModule);
|
||
|
|
||
|
// remove module from chunk
|
||
|
chunkGraph.replaceModule(rootModule, newModule);
|
||
|
// replace module references with the concatenated module
|
||
|
moduleGraph.moveModuleConnections(rootModule, newModule, c => {
|
||
|
const otherModule =
|
||
|
c.module === rootModule ? c.originModule : c.module;
|
||
|
const innerConnection =
|
||
|
c.dependency instanceof HarmonyImportDependency &&
|
||
|
modules.has(/** @type {Module} */ (otherModule));
|
||
|
return !innerConnection;
|
||
|
});
|
||
|
// add concatenated module to the compilation
|
||
|
compilation.modules.add(newModule);
|
||
|
|
||
|
callback();
|
||
|
};
|
||
|
|
||
|
build();
|
||
|
},
|
||
|
err => {
|
||
|
logger.timeEnd("create concatenated modules");
|
||
|
process.nextTick(callback.bind(null, err));
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {Compilation} compilation the compilation
|
||
|
* @param {Module} module the module to be added
|
||
|
* @param {RuntimeSpec} runtime the runtime scope
|
||
|
* @returns {Set<Module>} the imported modules
|
||
|
*/
|
||
|
_getImports(compilation, module, runtime) {
|
||
|
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.isTargetActive(runtime)
|
||
|
) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
const importedNames = compilation.getDependencyReferencedExports(
|
||
|
dep,
|
||
|
undefined
|
||
|
);
|
||
|
|
||
|
if (
|
||
|
importedNames.every(i =>
|
||
|
Array.isArray(i) ? i.length > 0 : i.name.length > 0
|
||
|
) ||
|
||
|
Array.isArray(moduleGraph.getProvidedExports(module))
|
||
|
) {
|
||
|
set.add(connection.module);
|
||
|
}
|
||
|
}
|
||
|
return set;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {Compilation} compilation webpack compilation
|
||
|
* @param {ConcatConfiguration} config concat configuration (will be modified when added)
|
||
|
* @param {Module} module the module to be added
|
||
|
* @param {RuntimeSpec} runtime the runtime scope of the generated code
|
||
|
* @param {RuntimeSpec} activeRuntime the runtime scope of the root module
|
||
|
* @param {Set<Module>} possibleModules modules that are candidates
|
||
|
* @param {Set<Module>} candidates list of potential candidates (will be added to)
|
||
|
* @param {Map<Module, Module | function(RequestShortener): string>} failureCache cache for problematic modules to be more performant
|
||
|
* @param {ChunkGraph} chunkGraph the chunk graph
|
||
|
* @param {boolean} avoidMutateOnFailure avoid mutating the config when adding fails
|
||
|
* @param {Statistics} statistics gathering metrics
|
||
|
* @returns {null | Module | function(RequestShortener): string} the problematic module
|
||
|
*/
|
||
|
_tryToAdd(
|
||
|
compilation,
|
||
|
config,
|
||
|
module,
|
||
|
runtime,
|
||
|
activeRuntime,
|
||
|
possibleModules,
|
||
|
candidates,
|
||
|
failureCache,
|
||
|
chunkGraph,
|
||
|
avoidMutateOnFailure,
|
||
|
statistics
|
||
|
) {
|
||
|
const cacheEntry = failureCache.get(module);
|
||
|
if (cacheEntry) {
|
||
|
statistics.cached++;
|
||
|
return cacheEntry;
|
||
|
}
|
||
|
|
||
|
// Already added?
|
||
|
if (config.has(module)) {
|
||
|
statistics.alreadyInConfig++;
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// Not possible to add?
|
||
|
if (!possibleModules.has(module)) {
|
||
|
statistics.invalidModule++;
|
||
|
failureCache.set(module, module); // cache failures for performance
|
||
|
return module;
|
||
|
}
|
||
|
|
||
|
// Module must be in the correct chunks
|
||
|
const missingChunks = Array.from(
|
||
|
chunkGraph.getModuleChunksIterable(config.rootModule)
|
||
|
).filter(chunk => !chunkGraph.isModuleInChunk(module, chunk));
|
||
|
if (missingChunks.length > 0) {
|
||
|
/**
|
||
|
* @param {RequestShortener} requestShortener request shortener
|
||
|
* @returns {string} problem description
|
||
|
*/
|
||
|
const problem = requestShortener => {
|
||
|
const missingChunksList = Array.from(
|
||
|
new Set(missingChunks.map(chunk => chunk.name || "unnamed chunk(s)"))
|
||
|
).sort();
|
||
|
const chunks = Array.from(
|
||
|
new Set(
|
||
|
Array.from(chunkGraph.getModuleChunksIterable(module)).map(
|
||
|
chunk => chunk.name || "unnamed chunk(s)"
|
||
|
)
|
||
|
)
|
||
|
).sort();
|
||
|
return `Module ${module.readableIdentifier(
|
||
|
requestShortener
|
||
|
)} is not in the same chunk(s) (expected in chunk(s) ${missingChunksList.join(
|
||
|
", "
|
||
|
)}, module is in chunk(s) ${chunks.join(", ")})`;
|
||
|
};
|
||
|
statistics.incorrectChunks++;
|
||
|
failureCache.set(module, problem); // cache failures for performance
|
||
|
return problem;
|
||
|
}
|
||
|
|
||
|
const moduleGraph = compilation.moduleGraph;
|
||
|
|
||
|
const incomingConnections =
|
||
|
moduleGraph.getIncomingConnectionsByOriginModule(module);
|
||
|
|
||
|
const incomingConnectionsFromNonModules =
|
||
|
incomingConnections.get(null) || incomingConnections.get(undefined);
|
||
|
if (incomingConnectionsFromNonModules) {
|
||
|
const activeNonModulesConnections =
|
||
|
incomingConnectionsFromNonModules.filter(connection => {
|
||
|
// We are not interested in inactive connections
|
||
|
// or connections without dependency
|
||
|
return connection.isActive(runtime);
|
||
|
});
|
||
|
if (activeNonModulesConnections.length > 0) {
|
||
|
/**
|
||
|
* @param {RequestShortener} requestShortener request shortener
|
||
|
* @returns {string} problem description
|
||
|
*/
|
||
|
const problem = requestShortener => {
|
||
|
const importingExplanations = new Set(
|
||
|
activeNonModulesConnections.map(c => c.explanation).filter(Boolean)
|
||
|
);
|
||
|
const explanations = Array.from(importingExplanations).sort();
|
||
|
return `Module ${module.readableIdentifier(
|
||
|
requestShortener
|
||
|
)} is referenced ${
|
||
|
explanations.length > 0
|
||
|
? `by: ${explanations.join(", ")}`
|
||
|
: "in an unsupported way"
|
||
|
}`;
|
||
|
};
|
||
|
statistics.incorrectDependency++;
|
||
|
failureCache.set(module, problem); // cache failures for performance
|
||
|
return problem;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** @type {Map<Module, readonly ModuleGraph.ModuleGraphConnection[]>} */
|
||
|
const incomingConnectionsFromModules = new Map();
|
||
|
for (const [originModule, connections] of incomingConnections) {
|
||
|
if (originModule) {
|
||
|
// Ignore connection from orphan modules
|
||
|
if (chunkGraph.getNumberOfModuleChunks(originModule) === 0) continue;
|
||
|
|
||
|
// We don't care for connections from other runtimes
|
||
|
let originRuntime = undefined;
|
||
|
for (const r of chunkGraph.getModuleRuntimes(originModule)) {
|
||
|
originRuntime = mergeRuntimeOwned(originRuntime, r);
|
||
|
}
|
||
|
|
||
|
if (!intersectRuntime(runtime, originRuntime)) continue;
|
||
|
|
||
|
// We are not interested in inactive connections
|
||
|
const activeConnections = connections.filter(connection =>
|
||
|
connection.isActive(runtime)
|
||
|
);
|
||
|
if (activeConnections.length > 0)
|
||
|
incomingConnectionsFromModules.set(originModule, activeConnections);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const incomingModules = Array.from(incomingConnectionsFromModules.keys());
|
||
|
|
||
|
// Module must be in the same chunks like the referencing module
|
||
|
const otherChunkModules = incomingModules.filter(originModule => {
|
||
|
for (const chunk of chunkGraph.getModuleChunksIterable(
|
||
|
config.rootModule
|
||
|
)) {
|
||
|
if (!chunkGraph.isModuleInChunk(originModule, chunk)) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
});
|
||
|
if (otherChunkModules.length > 0) {
|
||
|
/**
|
||
|
* @param {RequestShortener} requestShortener request shortener
|
||
|
* @returns {string} problem description
|
||
|
*/
|
||
|
const problem = requestShortener => {
|
||
|
const names = otherChunkModules
|
||
|
.map(m => m.readableIdentifier(requestShortener))
|
||
|
.sort();
|
||
|
return `Module ${module.readableIdentifier(
|
||
|
requestShortener
|
||
|
)} is referenced from different chunks by these modules: ${names.join(
|
||
|
", "
|
||
|
)}`;
|
||
|
};
|
||
|
statistics.incorrectChunksOfImporter++;
|
||
|
failureCache.set(module, problem); // cache failures for performance
|
||
|
return problem;
|
||
|
}
|
||
|
|
||
|
/** @type {Map<Module, readonly ModuleGraph.ModuleGraphConnection[]>} */
|
||
|
const nonHarmonyConnections = new Map();
|
||
|
for (const [originModule, connections] of incomingConnectionsFromModules) {
|
||
|
const selected = connections.filter(
|
||
|
connection =>
|
||
|
!connection.dependency ||
|
||
|
!(connection.dependency instanceof HarmonyImportDependency)
|
||
|
);
|
||
|
if (selected.length > 0)
|
||
|
nonHarmonyConnections.set(originModule, connections);
|
||
|
}
|
||
|
if (nonHarmonyConnections.size > 0) {
|
||
|
/**
|
||
|
* @param {RequestShortener} requestShortener request shortener
|
||
|
* @returns {string} problem description
|
||
|
*/
|
||
|
const problem = requestShortener => {
|
||
|
const names = Array.from(nonHarmonyConnections)
|
||
|
.map(([originModule, connections]) => {
|
||
|
return `${originModule.readableIdentifier(
|
||
|
requestShortener
|
||
|
)} (referenced with ${Array.from(
|
||
|
new Set(
|
||
|
connections
|
||
|
.map(c => c.dependency && c.dependency.type)
|
||
|
.filter(Boolean)
|
||
|
)
|
||
|
)
|
||
|
.sort()
|
||
|
.join(", ")})`;
|
||
|
})
|
||
|
.sort();
|
||
|
return `Module ${module.readableIdentifier(
|
||
|
requestShortener
|
||
|
)} is referenced from these modules with unsupported syntax: ${names.join(
|
||
|
", "
|
||
|
)}`;
|
||
|
};
|
||
|
statistics.incorrectModuleDependency++;
|
||
|
failureCache.set(module, problem); // cache failures for performance
|
||
|
return problem;
|
||
|
}
|
||
|
|
||
|
if (runtime !== undefined && typeof runtime !== "string") {
|
||
|
// Module must be consistently referenced in the same runtimes
|
||
|
/** @type {{ originModule: Module, runtimeCondition: RuntimeSpec }[]} */
|
||
|
const otherRuntimeConnections = [];
|
||
|
outer: for (const [
|
||
|
originModule,
|
||
|
connections
|
||
|
] of incomingConnectionsFromModules) {
|
||
|
/** @type {false | RuntimeSpec} */
|
||
|
let currentRuntimeCondition = false;
|
||
|
for (const connection of connections) {
|
||
|
const runtimeCondition = filterRuntime(runtime, runtime => {
|
||
|
return connection.isTargetActive(runtime);
|
||
|
});
|
||
|
if (runtimeCondition === false) continue;
|
||
|
if (runtimeCondition === true) continue outer;
|
||
|
if (currentRuntimeCondition !== false) {
|
||
|
currentRuntimeCondition = mergeRuntime(
|
||
|
currentRuntimeCondition,
|
||
|
runtimeCondition
|
||
|
);
|
||
|
} else {
|
||
|
currentRuntimeCondition = runtimeCondition;
|
||
|
}
|
||
|
}
|
||
|
if (currentRuntimeCondition !== false) {
|
||
|
otherRuntimeConnections.push({
|
||
|
originModule,
|
||
|
runtimeCondition: currentRuntimeCondition
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
if (otherRuntimeConnections.length > 0) {
|
||
|
/**
|
||
|
* @param {RequestShortener} requestShortener request shortener
|
||
|
* @returns {string} problem description
|
||
|
*/
|
||
|
const problem = requestShortener => {
|
||
|
return `Module ${module.readableIdentifier(
|
||
|
requestShortener
|
||
|
)} is runtime-dependent referenced by these modules: ${Array.from(
|
||
|
otherRuntimeConnections,
|
||
|
({ originModule, runtimeCondition }) =>
|
||
|
`${originModule.readableIdentifier(
|
||
|
requestShortener
|
||
|
)} (expected runtime ${runtimeToString(
|
||
|
runtime
|
||
|
)}, module is only referenced in ${runtimeToString(
|
||
|
/** @type {RuntimeSpec} */ (runtimeCondition)
|
||
|
)})`
|
||
|
).join(", ")}`;
|
||
|
};
|
||
|
statistics.incorrectRuntimeCondition++;
|
||
|
failureCache.set(module, problem); // cache failures for performance
|
||
|
return problem;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let backup;
|
||
|
if (avoidMutateOnFailure) {
|
||
|
backup = config.snapshot();
|
||
|
}
|
||
|
|
||
|
// Add the module
|
||
|
config.add(module);
|
||
|
|
||
|
incomingModules.sort(compareModulesByIdentifier);
|
||
|
|
||
|
// Every module which depends on the added module must be in the configuration too.
|
||
|
for (const originModule of incomingModules) {
|
||
|
const problem = this._tryToAdd(
|
||
|
compilation,
|
||
|
config,
|
||
|
originModule,
|
||
|
runtime,
|
||
|
activeRuntime,
|
||
|
possibleModules,
|
||
|
candidates,
|
||
|
failureCache,
|
||
|
chunkGraph,
|
||
|
false,
|
||
|
statistics
|
||
|
);
|
||
|
if (problem) {
|
||
|
if (backup !== undefined) config.rollback(backup);
|
||
|
statistics.importerFailed++;
|
||
|
failureCache.set(module, problem); // cache failures for performance
|
||
|
return problem;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Add imports to possible candidates list
|
||
|
for (const imp of this._getImports(compilation, module, runtime)) {
|
||
|
candidates.add(imp);
|
||
|
}
|
||
|
statistics.added++;
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class ConcatConfiguration {
|
||
|
/**
|
||
|
* @param {Module} rootModule the root module
|
||
|
* @param {RuntimeSpec} runtime the runtime
|
||
|
*/
|
||
|
constructor(rootModule, runtime) {
|
||
|
this.rootModule = rootModule;
|
||
|
this.runtime = runtime;
|
||
|
/** @type {Set<Module>} */
|
||
|
this.modules = new Set();
|
||
|
this.modules.add(rootModule);
|
||
|
/** @type {Map<Module, Module | function(RequestShortener): string>} */
|
||
|
this.warnings = new Map();
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {Module} module the module
|
||
|
*/
|
||
|
add(module) {
|
||
|
this.modules.add(module);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {Module} module the module
|
||
|
* @returns {boolean} true, when the module is in the module set
|
||
|
*/
|
||
|
has(module) {
|
||
|
return this.modules.has(module);
|
||
|
}
|
||
|
|
||
|
isEmpty() {
|
||
|
return this.modules.size === 1;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param {Module} module the module
|
||
|
* @param {Module | function(RequestShortener): string} problem the problem
|
||
|
*/
|
||
|
addWarning(module, problem) {
|
||
|
this.warnings.set(module, problem);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @returns {Map<Module, Module | function(RequestShortener): string>} warnings
|
||
|
*/
|
||
|
getWarningsSorted() {
|
||
|
return new Map(
|
||
|
Array.from(this.warnings).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;
|
||
|
})
|
||
|
);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @returns {Set<Module>} modules as set
|
||
|
*/
|
||
|
getModules() {
|
||
|
return this.modules;
|
||
|
}
|
||
|
|
||
|
snapshot() {
|
||
|
return this.modules.size;
|
||
|
}
|
||
|
|
||
|
rollback(snapshot) {
|
||
|
const modules = this.modules;
|
||
|
for (const m of modules) {
|
||
|
if (snapshot === 0) {
|
||
|
modules.delete(m);
|
||
|
} else {
|
||
|
snapshot--;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
module.exports = ModuleConcatenationPlugin;
|