|
|
- /*
- 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;
|