/* 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 HarmonyExportImportedSpecifierDependency = require("../dependencies/HarmonyExportImportedSpecifierDependency"); const HarmonyCompatibilityDependency = require("../dependencies/HarmonyCompatibilityDependency"); function formatBailoutReason(msg) { return "ModuleConcatenation bailout: " + msg; } 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(typeof reason === "function" ? (rs) => formatBailoutReason(reason(rs)) : formatBailoutReason(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) => { const relevantModules = []; const possibleInners = new Set(); for(const module of modules) { // Only harmony modules are valid for optimization if(!module.meta || !module.meta.harmonyModule || !module.dependencies.some(d => d instanceof HarmonyCompatibilityDependency)) { setBailoutReason(module, "Module is not an ECMAScript module"); continue; } // Because of variable renaming we can't use modules with eval if(module.meta && module.meta.hasEval) { setBailoutReason(module, "Module uses eval()"); continue; } // Exports must be known (and not dynamic) if(!Array.isArray(module.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.getChunks().some(chunk => chunk.entryModule === module)) { setBailoutReason(module, "Module is an entry point"); 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)); const importingModuleTypes = new Map(Array.from(importingModules).map(m => [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(); return `Module is referenced from these modules with unsupported syntax: ${names.join(", ")}`; }); 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(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 newModule = new ConcatenatedModule(concatConfiguration.rootModule, Array.from(concatConfiguration.modules)); concatConfiguration.sortWarnings(); for(const warning of concatConfiguration.warnings) { 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 concatConfiguration.modules) { usedModules.add(m); chunks.forEach(chunk => chunk.removeModule(m)); } chunks.forEach(chunk => { chunk.addModule(newModule); if(chunk.entryModule === concatConfiguration.rootModule) chunk.entryModule = newModule; }); compilation.modules.push(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)) { 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) { 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; } } 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); } sortWarnings() { this.warnings = 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; })); } 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;