SplitChunksPlugin.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const crypto = require("crypto");
  7. const SortableSet = require("../util/SortableSet");
  8. const GraphHelpers = require("../GraphHelpers");
  9. const { isSubset } = require("../util/SetHelpers");
  10. const deterministicGrouping = require("../util/deterministicGrouping");
  11. const MinMaxSizeWarning = require("./MinMaxSizeWarning");
  12. const contextify = require("../util/identifier").contextify;
  13. /** @typedef {import("../Compiler")} Compiler */
  14. /** @typedef {import("../Chunk")} Chunk */
  15. /** @typedef {import("../Module")} Module */
  16. /** @typedef {import("../util/deterministicGrouping").Options<Module>} DeterministicGroupingOptionsForModule */
  17. /** @typedef {import("../util/deterministicGrouping").GroupedItems<Module>} DeterministicGroupingGroupedItemsForModule */
  18. const deterministicGroupingForModules = /** @type {function(DeterministicGroupingOptionsForModule): DeterministicGroupingGroupedItemsForModule[]} */ (deterministicGrouping);
  19. const hashFilename = name => {
  20. return crypto
  21. .createHash("md4")
  22. .update(name)
  23. .digest("hex")
  24. .slice(0, 8);
  25. };
  26. const sortByIdentifier = (a, b) => {
  27. if (a.identifier() > b.identifier()) return 1;
  28. if (a.identifier() < b.identifier()) return -1;
  29. return 0;
  30. };
  31. const getRequests = chunk => {
  32. let requests = 0;
  33. for (const chunkGroup of chunk.groupsIterable) {
  34. requests = Math.max(requests, chunkGroup.chunks.length);
  35. }
  36. return requests;
  37. };
  38. const getModulesSize = modules => {
  39. let sum = 0;
  40. for (const m of modules) {
  41. sum += m.size();
  42. }
  43. return sum;
  44. };
  45. /**
  46. * @template T
  47. * @param {Set<T>} a set
  48. * @param {Set<T>} b other set
  49. * @returns {boolean} true if at least one item of a is in b
  50. */
  51. const isOverlap = (a, b) => {
  52. for (const item of a) {
  53. if (b.has(item)) return true;
  54. }
  55. return false;
  56. };
  57. const compareEntries = (a, b) => {
  58. // 1. by priority
  59. const diffPriority = a.cacheGroup.priority - b.cacheGroup.priority;
  60. if (diffPriority) return diffPriority;
  61. // 2. by number of chunks
  62. const diffCount = a.chunks.size - b.chunks.size;
  63. if (diffCount) return diffCount;
  64. // 3. by size reduction
  65. const aSizeReduce = a.size * (a.chunks.size - 1);
  66. const bSizeReduce = b.size * (b.chunks.size - 1);
  67. const diffSizeReduce = aSizeReduce - bSizeReduce;
  68. if (diffSizeReduce) return diffSizeReduce;
  69. // 4. by cache group index
  70. const indexDiff = a.cacheGroupIndex - b.cacheGroupIndex;
  71. if (indexDiff) return indexDiff;
  72. // 5. by number of modules (to be able to compare by identifier)
  73. const modulesA = a.modules;
  74. const modulesB = b.modules;
  75. const diff = modulesA.size - modulesB.size;
  76. if (diff) return diff;
  77. // 6. by module identifiers
  78. modulesA.sort();
  79. modulesB.sort();
  80. const aI = modulesA[Symbol.iterator]();
  81. const bI = modulesB[Symbol.iterator]();
  82. // eslint-disable-next-line no-constant-condition
  83. while (true) {
  84. const aItem = aI.next();
  85. const bItem = bI.next();
  86. if (aItem.done) return 0;
  87. const aModuleIdentifier = aItem.value.identifier();
  88. const bModuleIdentifier = bItem.value.identifier();
  89. if (aModuleIdentifier > bModuleIdentifier) return -1;
  90. if (aModuleIdentifier < bModuleIdentifier) return 1;
  91. }
  92. };
  93. const compareNumbers = (a, b) => a - b;
  94. const INITIAL_CHUNK_FILTER = chunk => chunk.canBeInitial();
  95. const ASYNC_CHUNK_FILTER = chunk => !chunk.canBeInitial();
  96. const ALL_CHUNK_FILTER = chunk => true;
  97. module.exports = class SplitChunksPlugin {
  98. constructor(options) {
  99. this.options = SplitChunksPlugin.normalizeOptions(options);
  100. }
  101. static normalizeOptions(options = {}) {
  102. return {
  103. chunksFilter: SplitChunksPlugin.normalizeChunksFilter(
  104. options.chunks || "all"
  105. ),
  106. minSize: options.minSize || 0,
  107. enforceSizeThreshold: options.enforceSizeThreshold || 0,
  108. maxSize: options.maxSize || 0,
  109. minChunks: options.minChunks || 1,
  110. maxAsyncRequests: options.maxAsyncRequests || 1,
  111. maxInitialRequests: options.maxInitialRequests || 1,
  112. hidePathInfo: options.hidePathInfo || false,
  113. filename: options.filename || undefined,
  114. getCacheGroups: SplitChunksPlugin.normalizeCacheGroups({
  115. cacheGroups: options.cacheGroups,
  116. name: options.name,
  117. automaticNameDelimiter: options.automaticNameDelimiter,
  118. automaticNameMaxLength: options.automaticNameMaxLength
  119. }),
  120. automaticNameDelimiter: options.automaticNameDelimiter,
  121. automaticNameMaxLength: options.automaticNameMaxLength || 109,
  122. fallbackCacheGroup: SplitChunksPlugin.normalizeFallbackCacheGroup(
  123. options.fallbackCacheGroup || {},
  124. options
  125. )
  126. };
  127. }
  128. static normalizeName({
  129. name,
  130. automaticNameDelimiter,
  131. automaticNamePrefix,
  132. automaticNameMaxLength
  133. }) {
  134. if (name === true) {
  135. /** @type {WeakMap<Chunk[], Record<string, string>>} */
  136. const cache = new WeakMap();
  137. const fn = (module, chunks, cacheGroup) => {
  138. let cacheEntry = cache.get(chunks);
  139. if (cacheEntry === undefined) {
  140. cacheEntry = {};
  141. cache.set(chunks, cacheEntry);
  142. } else if (cacheGroup in cacheEntry) {
  143. return cacheEntry[cacheGroup];
  144. }
  145. const names = chunks.map(c => c.name);
  146. if (!names.every(Boolean)) {
  147. cacheEntry[cacheGroup] = undefined;
  148. return;
  149. }
  150. names.sort();
  151. const prefix =
  152. typeof automaticNamePrefix === "string"
  153. ? automaticNamePrefix
  154. : cacheGroup;
  155. const namePrefix = prefix ? prefix + automaticNameDelimiter : "";
  156. let name = namePrefix + names.join(automaticNameDelimiter);
  157. // Filenames and paths can't be too long otherwise an
  158. // ENAMETOOLONG error is raised. If the generated name if too
  159. // long, it is truncated and a hash is appended. The limit has
  160. // been set to 109 to prevent `[name].[chunkhash].[ext]` from
  161. // generating a 256+ character string.
  162. if (name.length > automaticNameMaxLength) {
  163. const hashedFilename = hashFilename(name);
  164. const sliceLength =
  165. automaticNameMaxLength -
  166. (automaticNameDelimiter.length + hashedFilename.length);
  167. name =
  168. name.slice(0, sliceLength) +
  169. automaticNameDelimiter +
  170. hashedFilename;
  171. }
  172. cacheEntry[cacheGroup] = name;
  173. return name;
  174. };
  175. return fn;
  176. }
  177. if (typeof name === "string") {
  178. const fn = () => {
  179. return name;
  180. };
  181. return fn;
  182. }
  183. if (typeof name === "function") return name;
  184. }
  185. static normalizeChunksFilter(chunks) {
  186. if (chunks === "initial") {
  187. return INITIAL_CHUNK_FILTER;
  188. }
  189. if (chunks === "async") {
  190. return ASYNC_CHUNK_FILTER;
  191. }
  192. if (chunks === "all") {
  193. return ALL_CHUNK_FILTER;
  194. }
  195. if (typeof chunks === "function") return chunks;
  196. }
  197. static normalizeFallbackCacheGroup(
  198. {
  199. minSize = undefined,
  200. maxSize = undefined,
  201. automaticNameDelimiter = undefined
  202. },
  203. {
  204. minSize: defaultMinSize = undefined,
  205. maxSize: defaultMaxSize = undefined,
  206. automaticNameDelimiter: defaultAutomaticNameDelimiter = undefined
  207. }
  208. ) {
  209. return {
  210. minSize: typeof minSize === "number" ? minSize : defaultMinSize || 0,
  211. maxSize: typeof maxSize === "number" ? maxSize : defaultMaxSize || 0,
  212. automaticNameDelimiter:
  213. automaticNameDelimiter || defaultAutomaticNameDelimiter || "~"
  214. };
  215. }
  216. static normalizeCacheGroups({
  217. cacheGroups,
  218. name,
  219. automaticNameDelimiter,
  220. automaticNameMaxLength
  221. }) {
  222. if (typeof cacheGroups === "function") {
  223. // TODO webpack 5 remove this
  224. if (cacheGroups.length !== 1) {
  225. return module => cacheGroups(module, module.getChunks());
  226. }
  227. return cacheGroups;
  228. }
  229. if (cacheGroups && typeof cacheGroups === "object") {
  230. const fn = module => {
  231. let results;
  232. for (const key of Object.keys(cacheGroups)) {
  233. let option = cacheGroups[key];
  234. if (option === false) continue;
  235. if (option instanceof RegExp || typeof option === "string") {
  236. option = {
  237. test: option
  238. };
  239. }
  240. if (typeof option === "function") {
  241. let result = option(module);
  242. if (result) {
  243. if (results === undefined) results = [];
  244. for (const r of Array.isArray(result) ? result : [result]) {
  245. const result = Object.assign({ key }, r);
  246. if (result.name) result.getName = () => result.name;
  247. if (result.chunks) {
  248. result.chunksFilter = SplitChunksPlugin.normalizeChunksFilter(
  249. result.chunks
  250. );
  251. }
  252. results.push(result);
  253. }
  254. }
  255. } else if (SplitChunksPlugin.checkTest(option.test, module)) {
  256. if (results === undefined) results = [];
  257. results.push({
  258. key: key,
  259. priority: option.priority,
  260. getName:
  261. SplitChunksPlugin.normalizeName({
  262. name: option.name || name,
  263. automaticNameDelimiter:
  264. typeof option.automaticNameDelimiter === "string"
  265. ? option.automaticNameDelimiter
  266. : automaticNameDelimiter,
  267. automaticNamePrefix: option.automaticNamePrefix,
  268. automaticNameMaxLength:
  269. option.automaticNameMaxLength || automaticNameMaxLength
  270. }) || (() => {}),
  271. chunksFilter: SplitChunksPlugin.normalizeChunksFilter(
  272. option.chunks
  273. ),
  274. enforce: option.enforce,
  275. minSize: option.minSize,
  276. enforceSizeThreshold: option.enforceSizeThreshold,
  277. maxSize: option.maxSize,
  278. minChunks: option.minChunks,
  279. maxAsyncRequests: option.maxAsyncRequests,
  280. maxInitialRequests: option.maxInitialRequests,
  281. filename: option.filename,
  282. reuseExistingChunk: option.reuseExistingChunk
  283. });
  284. }
  285. }
  286. return results;
  287. };
  288. return fn;
  289. }
  290. const fn = () => {};
  291. return fn;
  292. }
  293. static checkTest(test, module) {
  294. if (test === undefined) return true;
  295. if (typeof test === "function") {
  296. if (test.length !== 1) {
  297. return test(module, module.getChunks());
  298. }
  299. return test(module);
  300. }
  301. if (typeof test === "boolean") return test;
  302. if (typeof test === "string") {
  303. if (
  304. module.nameForCondition &&
  305. module.nameForCondition().startsWith(test)
  306. ) {
  307. return true;
  308. }
  309. for (const chunk of module.chunksIterable) {
  310. if (chunk.name && chunk.name.startsWith(test)) {
  311. return true;
  312. }
  313. }
  314. return false;
  315. }
  316. if (test instanceof RegExp) {
  317. if (module.nameForCondition && test.test(module.nameForCondition())) {
  318. return true;
  319. }
  320. for (const chunk of module.chunksIterable) {
  321. if (chunk.name && test.test(chunk.name)) {
  322. return true;
  323. }
  324. }
  325. return false;
  326. }
  327. return false;
  328. }
  329. /**
  330. * @param {Compiler} compiler webpack compiler
  331. * @returns {void}
  332. */
  333. apply(compiler) {
  334. compiler.hooks.thisCompilation.tap("SplitChunksPlugin", compilation => {
  335. let alreadyOptimized = false;
  336. compilation.hooks.unseal.tap("SplitChunksPlugin", () => {
  337. alreadyOptimized = false;
  338. });
  339. compilation.hooks.optimizeChunksAdvanced.tap(
  340. "SplitChunksPlugin",
  341. chunks => {
  342. if (alreadyOptimized) return;
  343. alreadyOptimized = true;
  344. // Give each selected chunk an index (to create strings from chunks)
  345. const indexMap = new Map();
  346. let index = 1;
  347. for (const chunk of chunks) {
  348. indexMap.set(chunk, index++);
  349. }
  350. const getKey = chunks => {
  351. return Array.from(chunks, c => indexMap.get(c))
  352. .sort(compareNumbers)
  353. .join();
  354. };
  355. /** @type {Map<string, Set<Chunk>>} */
  356. const chunkSetsInGraph = new Map();
  357. for (const module of compilation.modules) {
  358. const chunksKey = getKey(module.chunksIterable);
  359. if (!chunkSetsInGraph.has(chunksKey)) {
  360. chunkSetsInGraph.set(chunksKey, new Set(module.chunksIterable));
  361. }
  362. }
  363. // group these set of chunks by count
  364. // to allow to check less sets via isSubset
  365. // (only smaller sets can be subset)
  366. /** @type {Map<number, Array<Set<Chunk>>>} */
  367. const chunkSetsByCount = new Map();
  368. for (const chunksSet of chunkSetsInGraph.values()) {
  369. const count = chunksSet.size;
  370. let array = chunkSetsByCount.get(count);
  371. if (array === undefined) {
  372. array = [];
  373. chunkSetsByCount.set(count, array);
  374. }
  375. array.push(chunksSet);
  376. }
  377. // Create a list of possible combinations
  378. const combinationsCache = new Map(); // Map<string, Set<Chunk>[]>
  379. const getCombinations = key => {
  380. const chunksSet = chunkSetsInGraph.get(key);
  381. var array = [chunksSet];
  382. if (chunksSet.size > 1) {
  383. for (const [count, setArray] of chunkSetsByCount) {
  384. // "equal" is not needed because they would have been merge in the first step
  385. if (count < chunksSet.size) {
  386. for (const set of setArray) {
  387. if (isSubset(chunksSet, set)) {
  388. array.push(set);
  389. }
  390. }
  391. }
  392. }
  393. }
  394. return array;
  395. };
  396. /**
  397. * @typedef {Object} SelectedChunksResult
  398. * @property {Chunk[]} chunks the list of chunks
  399. * @property {string} key a key of the list
  400. */
  401. /**
  402. * @typedef {function(Chunk): boolean} ChunkFilterFunction
  403. */
  404. /** @type {WeakMap<Set<Chunk>, WeakMap<ChunkFilterFunction, SelectedChunksResult>>} */
  405. const selectedChunksCacheByChunksSet = new WeakMap();
  406. /**
  407. * get list and key by applying the filter function to the list
  408. * It is cached for performance reasons
  409. * @param {Set<Chunk>} chunks list of chunks
  410. * @param {ChunkFilterFunction} chunkFilter filter function for chunks
  411. * @returns {SelectedChunksResult} list and key
  412. */
  413. const getSelectedChunks = (chunks, chunkFilter) => {
  414. let entry = selectedChunksCacheByChunksSet.get(chunks);
  415. if (entry === undefined) {
  416. entry = new WeakMap();
  417. selectedChunksCacheByChunksSet.set(chunks, entry);
  418. }
  419. /** @type {SelectedChunksResult} */
  420. let entry2 = entry.get(chunkFilter);
  421. if (entry2 === undefined) {
  422. /** @type {Chunk[]} */
  423. const selectedChunks = [];
  424. for (const chunk of chunks) {
  425. if (chunkFilter(chunk)) selectedChunks.push(chunk);
  426. }
  427. entry2 = {
  428. chunks: selectedChunks,
  429. key: getKey(selectedChunks)
  430. };
  431. entry.set(chunkFilter, entry2);
  432. }
  433. return entry2;
  434. };
  435. /**
  436. * @typedef {Object} ChunksInfoItem
  437. * @property {SortableSet} modules
  438. * @property {TODO} cacheGroup
  439. * @property {number} cacheGroupIndex
  440. * @property {string} name
  441. * @property {number} size
  442. * @property {Set<Chunk>} chunks
  443. * @property {Set<Chunk>} reuseableChunks
  444. * @property {Set<string>} chunksKeys
  445. */
  446. // Map a list of chunks to a list of modules
  447. // For the key the chunk "index" is used, the value is a SortableSet of modules
  448. /** @type {Map<string, ChunksInfoItem>} */
  449. const chunksInfoMap = new Map();
  450. /**
  451. * @param {TODO} cacheGroup the current cache group
  452. * @param {number} cacheGroupIndex the index of the cache group of ordering
  453. * @param {Chunk[]} selectedChunks chunks selected for this module
  454. * @param {string} selectedChunksKey a key of selectedChunks
  455. * @param {Module} module the current module
  456. * @returns {void}
  457. */
  458. const addModuleToChunksInfoMap = (
  459. cacheGroup,
  460. cacheGroupIndex,
  461. selectedChunks,
  462. selectedChunksKey,
  463. module
  464. ) => {
  465. // Break if minimum number of chunks is not reached
  466. if (selectedChunks.length < cacheGroup.minChunks) return;
  467. // Determine name for split chunk
  468. const name = cacheGroup.getName(
  469. module,
  470. selectedChunks,
  471. cacheGroup.key
  472. );
  473. // Create key for maps
  474. // When it has a name we use the name as key
  475. // Elsewise we create the key from chunks and cache group key
  476. // This automatically merges equal names
  477. const key =
  478. cacheGroup.key +
  479. (name ? ` name:${name}` : ` chunks:${selectedChunksKey}`);
  480. // Add module to maps
  481. let info = chunksInfoMap.get(key);
  482. if (info === undefined) {
  483. chunksInfoMap.set(
  484. key,
  485. (info = {
  486. modules: new SortableSet(undefined, sortByIdentifier),
  487. cacheGroup,
  488. cacheGroupIndex,
  489. name,
  490. size: 0,
  491. chunks: new Set(),
  492. reuseableChunks: new Set(),
  493. chunksKeys: new Set()
  494. })
  495. );
  496. }
  497. info.modules.add(module);
  498. info.size += module.size();
  499. if (!info.chunksKeys.has(selectedChunksKey)) {
  500. info.chunksKeys.add(selectedChunksKey);
  501. for (const chunk of selectedChunks) {
  502. info.chunks.add(chunk);
  503. }
  504. }
  505. };
  506. // Walk through all modules
  507. for (const module of compilation.modules) {
  508. // Get cache group
  509. let cacheGroups = this.options.getCacheGroups(module);
  510. if (!Array.isArray(cacheGroups) || cacheGroups.length === 0) {
  511. continue;
  512. }
  513. // Prepare some values
  514. const chunksKey = getKey(module.chunksIterable);
  515. let combs = combinationsCache.get(chunksKey);
  516. if (combs === undefined) {
  517. combs = getCombinations(chunksKey);
  518. combinationsCache.set(chunksKey, combs);
  519. }
  520. let cacheGroupIndex = 0;
  521. for (const cacheGroupSource of cacheGroups) {
  522. const minSize =
  523. cacheGroupSource.minSize !== undefined
  524. ? cacheGroupSource.minSize
  525. : cacheGroupSource.enforce
  526. ? 0
  527. : this.options.minSize;
  528. const enforceSizeThreshold =
  529. cacheGroupSource.enforceSizeThreshold !== undefined
  530. ? cacheGroupSource.enforceSizeThreshold
  531. : cacheGroupSource.enforce
  532. ? 0
  533. : this.options.enforceSizeThreshold;
  534. const cacheGroup = {
  535. key: cacheGroupSource.key,
  536. priority: cacheGroupSource.priority || 0,
  537. chunksFilter:
  538. cacheGroupSource.chunksFilter || this.options.chunksFilter,
  539. minSize,
  540. minSizeForMaxSize:
  541. cacheGroupSource.minSize !== undefined
  542. ? cacheGroupSource.minSize
  543. : this.options.minSize,
  544. enforceSizeThreshold,
  545. maxSize:
  546. cacheGroupSource.maxSize !== undefined
  547. ? cacheGroupSource.maxSize
  548. : cacheGroupSource.enforce
  549. ? 0
  550. : this.options.maxSize,
  551. minChunks:
  552. cacheGroupSource.minChunks !== undefined
  553. ? cacheGroupSource.minChunks
  554. : cacheGroupSource.enforce
  555. ? 1
  556. : this.options.minChunks,
  557. maxAsyncRequests:
  558. cacheGroupSource.maxAsyncRequests !== undefined
  559. ? cacheGroupSource.maxAsyncRequests
  560. : cacheGroupSource.enforce
  561. ? Infinity
  562. : this.options.maxAsyncRequests,
  563. maxInitialRequests:
  564. cacheGroupSource.maxInitialRequests !== undefined
  565. ? cacheGroupSource.maxInitialRequests
  566. : cacheGroupSource.enforce
  567. ? Infinity
  568. : this.options.maxInitialRequests,
  569. getName:
  570. cacheGroupSource.getName !== undefined
  571. ? cacheGroupSource.getName
  572. : this.options.getName,
  573. filename:
  574. cacheGroupSource.filename !== undefined
  575. ? cacheGroupSource.filename
  576. : this.options.filename,
  577. automaticNameDelimiter:
  578. cacheGroupSource.automaticNameDelimiter !== undefined
  579. ? cacheGroupSource.automaticNameDelimiter
  580. : this.options.automaticNameDelimiter,
  581. reuseExistingChunk: cacheGroupSource.reuseExistingChunk,
  582. _validateSize: minSize > 0,
  583. _conditionalEnforce: enforceSizeThreshold > 0
  584. };
  585. // For all combination of chunk selection
  586. for (const chunkCombination of combs) {
  587. // Break if minimum number of chunks is not reached
  588. if (chunkCombination.size < cacheGroup.minChunks) continue;
  589. // Select chunks by configuration
  590. const {
  591. chunks: selectedChunks,
  592. key: selectedChunksKey
  593. } = getSelectedChunks(
  594. chunkCombination,
  595. cacheGroup.chunksFilter
  596. );
  597. addModuleToChunksInfoMap(
  598. cacheGroup,
  599. cacheGroupIndex,
  600. selectedChunks,
  601. selectedChunksKey,
  602. module
  603. );
  604. }
  605. cacheGroupIndex++;
  606. }
  607. }
  608. // Filter items were size < minSize
  609. for (const pair of chunksInfoMap) {
  610. const info = pair[1];
  611. if (
  612. info.cacheGroup._validateSize &&
  613. info.size < info.cacheGroup.minSize
  614. ) {
  615. chunksInfoMap.delete(pair[0]);
  616. }
  617. }
  618. /** @type {Map<Chunk, {minSize: number, maxSize: number, automaticNameDelimiter: string, keys: string[]}>} */
  619. const maxSizeQueueMap = new Map();
  620. while (chunksInfoMap.size > 0) {
  621. // Find best matching entry
  622. let bestEntryKey;
  623. let bestEntry;
  624. for (const pair of chunksInfoMap) {
  625. const key = pair[0];
  626. const info = pair[1];
  627. if (bestEntry === undefined) {
  628. bestEntry = info;
  629. bestEntryKey = key;
  630. } else if (compareEntries(bestEntry, info) < 0) {
  631. bestEntry = info;
  632. bestEntryKey = key;
  633. }
  634. }
  635. const item = bestEntry;
  636. chunksInfoMap.delete(bestEntryKey);
  637. let chunkName = item.name;
  638. // Variable for the new chunk (lazy created)
  639. /** @type {Chunk} */
  640. let newChunk;
  641. // When no chunk name, check if we can reuse a chunk instead of creating a new one
  642. let isReused = false;
  643. if (item.cacheGroup.reuseExistingChunk) {
  644. outer: for (const chunk of item.chunks) {
  645. if (chunk.getNumberOfModules() !== item.modules.size) continue;
  646. if (chunk.hasEntryModule()) continue;
  647. for (const module of item.modules) {
  648. if (!chunk.containsModule(module)) continue outer;
  649. }
  650. if (!newChunk || !newChunk.name) {
  651. newChunk = chunk;
  652. } else if (
  653. chunk.name &&
  654. chunk.name.length < newChunk.name.length
  655. ) {
  656. newChunk = chunk;
  657. } else if (
  658. chunk.name &&
  659. chunk.name.length === newChunk.name.length &&
  660. chunk.name < newChunk.name
  661. ) {
  662. newChunk = chunk;
  663. }
  664. chunkName = undefined;
  665. isReused = true;
  666. }
  667. }
  668. // Check if maxRequests condition can be fulfilled
  669. const selectedChunks = Array.from(item.chunks).filter(chunk => {
  670. // skip if we address ourself
  671. return (
  672. (!chunkName || chunk.name !== chunkName) && chunk !== newChunk
  673. );
  674. });
  675. const enforced =
  676. item.cacheGroup._conditionalEnforce &&
  677. item.size >= item.cacheGroup.enforceSizeThreshold;
  678. // Skip when no chunk selected
  679. if (selectedChunks.length === 0) continue;
  680. const usedChunks = new Set(selectedChunks);
  681. // Check if maxRequests condition can be fulfilled
  682. if (
  683. !enforced &&
  684. (Number.isFinite(item.cacheGroup.maxInitialRequests) ||
  685. Number.isFinite(item.cacheGroup.maxAsyncRequests))
  686. ) {
  687. for (const chunk of usedChunks) {
  688. // respect max requests
  689. const maxRequests = chunk.isOnlyInitial()
  690. ? item.cacheGroup.maxInitialRequests
  691. : chunk.canBeInitial()
  692. ? Math.min(
  693. item.cacheGroup.maxInitialRequests,
  694. item.cacheGroup.maxAsyncRequests
  695. )
  696. : item.cacheGroup.maxAsyncRequests;
  697. if (
  698. isFinite(maxRequests) &&
  699. getRequests(chunk) >= maxRequests
  700. ) {
  701. usedChunks.delete(chunk);
  702. }
  703. }
  704. }
  705. outer: for (const chunk of usedChunks) {
  706. for (const module of item.modules) {
  707. if (chunk.containsModule(module)) continue outer;
  708. }
  709. usedChunks.delete(chunk);
  710. }
  711. // Were some (invalid) chunks removed from usedChunks?
  712. // => readd all modules to the queue, as things could have been changed
  713. if (usedChunks.size < selectedChunks.length) {
  714. if (usedChunks.size >= item.cacheGroup.minChunks) {
  715. const chunksArr = Array.from(usedChunks);
  716. for (const module of item.modules) {
  717. addModuleToChunksInfoMap(
  718. item.cacheGroup,
  719. item.cacheGroupIndex,
  720. chunksArr,
  721. getKey(usedChunks),
  722. module
  723. );
  724. }
  725. }
  726. continue;
  727. }
  728. // Create the new chunk if not reusing one
  729. if (!isReused) {
  730. newChunk = compilation.addChunk(chunkName);
  731. }
  732. // Walk through all chunks
  733. for (const chunk of usedChunks) {
  734. // Add graph connections for splitted chunk
  735. chunk.split(newChunk);
  736. }
  737. // Add a note to the chunk
  738. newChunk.chunkReason = isReused
  739. ? "reused as split chunk"
  740. : "split chunk";
  741. if (item.cacheGroup.key) {
  742. newChunk.chunkReason += ` (cache group: ${item.cacheGroup.key})`;
  743. }
  744. if (chunkName) {
  745. newChunk.chunkReason += ` (name: ${chunkName})`;
  746. // If the chosen name is already an entry point we remove the entry point
  747. const entrypoint = compilation.entrypoints.get(chunkName);
  748. if (entrypoint) {
  749. compilation.entrypoints.delete(chunkName);
  750. entrypoint.remove();
  751. newChunk.entryModule = undefined;
  752. }
  753. }
  754. if (item.cacheGroup.filename) {
  755. if (!newChunk.isOnlyInitial()) {
  756. throw new Error(
  757. "SplitChunksPlugin: You are trying to set a filename for a chunk which is (also) loaded on demand. " +
  758. "The runtime can only handle loading of chunks which match the chunkFilename schema. " +
  759. "Using a custom filename would fail at runtime. " +
  760. `(cache group: ${item.cacheGroup.key})`
  761. );
  762. }
  763. newChunk.filenameTemplate = item.cacheGroup.filename;
  764. }
  765. if (!isReused) {
  766. // Add all modules to the new chunk
  767. for (const module of item.modules) {
  768. if (typeof module.chunkCondition === "function") {
  769. if (!module.chunkCondition(newChunk)) continue;
  770. }
  771. // Add module to new chunk
  772. GraphHelpers.connectChunkAndModule(newChunk, module);
  773. // Remove module from used chunks
  774. for (const chunk of usedChunks) {
  775. chunk.removeModule(module);
  776. module.rewriteChunkInReasons(chunk, [newChunk]);
  777. }
  778. }
  779. } else {
  780. // Remove all modules from used chunks
  781. for (const module of item.modules) {
  782. for (const chunk of usedChunks) {
  783. chunk.removeModule(module);
  784. module.rewriteChunkInReasons(chunk, [newChunk]);
  785. }
  786. }
  787. }
  788. if (item.cacheGroup.maxSize > 0) {
  789. const oldMaxSizeSettings = maxSizeQueueMap.get(newChunk);
  790. maxSizeQueueMap.set(newChunk, {
  791. minSize: Math.max(
  792. oldMaxSizeSettings ? oldMaxSizeSettings.minSize : 0,
  793. item.cacheGroup.minSizeForMaxSize
  794. ),
  795. maxSize: Math.min(
  796. oldMaxSizeSettings ? oldMaxSizeSettings.maxSize : Infinity,
  797. item.cacheGroup.maxSize
  798. ),
  799. automaticNameDelimiter: item.cacheGroup.automaticNameDelimiter,
  800. keys: oldMaxSizeSettings
  801. ? oldMaxSizeSettings.keys.concat(item.cacheGroup.key)
  802. : [item.cacheGroup.key]
  803. });
  804. }
  805. // remove all modules from other entries and update size
  806. for (const [key, info] of chunksInfoMap) {
  807. if (isOverlap(info.chunks, usedChunks)) {
  808. // update modules and total size
  809. // may remove it from the map when < minSize
  810. const oldSize = info.modules.size;
  811. for (const module of item.modules) {
  812. info.modules.delete(module);
  813. }
  814. if (info.modules.size !== oldSize) {
  815. if (info.modules.size === 0) {
  816. chunksInfoMap.delete(key);
  817. continue;
  818. }
  819. info.size = getModulesSize(info.modules);
  820. if (
  821. info.cacheGroup._validateSize &&
  822. info.size < info.cacheGroup.minSize
  823. ) {
  824. chunksInfoMap.delete(key);
  825. }
  826. if (info.modules.size === 0) {
  827. chunksInfoMap.delete(key);
  828. }
  829. }
  830. }
  831. }
  832. }
  833. const incorrectMinMaxSizeSet = new Set();
  834. // Make sure that maxSize is fulfilled
  835. for (const chunk of compilation.chunks.slice()) {
  836. const { minSize, maxSize, automaticNameDelimiter, keys } =
  837. maxSizeQueueMap.get(chunk) || this.options.fallbackCacheGroup;
  838. if (!maxSize) continue;
  839. if (minSize > maxSize) {
  840. const warningKey = `${keys && keys.join()} ${minSize} ${maxSize}`;
  841. if (!incorrectMinMaxSizeSet.has(warningKey)) {
  842. incorrectMinMaxSizeSet.add(warningKey);
  843. compilation.warnings.push(
  844. new MinMaxSizeWarning(keys, minSize, maxSize)
  845. );
  846. }
  847. }
  848. const results = deterministicGroupingForModules({
  849. maxSize: Math.max(minSize, maxSize),
  850. minSize,
  851. items: chunk.modulesIterable,
  852. getKey(module) {
  853. const ident = contextify(
  854. compilation.options.context,
  855. module.identifier()
  856. );
  857. const name = module.nameForCondition
  858. ? contextify(
  859. compilation.options.context,
  860. module.nameForCondition()
  861. )
  862. : ident.replace(/^.*!|\?[^?!]*$/g, "");
  863. const fullKey =
  864. name + automaticNameDelimiter + hashFilename(ident);
  865. return fullKey.replace(/[\\/?]/g, "_");
  866. },
  867. getSize(module) {
  868. return module.size();
  869. }
  870. });
  871. results.sort((a, b) => {
  872. if (a.key < b.key) return -1;
  873. if (a.key > b.key) return 1;
  874. return 0;
  875. });
  876. for (let i = 0; i < results.length; i++) {
  877. const group = results[i];
  878. const key = this.options.hidePathInfo
  879. ? hashFilename(group.key)
  880. : group.key;
  881. let name = chunk.name
  882. ? chunk.name + automaticNameDelimiter + key
  883. : null;
  884. if (name && name.length > 100) {
  885. name =
  886. name.slice(0, 100) +
  887. automaticNameDelimiter +
  888. hashFilename(name);
  889. }
  890. let newPart;
  891. if (i !== results.length - 1) {
  892. newPart = compilation.addChunk(name);
  893. chunk.split(newPart);
  894. newPart.chunkReason = chunk.chunkReason;
  895. // Add all modules to the new chunk
  896. for (const module of group.items) {
  897. if (typeof module.chunkCondition === "function") {
  898. if (!module.chunkCondition(newPart)) continue;
  899. }
  900. // Add module to new chunk
  901. GraphHelpers.connectChunkAndModule(newPart, module);
  902. // Remove module from used chunks
  903. chunk.removeModule(module);
  904. module.rewriteChunkInReasons(chunk, [newPart]);
  905. }
  906. } else {
  907. // change the chunk to be a part
  908. newPart = chunk;
  909. chunk.name = name;
  910. }
  911. }
  912. }
  913. }
  914. );
  915. });
  916. }
  917. };