UNPKG

webpack

Version:

Packs CommonJs/AMD modules for the browser. Allows to split your codebase into multiple bundles, which can be loaded on demand. Support loaders to preprocess files, i.e. json, jsx, es7, css, less, ... and your custom stuff.

751 lines (680 loc) 22.7 kB
/* 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 ModuleRestoreError = require("../ModuleRestoreError"); const ModuleStoreError = require("../ModuleStoreError"); const { STAGE_DEFAULT } = require("../OptimizationStages"); const HarmonyImportDependency = require("../dependencies/HarmonyImportDependency"); const StackedMap = require("../util/StackedMap"); const { compareModulesByIdentifier } = require("../util/comparators"); const { intersectRuntime, mergeRuntimeOwned } = 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 */ 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) { compiler.hooks.compilation.tap( "ModuleConcatenationPlugin", (compilation, { normalModuleFactory }) => { const moduleGraph = compilation.moduleGraph; const bailoutReasonMap = new Map(); const cache = compilation.getCache("ModuleConcatenationPlugin"); const setBailoutReason = (module, reason) => { setInnerBailoutReason(module, reason); moduleGraph .getOptimizationBailout(module) .push( typeof reason === "function" ? rs => formatBailoutReason(reason(rs)) : formatBailoutReason(reason) ); }; const setInnerBailoutReason = (module, reason) => { bailoutReasonMap.set(module, reason); }; const getInnerBailoutReason = (module, requestShortener) => { const reason = bailoutReasonMap.get(module); if (typeof reason === "function") return reason(requestShortener); return 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("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"); 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 if (usedAsInner.has(currentRoot)) continue; let runtime = undefined; for (const r of chunkGraph.getModuleRuntimes(currentRoot)) { runtime = mergeRuntimeOwned(runtime, r); } // create a configuration with the root const currentConfiguration = new ConcatConfiguration(currentRoot); // 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, runtime )) { candidates.add(imp); } for (const imp of candidates) { // _tryToAdd modifies the config even if it fails // so make sure to only accept changes when it succeed const backup = currentConfiguration.snapshot(); const impCandidates = new Set(); const problem = this._tryToAdd( compilation, currentConfiguration, imp, runtime, possibleInners, impCandidates, failureCache, chunkGraph ); if (problem) { failureCache.set(imp, problem); currentConfiguration.addWarning(imp, problem); // roll back currentConfiguration.rollback(backup); } else { for (const c of impCandidates) { candidates.add(c); } } } if (!currentConfiguration.isEmpty()) { concatConfigurations.push(currentConfiguration); for (const module of currentConfiguration.getModules()) { if (module !== currentConfiguration.rootModule) { usedAsInner.add(module); } } } else { 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} concat configurations` ); // 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, compiler.root ); const cacheItem = cache.getItemCache( newModule.identifier(), null ); const restore = () => { cacheItem.get((err, cacheModule) => { if (err) { return callback(new ModuleRestoreError(newModule, err)); } if (cacheModule) { cacheModule.updateCacheModule(newModule); newModule = cacheModule; } build(); }); }; const build = () => { newModule.build( compiler.options, compilation, null, null, err => { if (err) { if (!err.module) { err.module = newModule; } return callback(err); } integrateAndStore(); } ); }; const integrateAndStore = () => { 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 )) { chunkGraph.disconnectChunkAndModule(chunk, m); } } } compilation.modules.delete(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(otherModule); return !innerConnection; } ); // add concatenated module to the compilation compilation.modules.add(newModule); // TODO check if module needs build to avoid caching it without change cacheItem.store(newModule, err => { if (err) { return callback(new ModuleStoreError(newModule, err)); } callback(); }); }; restore(); }, err => { logger.timeEnd("create concatenated modules"); process.nextTick(() => callback(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 * @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 * @returns {Module | function(RequestShortener): string} the problematic module */ _tryToAdd( compilation, config, module, runtime, possibleModules, candidates, failureCache, chunkGraph ) { 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 correct chunks const missingChunks = Array.from( chunkGraph.getModuleChunksIterable(config.rootModule) ) .filter(chunk => !chunkGraph.isModuleInChunk(module, chunk)) .map(chunk => chunk.name || "unnamed chunk(s)"); if (missingChunks.length > 0) { const missingChunksList = Array.from(new Set(missingChunks)).sort(); const chunks = Array.from( new Set( Array.from(chunkGraph.getModuleChunksIterable(module)).map( chunk => chunk.name || "unnamed chunk(s)" ) ) ).sort(); const problem = requestShortener => `Module ${module.readableIdentifier( requestShortener )} is not in the same chunk(s) (expected in chunk(s) ${missingChunksList.join( ", " )}, module is in chunk(s) ${chunks.join(", ")})`; failureCache.set(module, problem); // cache failures for performance return problem; } // Add the module config.add(module); const moduleGraph = compilation.moduleGraph; const incomingConnections = Array.from( moduleGraph.getIncomingConnections(module) ).filter(connection => { // We are not interested in inactive connections if (!connection.isActive(runtime)) return false; // Include, but do not analyse further, connections from non-modules if (!connection.originModule) return true; // Ignore connection from orphan modules if (chunkGraph.getNumberOfModuleChunks(connection.originModule) === 0) return false; // We don't care for connections from other runtimes let originRuntime = undefined; for (const r of chunkGraph.getModuleRuntimes(connection.originModule)) { originRuntime = mergeRuntimeOwned(originRuntime, r); } return intersectRuntime(runtime, originRuntime); }); const nonHarmonyConnections = incomingConnections.filter( connection => !connection.originModule || !connection.dependency || !(connection.dependency instanceof HarmonyImportDependency) ); if (nonHarmonyConnections.length > 0) { const problem = requestShortener => { const importingModules = new Set( nonHarmonyConnections.map(c => c.originModule).filter(Boolean) ); const importingExplanations = new Set( nonHarmonyConnections.map(c => c.explanation).filter(Boolean) ); const importingModuleTypes = new Map( Array.from(importingModules).map( m => /** @type {[Module, Set<string>]} */ ([ m, new Set( nonHarmonyConnections .filter(c => c.originModule === m) .map(c => c.dependency.type) .sort() ) ]) ) ); 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 ${module.readableIdentifier( requestShortener )} is referenced from these modules with unsupported syntax: ${names.join( ", " )}`; } else if (names.length === 0 && explanations.length > 0) { return `Module ${module.readableIdentifier( requestShortener )} is referenced by: ${explanations.join(", ")}`; } else if (names.length > 0 && explanations.length > 0) { return `Module ${module.readableIdentifier( requestShortener )} is referenced from these modules with unsupported syntax: ${names.join( ", " )} and by: ${explanations.join(", ")}`; } else { return `Module ${module.readableIdentifier( requestShortener )} is referenced in a unsupported way`; } }; failureCache.set(module, problem); // cache failures for performance return problem; } // Module must be in the same chunks like the referencing module const otherChunkConnections = incomingConnections.filter(connection => { for (const chunk of chunkGraph.getModuleChunksIterable( config.rootModule )) { if (!chunkGraph.isModuleInChunk(connection.originModule, chunk)) { return true; } } return false; }); if (otherChunkConnections.length > 0) { const problem = requestShortener => { const importingModules = new Set( otherChunkConnections.map(c => c.originModule) ); const names = Array.from(importingModules) .map(m => m.readableIdentifier(requestShortener)) .sort(); return `Module ${module.readableIdentifier( requestShortener )} is referenced from different chunks by these modules: ${names.join( ", " )}`; }; failureCache.set(module, problem); // cache failures for performance return problem; } const incomingModules = Array.from( new Set(incomingConnections.map(c => c.originModule)) ).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, possibleModules, candidates, failureCache, chunkGraph ); if (problem) { 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); } return null; } } class ConcatConfiguration { /** * * @param {Module} rootModule the root module */ constructor(rootModule) { this.rootModule = rootModule; /** @type {StackedMap<Module, true>} */ this.modules = new StackedMap(); this.modules.set(rootModule, true); /** @type {StackedMap<Module, Module | function(RequestShortener): string>} */ this.warnings = new StackedMap(); } add(module) { this.modules.set(module, true); } 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; }) ); } /** * @returns {Set<Module>} modules as set */ getModules() { return this.modules.asSet(); } snapshot() { const base = this.modules; this.modules = this.modules.createChild(); return base; } rollback(snapshot) { this.modules = snapshot; } } module.exports = ModuleConcatenationPlugin;