SplitChunksPlugin.js 28 KB

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