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

938 lines
29 KiB

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