diff options
Diffstat (limited to 'node_modules/webpack/lib/optimize/ModuleConcatenationPlugin.js')
-rw-r--r-- | node_modules/webpack/lib/optimize/ModuleConcatenationPlugin.js | 485 |
1 files changed, 485 insertions, 0 deletions
diff --git a/node_modules/webpack/lib/optimize/ModuleConcatenationPlugin.js b/node_modules/webpack/lib/optimize/ModuleConcatenationPlugin.js new file mode 100644 index 0000000..0c5bfd3 --- /dev/null +++ b/node_modules/webpack/lib/optimize/ModuleConcatenationPlugin.js @@ -0,0 +1,485 @@ +/* + 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; |