mirror of https://github.com/webpack/webpack.git
				
				
				
			
		
			
				
	
	
		
			293 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
			
		
		
	
	
			293 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
| /*
 | |
| 	MIT License http://www.opensource.org/licenses/mit-license.php
 | |
| 	Author Tobias Koppers @sokra
 | |
| */
 | |
| "use strict";
 | |
| 
 | |
| const HarmonyImportDependency = require("../dependencies/HarmonyImportDependency");
 | |
| const ConcatenatedModule = require("./ConcatenatedModule");
 | |
| const HarmonyExportImportedSpecifierDependency = require("../dependencies/HarmonyExportImportedSpecifierDependency");
 | |
| 
 | |
| class ModuleConcatenationPlugin {
 | |
| 	constructor(options) {
 | |
| 		if(typeof options !== "object") options = {};
 | |
| 		this.options = options;
 | |
| 	}
 | |
| 
 | |
| 	apply(compiler) {
 | |
| 		compiler.plugin("compilation", (compilation, params) => {
 | |
| 			params.normalModuleFactory.plugin("parser", (parser, parserOptions) => {
 | |
| 				parser.plugin("call eval", () => {
 | |
| 					parser.state.module.meta.hasEval = true;
 | |
| 				});
 | |
| 			});
 | |
| 			const bailoutReasonMap = new Map();
 | |
| 
 | |
| 			function setBailoutReason(module, reason) {
 | |
| 				bailoutReasonMap.set(module, reason);
 | |
| 				module.optimizationBailout.push(reason);
 | |
| 			}
 | |
| 
 | |
| 			function getBailoutReason(module, requestShortener) {
 | |
| 				const reason = bailoutReasonMap.get(module);
 | |
| 				if(typeof reason === "function") return reason(requestShortener);
 | |
| 				return reason;
 | |
| 			}
 | |
| 
 | |
| 			compilation.plugin("optimize-chunk-modules", (chunks, modules) => {
 | |
| 				chunks.forEach(chunk => {
 | |
| 					const relevantModules = [];
 | |
| 					const possibleInners = new Set();
 | |
| 					for(const module of chunk.modulesIterable) {
 | |
| 						// Only harmony modules are valid for optimization
 | |
| 						if(!module.meta || !module.meta.harmonyModule) {
 | |
| 							continue;
 | |
| 						}
 | |
| 
 | |
| 						// Module must not be in other chunks
 | |
| 						// TODO add an option to allow module to be in other entry points
 | |
| 						if(module.getNumberOfChunks() !== 1) {
 | |
| 							setBailoutReason(module, "ModuleConcatenation: module is in multiple chunks");
 | |
| 							continue;
 | |
| 						}
 | |
| 
 | |
| 						// Because of variable renaming we can't use modules with eval
 | |
| 						if(module.meta && module.meta.hasEval) {
 | |
| 							setBailoutReason(module, "ModuleConcatenation: eval is used in the module");
 | |
| 							continue;
 | |
| 						}
 | |
| 
 | |
| 						relevantModules.push(module);
 | |
| 
 | |
| 						// Module must not be the entry points
 | |
| 						if(chunk.entryModule === module) {
 | |
| 							setBailoutReason(module, "ModuleConcatenation (inner): module is an entrypoint");
 | |
| 							continue;
 | |
| 						}
 | |
| 
 | |
| 						// Exports must be known (and not dynamic)
 | |
| 						if(!Array.isArray(module.providedExports)) {
 | |
| 							setBailoutReason(module, "ModuleConcatenation (inner): exports are not known");
 | |
| 							continue;
 | |
| 						}
 | |
| 
 | |
| 						// Using dependency variables is not possible as this wraps the code in a function
 | |
| 						if(module.variables.length > 0) {
 | |
| 							setBailoutReason(module, "ModuleConcatenation (inner): dependency variables are used (i. e. ProvidePlugin)");
 | |
| 							continue;
 | |
| 						}
 | |
| 
 | |
| 						// Module must only be used by Harmony Imports
 | |
| 						const nonHarmonyReasons = module.reasons.filter(reason => !(reason.dependency instanceof HarmonyImportDependency));
 | |
| 						if(nonHarmonyReasons.length > 0) {
 | |
| 							const importingModules = new Set(nonHarmonyReasons.map(r => r.module));
 | |
| 							setBailoutReason(module, (requestShortener) => {
 | |
| 								const names = Array.from(importingModules).map(m => m.readableIdentifier(requestShortener));
 | |
| 								return `ModuleConcatenation (inner): module is used with non-harmony imports from ${names.join(", ")}`;
 | |
| 							});
 | |
| 							continue;
 | |
| 						}
 | |
| 
 | |
| 						possibleInners.add(module);
 | |
| 					}
 | |
| 					// sort by depth
 | |
| 					// modules with lower depth are more likly 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(currentRoot)) {
 | |
| 							const problem = this.tryToAdd(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.modules) {
 | |
| 								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 orderedModules = new Set();
 | |
| 						this.addInOrder(concatConfiguration.rootModule, concatConfiguration.modules, orderedModules);
 | |
| 						const newModule = new ConcatenatedModule(concatConfiguration.rootModule, Array.from(orderedModules));
 | |
| 						for(const warning of concatConfiguration.warnings) {
 | |
| 							newModule.optimizationBailout.push((requestShortener) => {
 | |
| 								const reason = getBailoutReason(warning[0], requestShortener);
 | |
| 								const reasonPrefix = reason ? `: ${reason}` : "";
 | |
| 								if(warning[0] === warning[1])
 | |
| 									return `ModuleConcatenation: Cannot concat with ${warning[0].readableIdentifier(requestShortener)}${reasonPrefix}`;
 | |
| 								else
 | |
| 									return `ModuleConcatenation: Cannot concat with ${warning[0].readableIdentifier(requestShortener)} because of ${warning[1].readableIdentifier(requestShortener)}${reasonPrefix}`;
 | |
| 							});
 | |
| 						}
 | |
| 						for(const m of orderedModules) {
 | |
| 							usedModules.add(m);
 | |
| 							chunk.removeModule(m);
 | |
| 						}
 | |
| 						chunk.addModule(newModule);
 | |
| 						compilation.modules.push(newModule);
 | |
| 						if(chunk.entryModule === concatConfiguration.rootModule)
 | |
| 							chunk.entryModule = newModule;
 | |
| 						newModule.reasons.forEach(reason => reason.dependency.module = newModule);
 | |
| 						newModule.dependencies.forEach(dep => {
 | |
| 							if(dep.module) {
 | |
| 								dep.module.reasons.forEach(reason => {
 | |
| 									if(reason.dependency === dep)
 | |
| 										reason.module = newModule;
 | |
| 								});
 | |
| 							}
 | |
| 						});
 | |
| 					}
 | |
| 					compilation.modules = compilation.modules.filter(m => !usedModules.has(m));
 | |
| 				});
 | |
| 			});
 | |
| 		});
 | |
| 	}
 | |
| 
 | |
| 	getImports(module) {
 | |
| 		return Array.from(new Set(module.dependencies
 | |
| 
 | |
| 			// Only harmony Dependencies
 | |
| 			.filter(dep => dep instanceof HarmonyImportDependency && dep.module)
 | |
| 
 | |
| 			// Dependencies are simple enough to concat them
 | |
| 			.filter(dep => {
 | |
| 				return !module.dependencies.some(d =>
 | |
| 					d instanceof HarmonyExportImportedSpecifierDependency &&
 | |
| 					d.importDependency === dep &&
 | |
| 					!d.id &&
 | |
| 					!Array.isArray(dep.module.providedExports)
 | |
| 				);
 | |
| 			})
 | |
| 
 | |
| 			// Take the imported module
 | |
| 			.map(dep => dep.module)
 | |
| 		));
 | |
| 	}
 | |
| 
 | |
| 	tryToAdd(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)) {
 | |
| 			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) {
 | |
| 			const problem = this.tryToAdd(testConfig, reason.module, possibleModules, failureCache);
 | |
| 			if(problem) {
 | |
| 				failureCache.set(module, problem); // cache failures for performance
 | |
| 				return problem;
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Eagerly try to add imports too if possible
 | |
| 		for(const imp of this.getImports(module)) {
 | |
| 			const problem = this.tryToAdd(testConfig, imp, possibleModules, failureCache);
 | |
| 			if(problem) {
 | |
| 				config.addWarning(module, problem);
 | |
| 			}
 | |
| 		}
 | |
| 
 | |
| 		// Commit experimental changes
 | |
| 		config.set(testConfig);
 | |
| 		return null;
 | |
| 	}
 | |
| 
 | |
| 	addInOrder(module, unorderedSet, orderedSet) {
 | |
| 		if(orderedSet.has(module)) return;
 | |
| 		if(!unorderedSet.has(module)) return;
 | |
| 		orderedSet.add(module);
 | |
| 		for(const imp of this.getImports(module))
 | |
| 			this.addInOrder(imp, unorderedSet, orderedSet);
 | |
| 		orderedSet.delete(module);
 | |
| 		orderedSet.add(module);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| class ConcatConfiguration {
 | |
| 	constructor(rootModule) {
 | |
| 		this.rootModule = rootModule;
 | |
| 		this.modules = new Set([rootModule]);
 | |
| 		this.warnings = new Map();
 | |
| 	}
 | |
| 
 | |
| 	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);
 | |
| 	}
 | |
| 
 | |
| 	clone() {
 | |
| 		const clone = new ConcatConfiguration(this.rootModule);
 | |
| 		for(const module of this.modules)
 | |
| 			clone.add(module);
 | |
| 		for(const pair of this.warnings)
 | |
| 			clone.addWarning(pair[0], pair[1]);
 | |
| 		return clone;
 | |
| 	}
 | |
| 
 | |
| 	set(config) {
 | |
| 		this.rootModule = config.rootModule;
 | |
| 		this.modules = new Set(config.modules);
 | |
| 		this.warnings = new Map(config.warnings);
 | |
| 	}
 | |
| }
 | |
| 
 | |
| module.exports = ModuleConcatenationPlugin;
 |