You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

485 lines
14 KiB

4 years ago
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const HarmonyImportDependency = require("../dependencies/HarmonyImportDependency");
  7. const ModuleHotAcceptDependency = require("../dependencies/ModuleHotAcceptDependency");
  8. const ModuleHotDeclineDependency = require("../dependencies/ModuleHotDeclineDependency");
  9. const ConcatenatedModule = require("./ConcatenatedModule");
  10. const HarmonyCompatibilityDependency = require("../dependencies/HarmonyCompatibilityDependency");
  11. const StackedSetMap = require("../util/StackedSetMap");
  12. const formatBailoutReason = msg => {
  13. return "ModuleConcatenation bailout: " + msg;
  14. };
  15. class ModuleConcatenationPlugin {
  16. constructor(options) {
  17. if (typeof options !== "object") options = {};
  18. this.options = options;
  19. }
  20. apply(compiler) {
  21. compiler.hooks.compilation.tap(
  22. "ModuleConcatenationPlugin",
  23. (compilation, { normalModuleFactory }) => {
  24. const handler = (parser, parserOptions) => {
  25. parser.hooks.call.for("eval").tap("ModuleConcatenationPlugin", () => {
  26. // Because of variable renaming we can't use modules with eval.
  27. parser.state.module.buildMeta.moduleConcatenationBailout = "eval()";
  28. });
  29. };
  30. normalModuleFactory.hooks.parser
  31. .for("javascript/auto")
  32. .tap("ModuleConcatenationPlugin", handler);
  33. normalModuleFactory.hooks.parser
  34. .for("javascript/dynamic")
  35. .tap("ModuleConcatenationPlugin", handler);
  36. normalModuleFactory.hooks.parser
  37. .for("javascript/esm")
  38. .tap("ModuleConcatenationPlugin", handler);
  39. const bailoutReasonMap = new Map();
  40. const setBailoutReason = (module, reason) => {
  41. bailoutReasonMap.set(module, reason);
  42. module.optimizationBailout.push(
  43. typeof reason === "function"
  44. ? rs => formatBailoutReason(reason(rs))
  45. : formatBailoutReason(reason)
  46. );
  47. };
  48. const getBailoutReason = (module, requestShortener) => {
  49. const reason = bailoutReasonMap.get(module);
  50. if (typeof reason === "function") return reason(requestShortener);
  51. return reason;
  52. };
  53. compilation.hooks.optimizeChunkModules.tap(
  54. "ModuleConcatenationPlugin",
  55. (allChunks, modules) => {
  56. const relevantModules = [];
  57. const possibleInners = new Set();
  58. for (const module of modules) {
  59. // Only harmony modules are valid for optimization
  60. if (
  61. !module.buildMeta ||
  62. module.buildMeta.exportsType !== "namespace" ||
  63. !module.dependencies.some(
  64. d => d instanceof HarmonyCompatibilityDependency
  65. )
  66. ) {
  67. setBailoutReason(module, "Module is not an ECMAScript module");
  68. continue;
  69. }
  70. // Some expressions are not compatible with module concatenation
  71. // because they may produce unexpected results. The plugin bails out
  72. // if some were detected upfront.
  73. if (
  74. module.buildMeta &&
  75. module.buildMeta.moduleConcatenationBailout
  76. ) {
  77. setBailoutReason(
  78. module,
  79. `Module uses ${module.buildMeta.moduleConcatenationBailout}`
  80. );
  81. continue;
  82. }
  83. // Exports must be known (and not dynamic)
  84. if (!Array.isArray(module.buildMeta.providedExports)) {
  85. setBailoutReason(module, "Module exports are unknown");
  86. continue;
  87. }
  88. // Using dependency variables is not possible as this wraps the code in a function
  89. if (module.variables.length > 0) {
  90. setBailoutReason(
  91. module,
  92. `Module uses injected variables (${module.variables
  93. .map(v => v.name)
  94. .join(", ")})`
  95. );
  96. continue;
  97. }
  98. // Hot Module Replacement need it's own module to work correctly
  99. if (
  100. module.dependencies.some(
  101. dep =>
  102. dep instanceof ModuleHotAcceptDependency ||
  103. dep instanceof ModuleHotDeclineDependency
  104. )
  105. ) {
  106. setBailoutReason(module, "Module uses Hot Module Replacement");
  107. continue;
  108. }
  109. relevantModules.push(module);
  110. // Module must not be the entry points
  111. if (module.isEntryModule()) {
  112. setBailoutReason(module, "Module is an entry point");
  113. continue;
  114. }
  115. // Module must be in any chunk (we don't want to do useless work)
  116. if (module.getNumberOfChunks() === 0) {
  117. setBailoutReason(module, "Module is not in any chunk");
  118. continue;
  119. }
  120. // Module must only be used by Harmony Imports
  121. const nonHarmonyReasons = module.reasons.filter(
  122. reason =>
  123. !reason.dependency ||
  124. !(reason.dependency instanceof HarmonyImportDependency)
  125. );
  126. if (nonHarmonyReasons.length > 0) {
  127. const importingModules = new Set(
  128. nonHarmonyReasons.map(r => r.module).filter(Boolean)
  129. );
  130. const importingExplanations = new Set(
  131. nonHarmonyReasons.map(r => r.explanation).filter(Boolean)
  132. );
  133. const importingModuleTypes = new Map(
  134. Array.from(importingModules).map(
  135. m => /** @type {[string, Set]} */ ([
  136. m,
  137. new Set(
  138. nonHarmonyReasons
  139. .filter(r => r.module === m)
  140. .map(r => r.dependency.type)
  141. .sort()
  142. )
  143. ])
  144. )
  145. );
  146. setBailoutReason(module, requestShortener => {
  147. const names = Array.from(importingModules)
  148. .map(
  149. m =>
  150. `${m.readableIdentifier(
  151. requestShortener
  152. )} (referenced with ${Array.from(
  153. importingModuleTypes.get(m)
  154. ).join(", ")})`
  155. )
  156. .sort();
  157. const explanations = Array.from(importingExplanations).sort();
  158. if (names.length > 0 && explanations.length === 0) {
  159. return `Module is referenced from these modules with unsupported syntax: ${names.join(
  160. ", "
  161. )}`;
  162. } else if (names.length === 0 && explanations.length > 0) {
  163. return `Module is referenced by: ${explanations.join(
  164. ", "
  165. )}`;
  166. } else if (names.length > 0 && explanations.length > 0) {
  167. return `Module is referenced from these modules with unsupported syntax: ${names.join(
  168. ", "
  169. )} and by: ${explanations.join(", ")}`;
  170. } else {
  171. return "Module is referenced in a unsupported way";
  172. }
  173. });
  174. continue;
  175. }
  176. possibleInners.add(module);
  177. }
  178. // sort by depth
  179. // modules with lower depth are more likely suited as roots
  180. // this improves performance, because modules already selected as inner are skipped
  181. relevantModules.sort((a, b) => {
  182. return a.depth - b.depth;
  183. });
  184. const concatConfigurations = [];
  185. const usedAsInner = new Set();
  186. for (const currentRoot of relevantModules) {
  187. // when used by another configuration as inner:
  188. // the other configuration is better and we can skip this one
  189. if (usedAsInner.has(currentRoot)) continue;
  190. // create a configuration with the root
  191. const currentConfiguration = new ConcatConfiguration(currentRoot);
  192. // cache failures to add modules
  193. const failureCache = new Map();
  194. // try to add all imports
  195. for (const imp of this._getImports(compilation, currentRoot)) {
  196. const problem = this._tryToAdd(
  197. compilation,
  198. currentConfiguration,
  199. imp,
  200. possibleInners,
  201. failureCache
  202. );
  203. if (problem) {
  204. failureCache.set(imp, problem);
  205. currentConfiguration.addWarning(imp, problem);
  206. }
  207. }
  208. if (!currentConfiguration.isEmpty()) {
  209. concatConfigurations.push(currentConfiguration);
  210. for (const module of currentConfiguration.getModules()) {
  211. if (module !== currentConfiguration.rootModule) {
  212. usedAsInner.add(module);
  213. }
  214. }
  215. }
  216. }
  217. // HACK: Sort configurations by length and start with the longest one
  218. // to get the biggers groups possible. Used modules are marked with usedModules
  219. // TODO: Allow to reuse existing configuration while trying to add dependencies.
  220. // This would improve performance. O(n^2) -> O(n)
  221. concatConfigurations.sort((a, b) => {
  222. return b.modules.size - a.modules.size;
  223. });
  224. const usedModules = new Set();
  225. for (const concatConfiguration of concatConfigurations) {
  226. if (usedModules.has(concatConfiguration.rootModule)) continue;
  227. const modules = concatConfiguration.getModules();
  228. const rootModule = concatConfiguration.rootModule;
  229. const newModule = new ConcatenatedModule(
  230. rootModule,
  231. Array.from(modules),
  232. ConcatenatedModule.createConcatenationList(
  233. rootModule,
  234. modules,
  235. compilation
  236. )
  237. );
  238. for (const warning of concatConfiguration.getWarningsSorted()) {
  239. newModule.optimizationBailout.push(requestShortener => {
  240. const reason = getBailoutReason(warning[0], requestShortener);
  241. const reasonWithPrefix = reason ? ` (<- ${reason})` : "";
  242. if (warning[0] === warning[1]) {
  243. return formatBailoutReason(
  244. `Cannot concat with ${warning[0].readableIdentifier(
  245. requestShortener
  246. )}${reasonWithPrefix}`
  247. );
  248. } else {
  249. return formatBailoutReason(
  250. `Cannot concat with ${warning[0].readableIdentifier(
  251. requestShortener
  252. )} because of ${warning[1].readableIdentifier(
  253. requestShortener
  254. )}${reasonWithPrefix}`
  255. );
  256. }
  257. });
  258. }
  259. const chunks = concatConfiguration.rootModule.getChunks();
  260. for (const m of modules) {
  261. usedModules.add(m);
  262. for (const chunk of chunks) {
  263. chunk.removeModule(m);
  264. }
  265. }
  266. for (const chunk of chunks) {
  267. chunk.addModule(newModule);
  268. newModule.addChunk(chunk);
  269. }
  270. for (const chunk of allChunks) {
  271. if (chunk.entryModule === concatConfiguration.rootModule) {
  272. chunk.entryModule = newModule;
  273. }
  274. }
  275. compilation.modules.push(newModule);
  276. for (const reason of newModule.reasons) {
  277. if (reason.dependency.module === concatConfiguration.rootModule)
  278. reason.dependency.module = newModule;
  279. if (
  280. reason.dependency.redirectedModule ===
  281. concatConfiguration.rootModule
  282. )
  283. reason.dependency.redirectedModule = newModule;
  284. }
  285. // TODO: remove when LTS node version contains fixed v8 version
  286. // @see https://github.com/webpack/webpack/pull/6613
  287. // Turbofan does not correctly inline for-of loops with polymorphic input arrays.
  288. // Work around issue by using a standard for loop and assigning dep.module.reasons
  289. for (let i = 0; i < newModule.dependencies.length; i++) {
  290. let dep = newModule.dependencies[i];
  291. if (dep.module) {
  292. let reasons = dep.module.reasons;
  293. for (let j = 0; j < reasons.length; j++) {
  294. let reason = reasons[j];
  295. if (reason.dependency === dep) {
  296. reason.module = newModule;
  297. }
  298. }
  299. }
  300. }
  301. }
  302. compilation.modules = compilation.modules.filter(
  303. m => !usedModules.has(m)
  304. );
  305. }
  306. );
  307. }
  308. );
  309. }
  310. _getImports(compilation, module) {
  311. return new Set(
  312. module.dependencies
  313. // Get reference info only for harmony Dependencies
  314. .map(dep => {
  315. if (!(dep instanceof HarmonyImportDependency)) return null;
  316. if (!compilation) return dep.getReference();
  317. return compilation.getDependencyReference(module, dep);
  318. })
  319. // Reference is valid and has a module
  320. // Dependencies are simple enough to concat them
  321. .filter(
  322. ref =>
  323. ref &&
  324. ref.module &&
  325. (Array.isArray(ref.importedNames) ||
  326. Array.isArray(ref.module.buildMeta.providedExports))
  327. )
  328. // Take the imported module
  329. .map(ref => ref.module)
  330. );
  331. }
  332. _tryToAdd(compilation, config, module, possibleModules, failureCache) {
  333. const cacheEntry = failureCache.get(module);
  334. if (cacheEntry) {
  335. return cacheEntry;
  336. }
  337. // Already added?
  338. if (config.has(module)) {
  339. return null;
  340. }
  341. // Not possible to add?
  342. if (!possibleModules.has(module)) {
  343. failureCache.set(module, module); // cache failures for performance
  344. return module;
  345. }
  346. // module must be in the same chunks
  347. if (!config.rootModule.hasEqualsChunks(module)) {
  348. failureCache.set(module, module); // cache failures for performance
  349. return module;
  350. }
  351. // Clone config to make experimental changes
  352. const testConfig = config.clone();
  353. // Add the module
  354. testConfig.add(module);
  355. // Every module which depends on the added module must be in the configuration too.
  356. for (const reason of module.reasons) {
  357. // Modules that are not used can be ignored
  358. if (
  359. reason.module.factoryMeta.sideEffectFree &&
  360. reason.module.used === false
  361. )
  362. continue;
  363. const problem = this._tryToAdd(
  364. compilation,
  365. testConfig,
  366. reason.module,
  367. possibleModules,
  368. failureCache
  369. );
  370. if (problem) {
  371. failureCache.set(module, problem); // cache failures for performance
  372. return problem;
  373. }
  374. }
  375. // Commit experimental changes
  376. config.set(testConfig);
  377. // Eagerly try to add imports too if possible
  378. for (const imp of this._getImports(compilation, module)) {
  379. const problem = this._tryToAdd(
  380. compilation,
  381. config,
  382. imp,
  383. possibleModules,
  384. failureCache
  385. );
  386. if (problem) {
  387. config.addWarning(imp, problem);
  388. }
  389. }
  390. return null;
  391. }
  392. }
  393. class ConcatConfiguration {
  394. constructor(rootModule, cloneFrom) {
  395. this.rootModule = rootModule;
  396. if (cloneFrom) {
  397. this.modules = cloneFrom.modules.createChild(5);
  398. this.warnings = cloneFrom.warnings.createChild(5);
  399. } else {
  400. this.modules = new StackedSetMap();
  401. this.modules.add(rootModule);
  402. this.warnings = new StackedSetMap();
  403. }
  404. }
  405. add(module) {
  406. this.modules.add(module);
  407. }
  408. has(module) {
  409. return this.modules.has(module);
  410. }
  411. isEmpty() {
  412. return this.modules.size === 1;
  413. }
  414. addWarning(module, problem) {
  415. this.warnings.set(module, problem);
  416. }
  417. getWarningsSorted() {
  418. return new Map(
  419. this.warnings.asPairArray().sort((a, b) => {
  420. const ai = a[0].identifier();
  421. const bi = b[0].identifier();
  422. if (ai < bi) return -1;
  423. if (ai > bi) return 1;
  424. return 0;
  425. })
  426. );
  427. }
  428. getModules() {
  429. return this.modules.asSet();
  430. }
  431. clone() {
  432. return new ConcatConfiguration(this.rootModule, this);
  433. }
  434. set(config) {
  435. this.rootModule = config.rootModule;
  436. this.modules = config.modules;
  437. this.warnings = config.warnings;
  438. }
  439. }
  440. module.exports = ModuleConcatenationPlugin;