| /* |
| MIT License http://www.opensource.org/licenses/mit-license.php |
| Author Tobias Koppers @sokra |
| */ |
| "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", () => { |
| // Because of variable renaming we can't use modules with eval. |
| 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) { |
| // Only harmony modules are valid for optimization |
| if ( |
| !module.buildMeta || |
| module.buildMeta.exportsType !== "namespace" || |
| !module.dependencies.some( |
| d => d instanceof HarmonyCompatibilityDependency |
| ) |
| ) { |
| setBailoutReason(module, "Module is not an ECMAScript module"); |
| continue; |
| } |
| |
| // Some expressions are not compatible with module concatenation |
| // because they may produce unexpected results. The plugin bails out |
| // if some were detected upfront. |
| if ( |
| module.buildMeta && |
| module.buildMeta.moduleConcatenationBailout |
| ) { |
| setBailoutReason( |
| module, |
| `Module uses ${module.buildMeta.moduleConcatenationBailout}` |
| ); |
| continue; |
| } |
| |
| // Exports must be known (and not dynamic) |
| if (!Array.isArray(module.buildMeta.providedExports)) { |
| setBailoutReason(module, "Module exports are unknown"); |
| continue; |
| } |
| |
| // Using dependency variables is not possible as this wraps the code in a function |
| if (module.variables.length > 0) { |
| setBailoutReason( |
| module, |
| `Module uses injected variables (${module.variables |
| .map(v => v.name) |
| .join(", ")})` |
| ); |
| continue; |
| } |
| |
| // 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; |
| } |
| |
| relevantModules.push(module); |
| |
| // Module must not be the entry points |
| if (module.isEntryModule()) { |
| setBailoutReason(module, "Module is an entry point"); |
| continue; |
| } |
| |
| // Module must be in any chunk (we don't want to do useless work) |
| if (module.getNumberOfChunks() === 0) { |
| setBailoutReason(module, "Module is not in any chunk"); |
| continue; |
| } |
| |
| // Module must only be used by Harmony Imports |
| 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 => /** @type {[string, Set]} */ ([ |
| 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); |
| } |
| // sort by depth |
| // modules with lower depth are more likely 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(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); |
| } |
| } |
| } |
| } |
| // 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 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; |
| } |
| // TODO: remove when LTS node version contains fixed v8 version |
| // @see https://github.com/webpack/webpack/pull/6613 |
| // Turbofan does not correctly inline for-of loops with polymorphic input arrays. |
| // Work around issue by using a standard for loop and assigning dep.module.reasons |
| 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 |
| |
| // Get reference info only for harmony Dependencies |
| .map(dep => { |
| if (!(dep instanceof HarmonyImportDependency)) return null; |
| if (!compilation) return dep.getReference(); |
| return compilation.getDependencyReference(module, dep); |
| }) |
| |
| // Reference is valid and has a module |
| // Dependencies are simple enough to concat them |
| .filter( |
| ref => |
| ref && |
| ref.module && |
| (Array.isArray(ref.importedNames) || |
| Array.isArray(ref.module.buildMeta.providedExports)) |
| ) |
| |
| // Take the imported module |
| .map(ref => ref.module) |
| ); |
| } |
| |
| _tryToAdd(compilation, config, module, possibleModules, failureCache) { |
| const cacheEntry = failureCache.get(module); |
| if (cacheEntry) { |
| return cacheEntry; |
| } |
| |
| // Already added? |
| if (config.has(module)) { |
| return null; |
| } |
| |
| // 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; |
| } |
| |
| // 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) { |
| // Modules that are not used can be ignored |
| 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); // cache failures for performance |
| return problem; |
| } |
| } |
| |
| // Commit experimental changes |
| config.set(testConfig); |
| |
| // Eagerly try to add imports too if possible |
| 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; |