ModuleConcatenationPlugin.js 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const asyncLib = require("neo-async");
  7. const ChunkGraph = require("../ChunkGraph");
  8. const Dependency = require("../Dependency");
  9. const Module = require("../Module");
  10. const ModuleGraph = require("../ModuleGraph");
  11. const { JAVASCRIPT_TYPE } = require("../ModuleSourceTypeConstants");
  12. const { STAGE_DEFAULT } = require("../OptimizationStages");
  13. const HarmonyImportDependency = require("../dependencies/HarmonyImportDependency");
  14. const { ImportPhaseUtils } = require("../dependencies/ImportPhase");
  15. const { compareModulesByIdentifier } = require("../util/comparators");
  16. const { isCommonJsConcatenationEnabled } = require("../util/concatenate");
  17. const {
  18. filterRuntime,
  19. intersectRuntime,
  20. mergeRuntime,
  21. mergeRuntimeOwned,
  22. runtimeToString
  23. } = require("../util/runtime");
  24. const ConcatenatedModule = require("./ConcatenatedModule");
  25. /** @import Chunk from "../Chunk" */
  26. /** @import Compilation from "../Compilation" */
  27. /** @import Compiler from "../Compiler" */
  28. /** @import { BuildInfo, BuildMeta } from "../Module" */
  29. /** @import RequestShortener from "../RequestShortener" */
  30. /** @import { RuntimeSpec } from "../util/runtime" */
  31. /** @typedef {Module | ((requestShortener: RequestShortener) => string)} Problem */
  32. /**
  33. * Merged runtime of all chunks a module is in, memoized for the whole pass.
  34. * Safe: the chunk graph is not mutated while configurations are built.
  35. * @param {ChunkGraph} chunkGraph the chunk graph
  36. * @param {Map<Module, RuntimeSpec>} cache memoization cache
  37. * @param {Module} module the module
  38. * @returns {RuntimeSpec} the merged runtime
  39. */
  40. const getMergedModuleRuntime = (chunkGraph, cache, module) => {
  41. const cached = cache.get(module);
  42. if (cached !== undefined || cache.has(module)) return cached;
  43. /** @type {RuntimeSpec} */
  44. let runtime;
  45. for (const r of chunkGraph.getModuleRuntimes(module)) {
  46. runtime = mergeRuntimeOwned(runtime, r);
  47. }
  48. cache.set(module, runtime);
  49. return runtime;
  50. };
  51. /**
  52. * Defines the statistics type used by this module.
  53. * @typedef {object} Statistics
  54. * @property {number} cached
  55. * @property {number} alreadyInConfig
  56. * @property {number} invalidModule
  57. * @property {number} incorrectChunks
  58. * @property {number} incorrectDependency
  59. * @property {number} incorrectModuleDependency
  60. * @property {number} incorrectChunksOfImporter
  61. * @property {number} incorrectRuntimeCondition
  62. * @property {number} importerFailed
  63. * @property {number} added
  64. */
  65. /**
  66. * Record why `module` can't be added (so retried lookups hit the cache), bump
  67. * the matching statistic, and return the problem to bubble up the recursion.
  68. * Only reached on failure, so it stays off the hot success path.
  69. * @param {Map<Module, Problem>} failureCache per-module failure cache
  70. * @param {Statistics} statistics running statistics
  71. * @param {keyof Statistics} statKey statistic to increment
  72. * @param {Module} module the module that couldn't be added
  73. * @param {Problem} problem the failure to cache and return
  74. * @returns {Problem} the same problem
  75. */
  76. const cacheFailure = (failureCache, statistics, statKey, module, problem) => {
  77. statistics[statKey]++;
  78. failureCache.set(module, problem);
  79. return problem;
  80. };
  81. // Read back by `performance.scopeHoistingBailouts` to tell these reasons apart
  82. // from the other kinds `getOptimizationBailout` collects.
  83. const BAILOUT_PREFIX = "ModuleConcatenation bailout: ";
  84. // A root's reason for rejecting another module, rather than the module's own.
  85. const REJECTED_PREFIX = "Cannot concat with ";
  86. /**
  87. * Format bailout reason.
  88. * @param {string} msg message
  89. * @returns {string} formatted message
  90. */
  91. const formatBailoutReason = (msg) => `${BAILOUT_PREFIX}${msg}`;
  92. const PLUGIN_NAME = "ModuleConcatenationPlugin";
  93. class ModuleConcatenationPlugin {
  94. /**
  95. * Applies the plugin by registering its hooks on the compiler.
  96. * @param {Compiler} compiler the compiler instance
  97. * @returns {void}
  98. */
  99. apply(compiler) {
  100. const { _backCompat: backCompat } = compiler;
  101. compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
  102. if (compilation.moduleMemCaches) {
  103. throw new Error(
  104. "optimization.concatenateModules can't be used with cacheUnaffected as module concatenation is a global effect"
  105. );
  106. }
  107. const moduleGraph = compilation.moduleGraph;
  108. /** @type {Map<Module, string | ((requestShortener: RequestShortener) => string)>} */
  109. const bailoutReasonMap = new Map();
  110. /**
  111. * Sets bailout reason.
  112. * @param {Module} module the module
  113. * @param {string | ((requestShortener: RequestShortener) => string)} reason the reason
  114. */
  115. const setBailoutReason = (module, reason) => {
  116. setInnerBailoutReason(module, reason);
  117. moduleGraph
  118. .getOptimizationBailout(module)
  119. .push(
  120. typeof reason === "function"
  121. ? (rs) => formatBailoutReason(reason(rs))
  122. : formatBailoutReason(reason)
  123. );
  124. };
  125. /**
  126. * Sets inner bailout reason.
  127. * @param {Module} module the module
  128. * @param {string | ((requestShortener: RequestShortener) => string)} reason the reason
  129. */
  130. const setInnerBailoutReason = (module, reason) => {
  131. bailoutReasonMap.set(module, reason);
  132. };
  133. /**
  134. * Gets inner bailout reason.
  135. * @param {Module} module the module
  136. * @param {RequestShortener} requestShortener the request shortener
  137. * @returns {string | ((requestShortener: RequestShortener) => string) | undefined} the reason
  138. */
  139. const getInnerBailoutReason = (module, requestShortener) => {
  140. const reason = bailoutReasonMap.get(module);
  141. if (typeof reason === "function") return reason(requestShortener);
  142. return reason;
  143. };
  144. /**
  145. * Format bailout warning.
  146. * @param {Module} module the module
  147. * @param {Problem} problem the problem
  148. * @returns {(requestShortener: RequestShortener) => string} the reason
  149. */
  150. const formatBailoutWarning = (module, problem) => (requestShortener) => {
  151. if (typeof problem === "function") {
  152. return formatBailoutReason(
  153. `${REJECTED_PREFIX}${module.readableIdentifier(
  154. requestShortener
  155. )}: ${problem(requestShortener)}`
  156. );
  157. }
  158. const reason = getInnerBailoutReason(module, requestShortener);
  159. const reasonWithPrefix = reason ? `: ${reason}` : "";
  160. if (module === problem) {
  161. return formatBailoutReason(
  162. `${REJECTED_PREFIX}${module.readableIdentifier(
  163. requestShortener
  164. )}${reasonWithPrefix}`
  165. );
  166. }
  167. return formatBailoutReason(
  168. `${REJECTED_PREFIX}${module.readableIdentifier(
  169. requestShortener
  170. )} because of ${problem.readableIdentifier(
  171. requestShortener
  172. )}${reasonWithPrefix}`
  173. );
  174. };
  175. compilation.hooks.optimizeChunkModules.tapAsync(
  176. {
  177. name: PLUGIN_NAME,
  178. stage: STAGE_DEFAULT
  179. },
  180. (allChunks, modules, callback) => {
  181. const logger = compilation.getLogger(
  182. "webpack.ModuleConcatenationPlugin"
  183. );
  184. const { chunkGraph, moduleGraph } = compilation;
  185. /** @type {Module[]} */
  186. const relevantModules = [];
  187. /** @type {Set<Module>} */
  188. const possibleInners = new Set();
  189. // commonjs concatenation defaults to on; { commonjs: false } opts out
  190. const concatenateCommonJsModules = isCommonJsConcatenationEnabled(
  191. compilation.options.optimization.concatenateModules
  192. );
  193. const context = {
  194. chunkGraph,
  195. moduleGraph,
  196. concatenateCommonJsModules
  197. };
  198. const deferEnabled = compilation.options.experiments.deferImport;
  199. logger.time("select relevant modules");
  200. for (const module of modules) {
  201. let canBeRoot = true;
  202. let canBeInner = true;
  203. const bailoutReason = module.getConcatenationBailoutReason(context);
  204. if (bailoutReason) {
  205. setBailoutReason(module, bailoutReason);
  206. continue;
  207. }
  208. // Must not be an async module
  209. if (moduleGraph.isAsync(module)) {
  210. setBailoutReason(module, "Module is async");
  211. continue;
  212. }
  213. // Must be in strict mode
  214. if (!(/** @type {BuildInfo} */ (module.buildInfo).strict)) {
  215. setBailoutReason(module, "Module is not in strict mode");
  216. continue;
  217. }
  218. // Module must be in any chunk (we don't want to do useless work)
  219. if (chunkGraph.getNumberOfModuleChunks(module) === 0) {
  220. setBailoutReason(module, "Module is not in any chunk");
  221. continue;
  222. }
  223. // Exports must be known (and not dynamic)
  224. const exportsInfo = moduleGraph.getExportsInfo(module);
  225. const relevantExports = exportsInfo.getRelevantExports(undefined);
  226. const unknownReexports = relevantExports.filter(
  227. (exportInfo) =>
  228. exportInfo.isReexport() && !exportInfo.getTarget(moduleGraph)
  229. );
  230. if (unknownReexports.length > 0) {
  231. setBailoutReason(
  232. module,
  233. `Reexports in this module do not have a static target (${Array.from(
  234. unknownReexports,
  235. (exportInfo) =>
  236. `${
  237. exportInfo.name || "other exports"
  238. }: ${exportInfo.getUsedInfo()}`
  239. ).join(", ")})`
  240. );
  241. continue;
  242. }
  243. // Root modules must have a static list of exports
  244. const unknownProvidedExports = relevantExports.filter(
  245. (exportInfo) => exportInfo.provided !== true
  246. );
  247. if (unknownProvidedExports.length > 0) {
  248. setBailoutReason(
  249. module,
  250. `List of module exports is dynamic (${Array.from(
  251. unknownProvidedExports,
  252. (exportInfo) =>
  253. `${
  254. exportInfo.name || "other exports"
  255. }: ${exportInfo.getProvidedInfo()} and ${exportInfo.getUsedInfo()}`
  256. ).join(", ")})`
  257. );
  258. canBeRoot = false;
  259. }
  260. // TODO: ConcatenatedModule.getSourceTypes only javascript now
  261. const basicTypes = Module.getSourceBasicTypes(module);
  262. if (basicTypes.size !== 1 || !basicTypes.has(JAVASCRIPT_TYPE)) {
  263. canBeRoot = false;
  264. }
  265. // A concatenated CommonJS module relies on the root's ESM export
  266. // rendering, so it can only be an inner module
  267. if (
  268. module.type.startsWith("javascript/") &&
  269. /** @type {BuildMeta} */
  270. (module.buildMeta).exportsType !== "namespace"
  271. ) {
  272. canBeRoot = false;
  273. }
  274. // Module must not be an entry point
  275. if (chunkGraph.isEntryModule(module)) {
  276. setInnerBailoutReason(module, "Module is an entry point");
  277. canBeInner = false;
  278. }
  279. if (deferEnabled && moduleGraph.isDeferred(module)) {
  280. setInnerBailoutReason(module, "Module is deferred");
  281. canBeInner = false;
  282. }
  283. if (canBeRoot) relevantModules.push(module);
  284. if (canBeInner) possibleInners.add(module);
  285. }
  286. logger.timeEnd("select relevant modules");
  287. logger.debug(
  288. `${relevantModules.length} potential root modules, ${possibleInners.size} potential inner modules`
  289. );
  290. // sort by depth
  291. // modules with lower depth are more likely suited as roots
  292. // this improves performance, because modules already selected as inner are skipped
  293. logger.time("sort relevant modules");
  294. relevantModules.sort(
  295. (a, b) =>
  296. /** @type {number} */ (moduleGraph.getDepth(a)) -
  297. /** @type {number} */ (moduleGraph.getDepth(b))
  298. );
  299. logger.timeEnd("sort relevant modules");
  300. /** @type {Statistics} */
  301. const stats = {
  302. cached: 0,
  303. alreadyInConfig: 0,
  304. invalidModule: 0,
  305. incorrectChunks: 0,
  306. incorrectDependency: 0,
  307. incorrectModuleDependency: 0,
  308. incorrectChunksOfImporter: 0,
  309. incorrectRuntimeCondition: 0,
  310. importerFailed: 0,
  311. added: 0
  312. };
  313. let statsCandidates = 0;
  314. let statsSizeSum = 0;
  315. let statsEmptyConfigurations = 0;
  316. logger.time("find modules to concatenate");
  317. /** @type {ConcatConfiguration[]} */
  318. const concatConfigurations = [];
  319. /** @type {Set<Module>} */
  320. const usedAsInner = new Set();
  321. // module -> merged runtime of its chunks, shared across all roots
  322. /** @type {Map<Module, RuntimeSpec>} */
  323. const moduleRuntimeCache = new Map();
  324. for (const currentRoot of relevantModules) {
  325. // when used by another configuration as inner:
  326. // the other configuration is better and we can skip this one
  327. // TODO reconsider that when it's only used in a different runtime
  328. if (usedAsInner.has(currentRoot)) continue;
  329. const chunkRuntime = getMergedModuleRuntime(
  330. chunkGraph,
  331. moduleRuntimeCache,
  332. currentRoot
  333. );
  334. const exportsInfo = moduleGraph.getExportsInfo(currentRoot);
  335. const filteredRuntime = filterRuntime(chunkRuntime, (r) =>
  336. exportsInfo.isModuleUsed(r)
  337. );
  338. const activeRuntime =
  339. filteredRuntime === true
  340. ? chunkRuntime
  341. : filteredRuntime === false
  342. ? undefined
  343. : filteredRuntime;
  344. // create a configuration with the root; cache the root's chunks once
  345. // so every candidate check reuses them instead of re-materializing
  346. const currentConfiguration = new ConcatConfiguration(
  347. currentRoot,
  348. activeRuntime,
  349. [...chunkGraph.getModuleChunksIterable(currentRoot)]
  350. );
  351. // cache failures to add modules
  352. /** @type {Map<Module, Problem>} */
  353. const failureCache = new Map();
  354. // potential optional import candidates
  355. /** @type {Set<Module>} */
  356. const candidates = new Set();
  357. // try to add all imports
  358. for (const imp of this._getImports(
  359. compilation,
  360. currentRoot,
  361. activeRuntime
  362. )) {
  363. candidates.add(imp);
  364. }
  365. for (const imp of candidates) {
  366. /** @type {Set<Module>} */
  367. const impCandidates = new Set();
  368. const problem = this._tryToAdd(
  369. compilation,
  370. currentConfiguration,
  371. imp,
  372. chunkRuntime,
  373. activeRuntime,
  374. possibleInners,
  375. impCandidates,
  376. failureCache,
  377. moduleRuntimeCache,
  378. chunkGraph,
  379. true,
  380. stats
  381. );
  382. if (problem) {
  383. failureCache.set(imp, problem);
  384. currentConfiguration.addWarning(imp, problem);
  385. } else {
  386. for (const c of impCandidates) {
  387. candidates.add(c);
  388. }
  389. }
  390. }
  391. statsCandidates += candidates.size;
  392. if (!currentConfiguration.isEmpty()) {
  393. const modules = currentConfiguration.getModules();
  394. statsSizeSum += modules.size;
  395. concatConfigurations.push(currentConfiguration);
  396. for (const module of modules) {
  397. if (module !== currentConfiguration.rootModule) {
  398. usedAsInner.add(module);
  399. }
  400. }
  401. } else {
  402. statsEmptyConfigurations++;
  403. const optimizationBailouts =
  404. moduleGraph.getOptimizationBailout(currentRoot);
  405. for (const warning of currentConfiguration.getWarningsSorted()) {
  406. optimizationBailouts.push(
  407. formatBailoutWarning(warning[0], warning[1])
  408. );
  409. }
  410. }
  411. }
  412. logger.timeEnd("find modules to concatenate");
  413. logger.debug(
  414. `${
  415. concatConfigurations.length
  416. } successful concat configurations (avg size: ${
  417. statsSizeSum / concatConfigurations.length
  418. }), ${statsEmptyConfigurations} bailed out completely`
  419. );
  420. logger.debug(
  421. `${statsCandidates} candidates were considered for adding (${stats.cached} cached failure, ${stats.alreadyInConfig} already in config, ${stats.invalidModule} invalid module, ${stats.incorrectChunks} incorrect chunks, ${stats.incorrectDependency} incorrect dependency, ${stats.incorrectChunksOfImporter} incorrect chunks of importer, ${stats.incorrectModuleDependency} incorrect module dependency, ${stats.incorrectRuntimeCondition} incorrect runtime condition, ${stats.importerFailed} importer failed, ${stats.added} added)`
  422. );
  423. // Create the largest configurations first: if a root also ended up inside
  424. // a bigger configuration, the bigger one wins (smaller skipped below).
  425. // Configurations can't be reused across roots: a module shared by several
  426. // chunks is concatenated into each consumer (module duplication).
  427. logger.time("sort concat configurations");
  428. concatConfigurations.sort((a, b) => b.modules.size - a.modules.size);
  429. logger.timeEnd("sort concat configurations");
  430. /** @type {Set<Module>} */
  431. const usedModules = new Set();
  432. logger.time("create concatenated modules");
  433. asyncLib.each(
  434. concatConfigurations,
  435. (concatConfiguration, callback) => {
  436. const rootModule = concatConfiguration.rootModule;
  437. // Root already concatenated into a larger configuration: skip.
  438. // (Inner modules may still be shared, e.g. duplicated across entries.)
  439. if (usedModules.has(rootModule)) return callback();
  440. const modules = concatConfiguration.getModules();
  441. for (const m of modules) {
  442. usedModules.add(m);
  443. }
  444. // Create a new ConcatenatedModule
  445. const newModule = ConcatenatedModule.create(
  446. rootModule,
  447. modules,
  448. concatConfiguration.runtime,
  449. compilation,
  450. compiler.root,
  451. compilation.outputOptions.hashFunction
  452. );
  453. const build = () => {
  454. newModule.build(
  455. compilation.options,
  456. compilation,
  457. /** @type {EXPECTED_ANY} */
  458. (null),
  459. /** @type {EXPECTED_ANY} */
  460. (null),
  461. (err) => {
  462. if (err) {
  463. if (!err.module) {
  464. err.module = newModule;
  465. }
  466. return callback(err);
  467. }
  468. integrate();
  469. }
  470. );
  471. };
  472. const integrate = () => {
  473. if (backCompat) {
  474. ChunkGraph.setChunkGraphForModule(newModule, chunkGraph);
  475. ModuleGraph.setModuleGraphForModule(newModule, moduleGraph);
  476. }
  477. for (const warning of concatConfiguration.getWarningsSorted()) {
  478. moduleGraph
  479. .getOptimizationBailout(newModule)
  480. .push(formatBailoutWarning(warning[0], warning[1]));
  481. }
  482. moduleGraph.cloneModuleAttributes(rootModule, newModule);
  483. for (const m of modules) {
  484. // add to builtModules when one of the included modules was built
  485. if (compilation.builtModules.has(m)) {
  486. compilation.builtModules.add(newModule);
  487. }
  488. if (m !== rootModule) {
  489. // attach external references to the concatenated module too
  490. moduleGraph.copyOutgoingModuleConnections(
  491. m,
  492. newModule,
  493. (c) =>
  494. c.originModule === m &&
  495. !(
  496. c.dependency &&
  497. Dependency.canConcatenate(
  498. c.dependency,
  499. concatenateCommonJsModules
  500. ) &&
  501. modules.has(c.module)
  502. )
  503. );
  504. // remove module from chunk
  505. for (const chunk of chunkGraph.getModuleChunksIterable(
  506. rootModule
  507. )) {
  508. const sourceTypes = chunkGraph.getChunkModuleSourceTypes(
  509. chunk,
  510. m
  511. );
  512. if (
  513. sourceTypes.size === 1 &&
  514. sourceTypes.has(JAVASCRIPT_TYPE)
  515. ) {
  516. chunkGraph.disconnectChunkAndModule(chunk, m);
  517. } else {
  518. const newSourceTypes = new Set(sourceTypes);
  519. newSourceTypes.delete(JAVASCRIPT_TYPE);
  520. chunkGraph.setChunkModuleSourceTypes(
  521. chunk,
  522. m,
  523. newSourceTypes
  524. );
  525. }
  526. }
  527. }
  528. }
  529. compilation.modules.delete(rootModule);
  530. ChunkGraph.clearChunkGraphForModule(rootModule);
  531. ModuleGraph.clearModuleGraphForModule(rootModule);
  532. // remove module from chunk
  533. chunkGraph.replaceModule(rootModule, newModule);
  534. // replace module references with the concatenated module
  535. moduleGraph.moveModuleConnections(
  536. rootModule,
  537. newModule,
  538. (c) => {
  539. const otherModule =
  540. c.module === rootModule ? c.originModule : c.module;
  541. const innerConnection =
  542. c.dependency &&
  543. Dependency.canConcatenate(
  544. c.dependency,
  545. concatenateCommonJsModules
  546. ) &&
  547. modules.has(/** @type {Module} */ (otherModule));
  548. return !innerConnection;
  549. }
  550. );
  551. // add concatenated module to the compilation
  552. compilation.modules.add(newModule);
  553. callback();
  554. };
  555. build();
  556. },
  557. (err) => {
  558. logger.timeEnd("create concatenated modules");
  559. process.nextTick(callback.bind(null, err));
  560. }
  561. );
  562. }
  563. );
  564. });
  565. }
  566. /**
  567. * Checks whether the module `import defer`s a module already in the config.
  568. * @param {ModuleGraph} moduleGraph the module graph
  569. * @param {Module} module the candidate module
  570. * @param {ConcatConfiguration} config the concat configuration
  571. * @returns {boolean} true when a deferred import targets a module in the config
  572. */
  573. _defersModuleIn(moduleGraph, module, config) {
  574. for (const dep of module.dependencies) {
  575. if (
  576. dep instanceof HarmonyImportDependency &&
  577. ImportPhaseUtils.isDefer(dep.phase)
  578. ) {
  579. const target = moduleGraph.getModule(dep);
  580. if (target && config.has(target)) return true;
  581. }
  582. }
  583. return false;
  584. }
  585. /**
  586. * Returns the imported modules.
  587. * @param {Compilation} compilation the compilation
  588. * @param {Module} module the module to be added
  589. * @param {RuntimeSpec} runtime the runtime scope
  590. * @returns {Set<Module>} the imported modules
  591. */
  592. _getImports(compilation, module, runtime) {
  593. const moduleGraph = compilation.moduleGraph;
  594. const concatenateCommonJsModules = isCommonJsConcatenationEnabled(
  595. compilation.options.optimization.concatenateModules
  596. );
  597. /** @type {Set<Module>} */
  598. const set = new Set();
  599. for (const dep of module.dependencies) {
  600. // Get reference info only for dependencies that support concatenation
  601. if (!Dependency.canConcatenate(dep, concatenateCommonJsModules)) continue;
  602. const connection = moduleGraph.getConnection(dep);
  603. // Reference is valid and has a module
  604. if (
  605. !connection ||
  606. !connection.module ||
  607. !connection.isTargetActive(runtime)
  608. ) {
  609. continue;
  610. }
  611. const importedNames = compilation.getDependencyReferencedExports(
  612. dep,
  613. undefined
  614. );
  615. if (
  616. importedNames.every((i) =>
  617. Array.isArray(i) ? i.length > 0 : i.name.length > 0
  618. ) ||
  619. Array.isArray(moduleGraph.getProvidedExports(module))
  620. ) {
  621. set.add(connection.module);
  622. }
  623. }
  624. return set;
  625. }
  626. /**
  627. * Returns the problematic module.
  628. * @param {Compilation} compilation webpack compilation
  629. * @param {ConcatConfiguration} config concat configuration (will be modified when added)
  630. * @param {Module} module the module to be added
  631. * @param {RuntimeSpec} runtime the runtime scope of the generated code
  632. * @param {RuntimeSpec} activeRuntime the runtime scope of the root module
  633. * @param {Set<Module>} possibleModules modules that are candidates
  634. * @param {Set<Module>} candidates list of potential candidates (will be added to)
  635. * @param {Map<Module, Problem>} failureCache cache for problematic modules to be more performant
  636. * @param {Map<Module, RuntimeSpec>} moduleRuntimeCache memoized merged runtime per module, shared across roots
  637. * @param {ChunkGraph} chunkGraph the chunk graph
  638. * @param {boolean} avoidMutateOnFailure avoid mutating the config when adding fails
  639. * @param {Statistics} statistics gathering metrics
  640. * @returns {null | Problem} the problematic module
  641. */
  642. _tryToAdd(
  643. compilation,
  644. config,
  645. module,
  646. runtime,
  647. activeRuntime,
  648. possibleModules,
  649. candidates,
  650. failureCache,
  651. moduleRuntimeCache,
  652. chunkGraph,
  653. avoidMutateOnFailure,
  654. statistics
  655. ) {
  656. const cacheEntry = failureCache.get(module);
  657. if (cacheEntry) {
  658. statistics.cached++;
  659. return cacheEntry;
  660. }
  661. // Already added?
  662. if (config.has(module)) {
  663. statistics.alreadyInConfig++;
  664. return null;
  665. }
  666. // Not possible to add?
  667. if (!possibleModules.has(module)) {
  668. return cacheFailure(
  669. failureCache,
  670. statistics,
  671. "invalidModule",
  672. module,
  673. module
  674. );
  675. }
  676. // A deferred import must stay a concatenation boundary: concatenating a
  677. // module with a module it `import defer`s (a cycle) would erase the
  678. // deferred namespace's lazy-evaluation and exotic-object semantics.
  679. if (
  680. compilation.options.experiments.deferImport &&
  681. this._defersModuleIn(compilation.moduleGraph, module, config)
  682. ) {
  683. /**
  684. * @param {RequestShortener} requestShortener request shortener
  685. * @returns {string} problem description
  686. */
  687. const problem = (requestShortener) =>
  688. `Module ${module.readableIdentifier(
  689. requestShortener
  690. )} imports a module in this configuration via import defer`;
  691. return cacheFailure(
  692. failureCache,
  693. statistics,
  694. "incorrectDependency",
  695. module,
  696. problem
  697. );
  698. }
  699. // Module must be in the correct chunks
  700. const missingChunks = config.rootChunks.filter(
  701. (chunk) => !chunkGraph.isModuleInChunk(module, chunk)
  702. );
  703. if (missingChunks.length > 0) {
  704. /**
  705. * Returns problem description.
  706. * @param {RequestShortener} requestShortener request shortener
  707. * @returns {string} problem description
  708. */
  709. const problem = (requestShortener) => {
  710. const missingChunksList = [
  711. ...new Set(
  712. missingChunks.map((chunk) => chunk.name || "unnamed chunk(s)")
  713. )
  714. ].sort();
  715. const chunks = [
  716. ...new Set(
  717. [...chunkGraph.getModuleChunksIterable(module)].map(
  718. (chunk) => chunk.name || "unnamed chunk(s)"
  719. )
  720. )
  721. ].sort();
  722. return `Module ${module.readableIdentifier(
  723. requestShortener
  724. )} is not in the same chunk(s) (expected in chunk(s) ${missingChunksList.join(
  725. ", "
  726. )}, module is in chunk(s) ${chunks.join(", ")})`;
  727. };
  728. return cacheFailure(
  729. failureCache,
  730. statistics,
  731. "incorrectChunks",
  732. module,
  733. problem
  734. );
  735. }
  736. const moduleGraph = compilation.moduleGraph;
  737. const incomingConnections =
  738. moduleGraph.getIncomingConnectionsByOriginModule(module);
  739. const incomingConnectionsFromNonModules =
  740. incomingConnections.get(null) || incomingConnections.get(undefined);
  741. if (incomingConnectionsFromNonModules) {
  742. const activeNonModulesConnections =
  743. incomingConnectionsFromNonModules.filter((connection) =>
  744. // We are not interested in inactive connections
  745. // or connections without dependency
  746. connection.isActive(runtime)
  747. );
  748. if (activeNonModulesConnections.length > 0) {
  749. /**
  750. * Returns problem description.
  751. * @param {RequestShortener} requestShortener request shortener
  752. * @returns {string} problem description
  753. */
  754. const problem = (requestShortener) => {
  755. /** @type {Set<string>} */
  756. const importingExplanations = new Set(
  757. activeNonModulesConnections
  758. .map((c) => c.explanation)
  759. .filter(Boolean)
  760. );
  761. const explanations = [...importingExplanations].sort();
  762. return `Module ${module.readableIdentifier(
  763. requestShortener
  764. )} is referenced ${
  765. explanations.length > 0
  766. ? `by: ${explanations.join(", ")}`
  767. : "in an unsupported way"
  768. }`;
  769. };
  770. return cacheFailure(
  771. failureCache,
  772. statistics,
  773. "incorrectDependency",
  774. module,
  775. problem
  776. );
  777. }
  778. }
  779. /** @type {Map<Module, ReadonlyArray<InstanceType<ModuleGraph.ModuleGraphConnection>>>} */
  780. const incomingConnectionsFromModules = new Map();
  781. for (const [originModule, connections] of incomingConnections) {
  782. if (originModule) {
  783. // Ignore connection from orphan modules
  784. if (chunkGraph.getNumberOfModuleChunks(originModule) === 0) continue;
  785. // We don't care for connections from other runtimes
  786. const originRuntime = getMergedModuleRuntime(
  787. chunkGraph,
  788. moduleRuntimeCache,
  789. originModule
  790. );
  791. if (!intersectRuntime(runtime, originRuntime)) continue;
  792. // We are not interested in inactive connections
  793. const activeConnections = connections.filter((connection) =>
  794. connection.isActive(runtime)
  795. );
  796. if (activeConnections.length > 0) {
  797. incomingConnectionsFromModules.set(originModule, activeConnections);
  798. }
  799. }
  800. }
  801. const incomingModules = [...incomingConnectionsFromModules.keys()];
  802. // Module must be in the same chunks like the referencing module
  803. const otherChunkModules = incomingModules.filter((originModule) => {
  804. for (const chunk of config.rootChunks) {
  805. if (!chunkGraph.isModuleInChunk(originModule, chunk)) {
  806. return true;
  807. }
  808. }
  809. return false;
  810. });
  811. if (otherChunkModules.length > 0) {
  812. /**
  813. * Returns problem description.
  814. * @param {RequestShortener} requestShortener request shortener
  815. * @returns {string} problem description
  816. */
  817. const problem = (requestShortener) => {
  818. const names = otherChunkModules
  819. .map((m) => m.readableIdentifier(requestShortener))
  820. .sort();
  821. return `Module ${module.readableIdentifier(
  822. requestShortener
  823. )} is referenced from different chunks by these modules: ${names.join(
  824. ", "
  825. )}`;
  826. };
  827. return cacheFailure(
  828. failureCache,
  829. statistics,
  830. "incorrectChunksOfImporter",
  831. module,
  832. problem
  833. );
  834. }
  835. const concatenateCommonJsModules = isCommonJsConcatenationEnabled(
  836. compilation.options.optimization.concatenateModules
  837. );
  838. /** @type {Map<Module, ReadonlyArray<InstanceType<ModuleGraph.ModuleGraphConnection>>>} */
  839. const nonTargetConnections = new Map();
  840. for (const [originModule, connections] of incomingConnectionsFromModules) {
  841. const selected = connections.filter(
  842. (connection) =>
  843. !connection.dependency ||
  844. !Dependency.canConcatenate(
  845. connection.dependency,
  846. concatenateCommonJsModules
  847. )
  848. );
  849. if (selected.length > 0) {
  850. nonTargetConnections.set(originModule, selected);
  851. }
  852. }
  853. if (nonTargetConnections.size > 0) {
  854. /**
  855. * Returns problem description.
  856. * @param {RequestShortener} requestShortener request shortener
  857. * @returns {string} problem description
  858. */
  859. const problem = (requestShortener) => {
  860. const names = [...nonTargetConnections]
  861. .map(
  862. ([originModule, connections]) =>
  863. `${originModule.readableIdentifier(
  864. requestShortener
  865. )} (referenced with ${[
  866. ...new Set(
  867. connections
  868. .map((c) => c.dependency && c.dependency.type)
  869. .filter(Boolean)
  870. )
  871. ]
  872. .sort()
  873. .join(", ")})`
  874. )
  875. .sort();
  876. return `Module ${module.readableIdentifier(
  877. requestShortener
  878. )} is referenced from these modules with unsupported syntax: ${names.join(
  879. ", "
  880. )}`;
  881. };
  882. return cacheFailure(
  883. failureCache,
  884. statistics,
  885. "incorrectModuleDependency",
  886. module,
  887. problem
  888. );
  889. }
  890. if (runtime !== undefined && typeof runtime !== "string") {
  891. // Module must be consistently referenced in the same runtimes
  892. /** @type {{ originModule: Module, runtimeCondition: RuntimeSpec }[]} */
  893. const otherRuntimeConnections = [];
  894. outer: for (const [
  895. originModule,
  896. connections
  897. ] of incomingConnectionsFromModules) {
  898. /** @type {false | RuntimeSpec} */
  899. let currentRuntimeCondition = false;
  900. for (const connection of connections) {
  901. const runtimeCondition = filterRuntime(runtime, (runtime) =>
  902. connection.isTargetActive(runtime)
  903. );
  904. if (runtimeCondition === false) continue;
  905. if (runtimeCondition === true) continue outer;
  906. currentRuntimeCondition =
  907. currentRuntimeCondition !== false
  908. ? mergeRuntime(currentRuntimeCondition, runtimeCondition)
  909. : runtimeCondition;
  910. }
  911. if (currentRuntimeCondition !== false) {
  912. otherRuntimeConnections.push({
  913. originModule,
  914. runtimeCondition: currentRuntimeCondition
  915. });
  916. }
  917. }
  918. if (otherRuntimeConnections.length > 0) {
  919. /**
  920. * Returns problem description.
  921. * @param {RequestShortener} requestShortener request shortener
  922. * @returns {string} problem description
  923. */
  924. const problem = (requestShortener) =>
  925. `Module ${module.readableIdentifier(
  926. requestShortener
  927. )} is runtime-dependent referenced by these modules: ${Array.from(
  928. otherRuntimeConnections,
  929. ({ originModule, runtimeCondition }) =>
  930. `${originModule.readableIdentifier(
  931. requestShortener
  932. )} (expected runtime ${runtimeToString(
  933. runtime
  934. )}, module is only referenced in ${runtimeToString(
  935. /** @type {RuntimeSpec} */ (runtimeCondition)
  936. )})`
  937. ).join(", ")}`;
  938. return cacheFailure(
  939. failureCache,
  940. statistics,
  941. "incorrectRuntimeCondition",
  942. module,
  943. problem
  944. );
  945. }
  946. }
  947. /** @type {undefined | number} */
  948. let backup;
  949. if (avoidMutateOnFailure) {
  950. backup = config.snapshot();
  951. }
  952. // Add the module
  953. config.add(module);
  954. incomingModules.sort(compareModulesByIdentifier);
  955. // Every module which depends on the added module must be in the configuration too.
  956. for (const originModule of incomingModules) {
  957. const problem = this._tryToAdd(
  958. compilation,
  959. config,
  960. originModule,
  961. runtime,
  962. activeRuntime,
  963. possibleModules,
  964. candidates,
  965. failureCache,
  966. moduleRuntimeCache,
  967. chunkGraph,
  968. false,
  969. statistics
  970. );
  971. if (problem) {
  972. if (backup !== undefined) config.rollback(backup);
  973. return cacheFailure(
  974. failureCache,
  975. statistics,
  976. "importerFailed",
  977. module,
  978. problem
  979. );
  980. }
  981. }
  982. // Add imports to possible candidates list
  983. for (const imp of this._getImports(compilation, module, runtime)) {
  984. candidates.add(imp);
  985. }
  986. statistics.added++;
  987. return null;
  988. }
  989. }
  990. /** @typedef {Map<Module, Problem>} Warnings */
  991. class ConcatConfiguration {
  992. /**
  993. * Creates an instance of ConcatConfiguration.
  994. * @param {Module} rootModule the root module
  995. * @param {RuntimeSpec} runtime the runtime
  996. * @param {Chunk[]} rootChunks the chunks the root module is in
  997. */
  998. constructor(rootModule, runtime, rootChunks) {
  999. /** @type {Module} */
  1000. this.rootModule = rootModule;
  1001. /** @type {RuntimeSpec} */
  1002. this.runtime = runtime;
  1003. /** @type {Chunk[]} the root's chunks, reused for every candidate's chunk check */
  1004. this.rootChunks = rootChunks;
  1005. /** @type {Set<Module>} */
  1006. this.modules = new Set();
  1007. this.modules.add(rootModule);
  1008. /** @type {Warnings} */
  1009. this.warnings = new Map();
  1010. }
  1011. /**
  1012. * Processes the provided module.
  1013. * @param {Module} module the module
  1014. */
  1015. add(module) {
  1016. this.modules.add(module);
  1017. }
  1018. /**
  1019. * Returns true, when the module is in the module set.
  1020. * @param {Module} module the module
  1021. * @returns {boolean} true, when the module is in the module set
  1022. */
  1023. has(module) {
  1024. return this.modules.has(module);
  1025. }
  1026. isEmpty() {
  1027. return this.modules.size === 1;
  1028. }
  1029. /**
  1030. * Adds the provided module to the concat configuration.
  1031. * @param {Module} module the module
  1032. * @param {Problem} problem the problem
  1033. */
  1034. addWarning(module, problem) {
  1035. this.warnings.set(module, problem);
  1036. }
  1037. /**
  1038. * Gets warnings sorted.
  1039. * @returns {Warnings} warnings
  1040. */
  1041. getWarningsSorted() {
  1042. return new Map(
  1043. [...this.warnings].sort((a, b) => {
  1044. const ai = a[0].identifier();
  1045. const bi = b[0].identifier();
  1046. if (ai < bi) return -1;
  1047. if (ai > bi) return 1;
  1048. return 0;
  1049. })
  1050. );
  1051. }
  1052. /**
  1053. * Returns modules as set.
  1054. * @returns {Set<Module>} modules as set
  1055. */
  1056. getModules() {
  1057. return this.modules;
  1058. }
  1059. snapshot() {
  1060. return this.modules.size;
  1061. }
  1062. /**
  1063. * Processes the provided snapshot.
  1064. * @param {number} snapshot snapshot
  1065. */
  1066. rollback(snapshot) {
  1067. const modules = this.modules;
  1068. for (const m of modules) {
  1069. if (snapshot === 0) {
  1070. modules.delete(m);
  1071. } else {
  1072. snapshot--;
  1073. }
  1074. }
  1075. }
  1076. }
  1077. ModuleConcatenationPlugin.BAILOUT_PREFIX = BAILOUT_PREFIX;
  1078. ModuleConcatenationPlugin.REJECTED_PREFIX = REJECTED_PREFIX;
  1079. module.exports = ModuleConcatenationPlugin;