SplitChunksPlugin.js 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972
  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 = b.cacheGroupIndex - a.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. const oldSize = info.modules.size;
  498. info.modules.add(module);
  499. if (info.modules.size !== oldSize) {
  500. info.size += module.size();
  501. }
  502. const oldChunksKeysSize = info.chunksKeys.size;
  503. info.chunksKeys.add(selectedChunksKey);
  504. if (oldChunksKeysSize !== info.chunksKeys.size) {
  505. for (const chunk of selectedChunks) {
  506. info.chunks.add(chunk);
  507. }
  508. }
  509. };
  510. // Walk through all modules
  511. for (const module of compilation.modules) {
  512. // Get cache group
  513. let cacheGroups = this.options.getCacheGroups(module);
  514. if (!Array.isArray(cacheGroups) || cacheGroups.length === 0) {
  515. continue;
  516. }
  517. // Prepare some values
  518. const chunksKey = getKey(module.chunksIterable);
  519. let combs = combinationsCache.get(chunksKey);
  520. if (combs === undefined) {
  521. combs = getCombinations(chunksKey);
  522. combinationsCache.set(chunksKey, combs);
  523. }
  524. let cacheGroupIndex = 0;
  525. for (const cacheGroupSource of cacheGroups) {
  526. const minSize =
  527. cacheGroupSource.minSize !== undefined
  528. ? cacheGroupSource.minSize
  529. : cacheGroupSource.enforce
  530. ? 0
  531. : this.options.minSize;
  532. const enforceSizeThreshold =
  533. cacheGroupSource.enforceSizeThreshold !== undefined
  534. ? cacheGroupSource.enforceSizeThreshold
  535. : cacheGroupSource.enforce
  536. ? 0
  537. : this.options.enforceSizeThreshold;
  538. const cacheGroup = {
  539. key: cacheGroupSource.key,
  540. priority: cacheGroupSource.priority || 0,
  541. chunksFilter:
  542. cacheGroupSource.chunksFilter || this.options.chunksFilter,
  543. minSize,
  544. minSizeForMaxSize:
  545. cacheGroupSource.minSize !== undefined
  546. ? cacheGroupSource.minSize
  547. : this.options.minSize,
  548. enforceSizeThreshold,
  549. maxSize:
  550. cacheGroupSource.maxSize !== undefined
  551. ? cacheGroupSource.maxSize
  552. : cacheGroupSource.enforce
  553. ? 0
  554. : this.options.maxSize,
  555. minChunks:
  556. cacheGroupSource.minChunks !== undefined
  557. ? cacheGroupSource.minChunks
  558. : cacheGroupSource.enforce
  559. ? 1
  560. : this.options.minChunks,
  561. maxAsyncRequests:
  562. cacheGroupSource.maxAsyncRequests !== undefined
  563. ? cacheGroupSource.maxAsyncRequests
  564. : cacheGroupSource.enforce
  565. ? Infinity
  566. : this.options.maxAsyncRequests,
  567. maxInitialRequests:
  568. cacheGroupSource.maxInitialRequests !== undefined
  569. ? cacheGroupSource.maxInitialRequests
  570. : cacheGroupSource.enforce
  571. ? Infinity
  572. : this.options.maxInitialRequests,
  573. getName:
  574. cacheGroupSource.getName !== undefined
  575. ? cacheGroupSource.getName
  576. : this.options.getName,
  577. filename:
  578. cacheGroupSource.filename !== undefined
  579. ? cacheGroupSource.filename
  580. : this.options.filename,
  581. automaticNameDelimiter:
  582. cacheGroupSource.automaticNameDelimiter !== undefined
  583. ? cacheGroupSource.automaticNameDelimiter
  584. : this.options.automaticNameDelimiter,
  585. reuseExistingChunk: cacheGroupSource.reuseExistingChunk,
  586. _validateSize: minSize > 0,
  587. _conditionalEnforce: enforceSizeThreshold > 0
  588. };
  589. // For all combination of chunk selection
  590. for (const chunkCombination of combs) {
  591. // Break if minimum number of chunks is not reached
  592. if (chunkCombination.size < cacheGroup.minChunks) continue;
  593. // Select chunks by configuration
  594. const {
  595. chunks: selectedChunks,
  596. key: selectedChunksKey
  597. } = getSelectedChunks(
  598. chunkCombination,
  599. cacheGroup.chunksFilter
  600. );
  601. addModuleToChunksInfoMap(
  602. cacheGroup,
  603. cacheGroupIndex,
  604. selectedChunks,
  605. selectedChunksKey,
  606. module
  607. );
  608. }
  609. cacheGroupIndex++;
  610. }
  611. }
  612. // Filter items were size < minSize
  613. for (const pair of chunksInfoMap) {
  614. const info = pair[1];
  615. if (
  616. info.cacheGroup._validateSize &&
  617. info.size < info.cacheGroup.minSize
  618. ) {
  619. chunksInfoMap.delete(pair[0]);
  620. }
  621. }
  622. /** @type {Map<Chunk, {minSize: number, maxSize: number, automaticNameDelimiter: string, keys: string[]}>} */
  623. const maxSizeQueueMap = new Map();
  624. while (chunksInfoMap.size > 0) {
  625. // Find best matching entry
  626. let bestEntryKey;
  627. let bestEntry;
  628. for (const pair of chunksInfoMap) {
  629. const key = pair[0];
  630. const info = pair[1];
  631. if (bestEntry === undefined) {
  632. bestEntry = info;
  633. bestEntryKey = key;
  634. } else if (compareEntries(bestEntry, info) < 0) {
  635. bestEntry = info;
  636. bestEntryKey = key;
  637. }
  638. }
  639. const item = bestEntry;
  640. chunksInfoMap.delete(bestEntryKey);
  641. let chunkName = item.name;
  642. // Variable for the new chunk (lazy created)
  643. /** @type {Chunk} */
  644. let newChunk;
  645. // When no chunk name, check if we can reuse a chunk instead of creating a new one
  646. let isReused = false;
  647. if (item.cacheGroup.reuseExistingChunk) {
  648. outer: for (const chunk of item.chunks) {
  649. if (chunk.getNumberOfModules() !== item.modules.size) continue;
  650. if (chunk.hasEntryModule()) continue;
  651. for (const module of item.modules) {
  652. if (!chunk.containsModule(module)) continue outer;
  653. }
  654. if (!newChunk || !newChunk.name) {
  655. newChunk = chunk;
  656. } else if (
  657. chunk.name &&
  658. chunk.name.length < newChunk.name.length
  659. ) {
  660. newChunk = chunk;
  661. } else if (
  662. chunk.name &&
  663. chunk.name.length === newChunk.name.length &&
  664. chunk.name < newChunk.name
  665. ) {
  666. newChunk = chunk;
  667. }
  668. chunkName = undefined;
  669. isReused = true;
  670. }
  671. }
  672. // Check if maxRequests condition can be fulfilled
  673. const selectedChunks = Array.from(item.chunks).filter(chunk => {
  674. // skip if we address ourself
  675. return (
  676. (!chunkName || chunk.name !== chunkName) && chunk !== newChunk
  677. );
  678. });
  679. const enforced =
  680. item.cacheGroup._conditionalEnforce &&
  681. item.size >= item.cacheGroup.enforceSizeThreshold;
  682. // Skip when no chunk selected
  683. if (selectedChunks.length === 0) continue;
  684. const usedChunks = new Set(selectedChunks);
  685. // Check if maxRequests condition can be fulfilled
  686. if (
  687. !enforced &&
  688. (Number.isFinite(item.cacheGroup.maxInitialRequests) ||
  689. Number.isFinite(item.cacheGroup.maxAsyncRequests))
  690. ) {
  691. for (const chunk of usedChunks) {
  692. // respect max requests
  693. const maxRequests = chunk.isOnlyInitial()
  694. ? item.cacheGroup.maxInitialRequests
  695. : chunk.canBeInitial()
  696. ? Math.min(
  697. item.cacheGroup.maxInitialRequests,
  698. item.cacheGroup.maxAsyncRequests
  699. )
  700. : item.cacheGroup.maxAsyncRequests;
  701. if (
  702. isFinite(maxRequests) &&
  703. getRequests(chunk) >= maxRequests
  704. ) {
  705. usedChunks.delete(chunk);
  706. }
  707. }
  708. }
  709. outer: for (const chunk of usedChunks) {
  710. for (const module of item.modules) {
  711. if (chunk.containsModule(module)) continue outer;
  712. }
  713. usedChunks.delete(chunk);
  714. }
  715. // Were some (invalid) chunks removed from usedChunks?
  716. // => readd all modules to the queue, as things could have been changed
  717. if (usedChunks.size < selectedChunks.length) {
  718. if (usedChunks.size >= item.cacheGroup.minChunks) {
  719. const chunksArr = Array.from(usedChunks);
  720. for (const module of item.modules) {
  721. addModuleToChunksInfoMap(
  722. item.cacheGroup,
  723. item.cacheGroupIndex,
  724. chunksArr,
  725. getKey(usedChunks),
  726. module
  727. );
  728. }
  729. }
  730. continue;
  731. }
  732. // Create the new chunk if not reusing one
  733. if (!isReused) {
  734. newChunk = compilation.addChunk(chunkName);
  735. }
  736. // Walk through all chunks
  737. for (const chunk of usedChunks) {
  738. // Add graph connections for splitted chunk
  739. chunk.split(newChunk);
  740. }
  741. // Add a note to the chunk
  742. newChunk.chunkReason = isReused
  743. ? "reused as split chunk"
  744. : "split chunk";
  745. if (item.cacheGroup.key) {
  746. newChunk.chunkReason += ` (cache group: ${item.cacheGroup.key})`;
  747. }
  748. if (chunkName) {
  749. newChunk.chunkReason += ` (name: ${chunkName})`;
  750. // If the chosen name is already an entry point we remove the entry point
  751. const entrypoint = compilation.entrypoints.get(chunkName);
  752. if (entrypoint) {
  753. compilation.entrypoints.delete(chunkName);
  754. entrypoint.remove();
  755. newChunk.entryModule = undefined;
  756. }
  757. }
  758. if (item.cacheGroup.filename) {
  759. if (!newChunk.isOnlyInitial()) {
  760. throw new Error(
  761. "SplitChunksPlugin: You are trying to set a filename for a chunk which is (also) loaded on demand. " +
  762. "The runtime can only handle loading of chunks which match the chunkFilename schema. " +
  763. "Using a custom filename would fail at runtime. " +
  764. `(cache group: ${item.cacheGroup.key})`
  765. );
  766. }
  767. newChunk.filenameTemplate = item.cacheGroup.filename;
  768. }
  769. if (!isReused) {
  770. // Add all modules to the new chunk
  771. for (const module of item.modules) {
  772. if (typeof module.chunkCondition === "function") {
  773. if (!module.chunkCondition(newChunk)) continue;
  774. }
  775. // Add module to new chunk
  776. GraphHelpers.connectChunkAndModule(newChunk, module);
  777. // Remove module from used chunks
  778. for (const chunk of usedChunks) {
  779. chunk.removeModule(module);
  780. module.rewriteChunkInReasons(chunk, [newChunk]);
  781. }
  782. }
  783. } else {
  784. // Remove all modules from used chunks
  785. for (const module of item.modules) {
  786. for (const chunk of usedChunks) {
  787. chunk.removeModule(module);
  788. module.rewriteChunkInReasons(chunk, [newChunk]);
  789. }
  790. }
  791. }
  792. if (item.cacheGroup.maxSize > 0) {
  793. const oldMaxSizeSettings = maxSizeQueueMap.get(newChunk);
  794. maxSizeQueueMap.set(newChunk, {
  795. minSize: Math.max(
  796. oldMaxSizeSettings ? oldMaxSizeSettings.minSize : 0,
  797. item.cacheGroup.minSizeForMaxSize
  798. ),
  799. maxSize: Math.min(
  800. oldMaxSizeSettings ? oldMaxSizeSettings.maxSize : Infinity,
  801. item.cacheGroup.maxSize
  802. ),
  803. automaticNameDelimiter: item.cacheGroup.automaticNameDelimiter,
  804. keys: oldMaxSizeSettings
  805. ? oldMaxSizeSettings.keys.concat(item.cacheGroup.key)
  806. : [item.cacheGroup.key]
  807. });
  808. }
  809. // remove all modules from other entries and update size
  810. for (const [key, info] of chunksInfoMap) {
  811. if (isOverlap(info.chunks, usedChunks)) {
  812. // update modules and total size
  813. // may remove it from the map when < minSize
  814. const oldSize = info.modules.size;
  815. for (const module of item.modules) {
  816. info.modules.delete(module);
  817. }
  818. if (info.modules.size !== oldSize) {
  819. if (info.modules.size === 0) {
  820. chunksInfoMap.delete(key);
  821. continue;
  822. }
  823. info.size = getModulesSize(info.modules);
  824. if (
  825. info.cacheGroup._validateSize &&
  826. info.size < info.cacheGroup.minSize
  827. ) {
  828. chunksInfoMap.delete(key);
  829. }
  830. if (info.modules.size === 0) {
  831. chunksInfoMap.delete(key);
  832. }
  833. }
  834. }
  835. }
  836. }
  837. const incorrectMinMaxSizeSet = new Set();
  838. // Make sure that maxSize is fulfilled
  839. for (const chunk of compilation.chunks.slice()) {
  840. const { minSize, maxSize, automaticNameDelimiter, keys } =
  841. maxSizeQueueMap.get(chunk) || this.options.fallbackCacheGroup;
  842. if (!maxSize) continue;
  843. if (minSize > maxSize) {
  844. const warningKey = `${keys && keys.join()} ${minSize} ${maxSize}`;
  845. if (!incorrectMinMaxSizeSet.has(warningKey)) {
  846. incorrectMinMaxSizeSet.add(warningKey);
  847. compilation.warnings.push(
  848. new MinMaxSizeWarning(keys, minSize, maxSize)
  849. );
  850. }
  851. }
  852. const results = deterministicGroupingForModules({
  853. maxSize: Math.max(minSize, maxSize),
  854. minSize,
  855. items: chunk.modulesIterable,
  856. getKey(module) {
  857. const ident = contextify(
  858. compilation.options.context,
  859. module.identifier()
  860. );
  861. const name = module.nameForCondition
  862. ? contextify(
  863. compilation.options.context,
  864. module.nameForCondition()
  865. )
  866. : ident.replace(/^.*!|\?[^?!]*$/g, "");
  867. const fullKey =
  868. name + automaticNameDelimiter + hashFilename(ident);
  869. return fullKey.replace(/[\\/?]/g, "_");
  870. },
  871. getSize(module) {
  872. return module.size();
  873. }
  874. });
  875. results.sort((a, b) => {
  876. if (a.key < b.key) return -1;
  877. if (a.key > b.key) return 1;
  878. return 0;
  879. });
  880. for (let i = 0; i < results.length; i++) {
  881. const group = results[i];
  882. const key = this.options.hidePathInfo
  883. ? hashFilename(group.key)
  884. : group.key;
  885. let name = chunk.name
  886. ? chunk.name + automaticNameDelimiter + key
  887. : null;
  888. if (name && name.length > 100) {
  889. name =
  890. name.slice(0, 100) +
  891. automaticNameDelimiter +
  892. hashFilename(name);
  893. }
  894. let newPart;
  895. if (i !== results.length - 1) {
  896. newPart = compilation.addChunk(name);
  897. chunk.split(newPart);
  898. newPart.chunkReason = chunk.chunkReason;
  899. // Add all modules to the new chunk
  900. for (const module of group.items) {
  901. if (typeof module.chunkCondition === "function") {
  902. if (!module.chunkCondition(newPart)) continue;
  903. }
  904. // Add module to new chunk
  905. GraphHelpers.connectChunkAndModule(newPart, module);
  906. // Remove module from used chunks
  907. chunk.removeModule(module);
  908. module.rewriteChunkInReasons(chunk, [newPart]);
  909. }
  910. } else {
  911. // change the chunk to be a part
  912. newPart = chunk;
  913. chunk.name = name;
  914. }
  915. }
  916. }
  917. }
  918. );
  919. });
  920. }
  921. };