123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309 |
- /*
- 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);
- newModule.addChunk(chunk);
- 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;
|