ChunkGroup.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const util = require("util");
  7. const SortableSet = require("./util/SortableSet");
  8. const {
  9. compareChunks,
  10. compareIterables,
  11. compareLocations
  12. } = require("./util/comparators");
  13. /** @typedef {import("./AsyncDependenciesBlock")} AsyncDependenciesBlock */
  14. /** @typedef {import("./Chunk")} Chunk */
  15. /** @typedef {import("./ChunkGraph")} ChunkGraph */
  16. /** @typedef {import("./Dependency").DependencyLocation} DependencyLocation */
  17. /** @typedef {import("./Entrypoint")} Entrypoint */
  18. /** @typedef {import("./Module")} Module */
  19. /** @typedef {import("./ModuleGraph")} ModuleGraph */
  20. /** @typedef {{id: number}} HasId */
  21. /** @typedef {{module: Module | null, loc: DependencyLocation, request: string}} OriginRecord */
  22. /**
  23. * @typedef {object} RawChunkGroupOptions
  24. * @property {number=} preloadOrder
  25. * @property {number=} prefetchOrder
  26. * @property {("low" | "high" | "auto")=} fetchPriority
  27. */
  28. /** @typedef {RawChunkGroupOptions & { name?: string | null }} ChunkGroupOptions */
  29. let debugId = 5000;
  30. /**
  31. * @template T
  32. * @param {SortableSet<T>} set set to convert to array.
  33. * @returns {T[]} the array format of existing set
  34. */
  35. const getArray = set => [...set];
  36. /**
  37. * A convenience method used to sort chunks based on their id's
  38. * @param {ChunkGroup} a first sorting comparator
  39. * @param {ChunkGroup} b second sorting comparator
  40. * @returns {1|0|-1} a sorting index to determine order
  41. */
  42. const sortById = (a, b) => {
  43. if (a.id < b.id) return -1;
  44. if (b.id < a.id) return 1;
  45. return 0;
  46. };
  47. /**
  48. * @param {OriginRecord} a the first comparator in sort
  49. * @param {OriginRecord} b the second comparator in sort
  50. * @returns {1|-1|0} returns sorting order as index
  51. */
  52. const sortOrigin = (a, b) => {
  53. const aIdent = a.module ? a.module.identifier() : "";
  54. const bIdent = b.module ? b.module.identifier() : "";
  55. if (aIdent < bIdent) return -1;
  56. if (aIdent > bIdent) return 1;
  57. return compareLocations(a.loc, b.loc);
  58. };
  59. class ChunkGroup {
  60. /**
  61. * Creates an instance of ChunkGroup.
  62. * @param {string | ChunkGroupOptions=} options chunk group options passed to chunkGroup
  63. */
  64. constructor(options) {
  65. if (typeof options === "string") {
  66. options = { name: options };
  67. } else if (!options) {
  68. options = { name: undefined };
  69. }
  70. /** @type {number} */
  71. this.groupDebugId = debugId++;
  72. this.options = /** @type {ChunkGroupOptions} */ (options);
  73. /** @type {SortableSet<ChunkGroup>} */
  74. this._children = new SortableSet(undefined, sortById);
  75. /** @type {SortableSet<ChunkGroup>} */
  76. this._parents = new SortableSet(undefined, sortById);
  77. /** @type {SortableSet<ChunkGroup>} */
  78. this._asyncEntrypoints = new SortableSet(undefined, sortById);
  79. this._blocks = new SortableSet();
  80. /** @type {Chunk[]} */
  81. this.chunks = [];
  82. /** @type {OriginRecord[]} */
  83. this.origins = [];
  84. /** Indices in top-down order */
  85. /**
  86. * @private
  87. * @type {Map<Module, number>}
  88. */
  89. this._modulePreOrderIndices = new Map();
  90. /** Indices in bottom-up order */
  91. /**
  92. * @private
  93. * @type {Map<Module, number>}
  94. */
  95. this._modulePostOrderIndices = new Map();
  96. /** @type {number | undefined} */
  97. this.index = undefined;
  98. }
  99. /**
  100. * when a new chunk is added to a chunkGroup, addingOptions will occur.
  101. * @param {ChunkGroupOptions} options the chunkGroup options passed to addOptions
  102. * @returns {void}
  103. */
  104. addOptions(options) {
  105. for (const _key of Object.keys(options)) {
  106. const key =
  107. /** @type {keyof ChunkGroupOptions} */
  108. (_key);
  109. if (this.options[key] === undefined) {
  110. /** @type {EXPECTED_ANY} */
  111. (this.options)[key] = options[key];
  112. } else if (this.options[key] !== options[key]) {
  113. if (key.endsWith("Order")) {
  114. const orderKey =
  115. /** @type {Exclude<keyof ChunkGroupOptions, "name" | "fetchPriority">} */
  116. (key);
  117. this.options[orderKey] = Math.max(
  118. /** @type {number} */
  119. (this.options[orderKey]),
  120. /** @type {number} */
  121. (options[orderKey])
  122. );
  123. } else {
  124. throw new Error(
  125. `ChunkGroup.addOptions: No option merge strategy for ${key}`
  126. );
  127. }
  128. }
  129. }
  130. }
  131. /**
  132. * returns the name of current ChunkGroup
  133. * @returns {string | null | undefined} returns the ChunkGroup name
  134. */
  135. get name() {
  136. return this.options.name;
  137. }
  138. /**
  139. * sets a new name for current ChunkGroup
  140. * @param {string | undefined} value the new name for ChunkGroup
  141. * @returns {void}
  142. */
  143. set name(value) {
  144. this.options.name = value;
  145. }
  146. /* istanbul ignore next */
  147. /**
  148. * get a uniqueId for ChunkGroup, made up of its member Chunk debugId's
  149. * @returns {string} a unique concatenation of chunk debugId's
  150. */
  151. get debugId() {
  152. return Array.from(this.chunks, x => x.debugId).join("+");
  153. }
  154. /**
  155. * get a unique id for ChunkGroup, made up of its member Chunk id's
  156. * @returns {string} a unique concatenation of chunk ids
  157. */
  158. get id() {
  159. return Array.from(this.chunks, x => x.id).join("+");
  160. }
  161. /**
  162. * Performs an unshift of a specific chunk
  163. * @param {Chunk} chunk chunk being unshifted
  164. * @returns {boolean} returns true if attempted chunk shift is accepted
  165. */
  166. unshiftChunk(chunk) {
  167. const oldIdx = this.chunks.indexOf(chunk);
  168. if (oldIdx > 0) {
  169. this.chunks.splice(oldIdx, 1);
  170. this.chunks.unshift(chunk);
  171. } else if (oldIdx < 0) {
  172. this.chunks.unshift(chunk);
  173. return true;
  174. }
  175. return false;
  176. }
  177. /**
  178. * inserts a chunk before another existing chunk in group
  179. * @param {Chunk} chunk Chunk being inserted
  180. * @param {Chunk} before Placeholder/target chunk marking new chunk insertion point
  181. * @returns {boolean} return true if insertion was successful
  182. */
  183. insertChunk(chunk, before) {
  184. const oldIdx = this.chunks.indexOf(chunk);
  185. const idx = this.chunks.indexOf(before);
  186. if (idx < 0) {
  187. throw new Error("before chunk not found");
  188. }
  189. if (oldIdx >= 0 && oldIdx > idx) {
  190. this.chunks.splice(oldIdx, 1);
  191. this.chunks.splice(idx, 0, chunk);
  192. } else if (oldIdx < 0) {
  193. this.chunks.splice(idx, 0, chunk);
  194. return true;
  195. }
  196. return false;
  197. }
  198. /**
  199. * add a chunk into ChunkGroup. Is pushed on or prepended
  200. * @param {Chunk} chunk chunk being pushed into ChunkGroupS
  201. * @returns {boolean} returns true if chunk addition was successful.
  202. */
  203. pushChunk(chunk) {
  204. const oldIdx = this.chunks.indexOf(chunk);
  205. if (oldIdx >= 0) {
  206. return false;
  207. }
  208. this.chunks.push(chunk);
  209. return true;
  210. }
  211. /**
  212. * @param {Chunk} oldChunk chunk to be replaced
  213. * @param {Chunk} newChunk New chunk that will be replaced with
  214. * @returns {boolean | undefined} returns true if the replacement was successful
  215. */
  216. replaceChunk(oldChunk, newChunk) {
  217. const oldIdx = this.chunks.indexOf(oldChunk);
  218. if (oldIdx < 0) return false;
  219. const newIdx = this.chunks.indexOf(newChunk);
  220. if (newIdx < 0) {
  221. this.chunks[oldIdx] = newChunk;
  222. return true;
  223. }
  224. if (newIdx < oldIdx) {
  225. this.chunks.splice(oldIdx, 1);
  226. return true;
  227. } else if (newIdx !== oldIdx) {
  228. this.chunks[oldIdx] = newChunk;
  229. this.chunks.splice(newIdx, 1);
  230. return true;
  231. }
  232. }
  233. /**
  234. * @param {Chunk} chunk chunk to remove
  235. * @returns {boolean} returns true if chunk was removed
  236. */
  237. removeChunk(chunk) {
  238. const idx = this.chunks.indexOf(chunk);
  239. if (idx >= 0) {
  240. this.chunks.splice(idx, 1);
  241. return true;
  242. }
  243. return false;
  244. }
  245. /**
  246. * @returns {boolean} true, when this chunk group will be loaded on initial page load
  247. */
  248. isInitial() {
  249. return false;
  250. }
  251. /**
  252. * @param {ChunkGroup} group chunk group to add
  253. * @returns {boolean} returns true if chunk group was added
  254. */
  255. addChild(group) {
  256. const size = this._children.size;
  257. this._children.add(group);
  258. return size !== this._children.size;
  259. }
  260. /**
  261. * @returns {ChunkGroup[]} returns the children of this group
  262. */
  263. getChildren() {
  264. return this._children.getFromCache(getArray);
  265. }
  266. getNumberOfChildren() {
  267. return this._children.size;
  268. }
  269. get childrenIterable() {
  270. return this._children;
  271. }
  272. /**
  273. * @param {ChunkGroup} group the chunk group to remove
  274. * @returns {boolean} returns true if the chunk group was removed
  275. */
  276. removeChild(group) {
  277. if (!this._children.has(group)) {
  278. return false;
  279. }
  280. this._children.delete(group);
  281. group.removeParent(this);
  282. return true;
  283. }
  284. /**
  285. * @param {ChunkGroup} parentChunk the parent group to be added into
  286. * @returns {boolean} returns true if this chunk group was added to the parent group
  287. */
  288. addParent(parentChunk) {
  289. if (!this._parents.has(parentChunk)) {
  290. this._parents.add(parentChunk);
  291. return true;
  292. }
  293. return false;
  294. }
  295. /**
  296. * @returns {ChunkGroup[]} returns the parents of this group
  297. */
  298. getParents() {
  299. return this._parents.getFromCache(getArray);
  300. }
  301. getNumberOfParents() {
  302. return this._parents.size;
  303. }
  304. /**
  305. * @param {ChunkGroup} parent the parent group
  306. * @returns {boolean} returns true if the parent group contains this group
  307. */
  308. hasParent(parent) {
  309. return this._parents.has(parent);
  310. }
  311. get parentsIterable() {
  312. return this._parents;
  313. }
  314. /**
  315. * @param {ChunkGroup} chunkGroup the parent group
  316. * @returns {boolean} returns true if this group has been removed from the parent
  317. */
  318. removeParent(chunkGroup) {
  319. if (this._parents.delete(chunkGroup)) {
  320. chunkGroup.removeChild(this);
  321. return true;
  322. }
  323. return false;
  324. }
  325. /**
  326. * @param {Entrypoint} entrypoint entrypoint to add
  327. * @returns {boolean} returns true if entrypoint was added
  328. */
  329. addAsyncEntrypoint(entrypoint) {
  330. const size = this._asyncEntrypoints.size;
  331. this._asyncEntrypoints.add(entrypoint);
  332. return size !== this._asyncEntrypoints.size;
  333. }
  334. get asyncEntrypointsIterable() {
  335. return this._asyncEntrypoints;
  336. }
  337. /**
  338. * @returns {Array<AsyncDependenciesBlock>} an array containing the blocks
  339. */
  340. getBlocks() {
  341. return this._blocks.getFromCache(getArray);
  342. }
  343. getNumberOfBlocks() {
  344. return this._blocks.size;
  345. }
  346. /**
  347. * @param {AsyncDependenciesBlock} block block
  348. * @returns {boolean} true, if block exists
  349. */
  350. hasBlock(block) {
  351. return this._blocks.has(block);
  352. }
  353. /**
  354. * @returns {Iterable<AsyncDependenciesBlock>} blocks
  355. */
  356. get blocksIterable() {
  357. return this._blocks;
  358. }
  359. /**
  360. * @param {AsyncDependenciesBlock} block a block
  361. * @returns {boolean} false, if block was already added
  362. */
  363. addBlock(block) {
  364. if (!this._blocks.has(block)) {
  365. this._blocks.add(block);
  366. return true;
  367. }
  368. return false;
  369. }
  370. /**
  371. * @param {Module | null} module origin module
  372. * @param {DependencyLocation} loc location of the reference in the origin module
  373. * @param {string} request request name of the reference
  374. * @returns {void}
  375. */
  376. addOrigin(module, loc, request) {
  377. this.origins.push({
  378. module,
  379. loc,
  380. request
  381. });
  382. }
  383. /**
  384. * @returns {string[]} the files contained this chunk group
  385. */
  386. getFiles() {
  387. const files = new Set();
  388. for (const chunk of this.chunks) {
  389. for (const file of chunk.files) {
  390. files.add(file);
  391. }
  392. }
  393. return [...files];
  394. }
  395. /**
  396. * @returns {void}
  397. */
  398. remove() {
  399. // cleanup parents
  400. for (const parentChunkGroup of this._parents) {
  401. // remove this chunk from its parents
  402. parentChunkGroup._children.delete(this);
  403. // cleanup "sub chunks"
  404. for (const chunkGroup of this._children) {
  405. /**
  406. * remove this chunk as "intermediary" and connect
  407. * it "sub chunks" and parents directly
  408. */
  409. // add parent to each "sub chunk"
  410. chunkGroup.addParent(parentChunkGroup);
  411. // add "sub chunk" to parent
  412. parentChunkGroup.addChild(chunkGroup);
  413. }
  414. }
  415. /**
  416. * we need to iterate again over the children
  417. * to remove this from the child's parents.
  418. * This can not be done in the above loop
  419. * as it is not guaranteed that `this._parents` contains anything.
  420. */
  421. for (const chunkGroup of this._children) {
  422. // remove this as parent of every "sub chunk"
  423. chunkGroup._parents.delete(this);
  424. }
  425. // remove chunks
  426. for (const chunk of this.chunks) {
  427. chunk.removeGroup(this);
  428. }
  429. }
  430. sortItems() {
  431. this.origins.sort(sortOrigin);
  432. }
  433. /**
  434. * Sorting predicate which allows current ChunkGroup to be compared against another.
  435. * Sorting values are based off of number of chunks in ChunkGroup.
  436. * @param {ChunkGraph} chunkGraph the chunk graph
  437. * @param {ChunkGroup} otherGroup the chunkGroup to compare this against
  438. * @returns {-1|0|1} sort position for comparison
  439. */
  440. compareTo(chunkGraph, otherGroup) {
  441. if (this.chunks.length > otherGroup.chunks.length) return -1;
  442. if (this.chunks.length < otherGroup.chunks.length) return 1;
  443. return compareIterables(compareChunks(chunkGraph))(
  444. this.chunks,
  445. otherGroup.chunks
  446. );
  447. }
  448. /**
  449. * @param {ModuleGraph} moduleGraph the module graph
  450. * @param {ChunkGraph} chunkGraph the chunk graph
  451. * @returns {Record<string, ChunkGroup[]>} mapping from children type to ordered list of ChunkGroups
  452. */
  453. getChildrenByOrders(moduleGraph, chunkGraph) {
  454. /** @type {Map<string, {order: number, group: ChunkGroup}[]>} */
  455. const lists = new Map();
  456. for (const childGroup of this._children) {
  457. for (const key of Object.keys(childGroup.options)) {
  458. if (key.endsWith("Order")) {
  459. const name = key.slice(0, key.length - "Order".length);
  460. let list = lists.get(name);
  461. if (list === undefined) {
  462. lists.set(name, (list = []));
  463. }
  464. list.push({
  465. order:
  466. /** @type {number} */
  467. (
  468. childGroup.options[/** @type {keyof ChunkGroupOptions} */ (key)]
  469. ),
  470. group: childGroup
  471. });
  472. }
  473. }
  474. }
  475. /** @type {Record<string, ChunkGroup[]>} */
  476. const result = Object.create(null);
  477. for (const [name, list] of lists) {
  478. list.sort((a, b) => {
  479. const cmp = b.order - a.order;
  480. if (cmp !== 0) return cmp;
  481. return a.group.compareTo(chunkGraph, b.group);
  482. });
  483. result[name] = list.map(i => i.group);
  484. }
  485. return result;
  486. }
  487. /**
  488. * Sets the top-down index of a module in this ChunkGroup
  489. * @param {Module} module module for which the index should be set
  490. * @param {number} index the index of the module
  491. * @returns {void}
  492. */
  493. setModulePreOrderIndex(module, index) {
  494. this._modulePreOrderIndices.set(module, index);
  495. }
  496. /**
  497. * Gets the top-down index of a module in this ChunkGroup
  498. * @param {Module} module the module
  499. * @returns {number | undefined} index
  500. */
  501. getModulePreOrderIndex(module) {
  502. return this._modulePreOrderIndices.get(module);
  503. }
  504. /**
  505. * Sets the bottom-up index of a module in this ChunkGroup
  506. * @param {Module} module module for which the index should be set
  507. * @param {number} index the index of the module
  508. * @returns {void}
  509. */
  510. setModulePostOrderIndex(module, index) {
  511. this._modulePostOrderIndices.set(module, index);
  512. }
  513. /**
  514. * Gets the bottom-up index of a module in this ChunkGroup
  515. * @param {Module} module the module
  516. * @returns {number | undefined} index
  517. */
  518. getModulePostOrderIndex(module) {
  519. return this._modulePostOrderIndices.get(module);
  520. }
  521. /* istanbul ignore next */
  522. checkConstraints() {
  523. const chunk = this;
  524. for (const child of chunk._children) {
  525. if (!child._parents.has(chunk)) {
  526. throw new Error(
  527. `checkConstraints: child missing parent ${chunk.debugId} -> ${child.debugId}`
  528. );
  529. }
  530. }
  531. for (const parentChunk of chunk._parents) {
  532. if (!parentChunk._children.has(chunk)) {
  533. throw new Error(
  534. `checkConstraints: parent missing child ${parentChunk.debugId} <- ${chunk.debugId}`
  535. );
  536. }
  537. }
  538. }
  539. }
  540. ChunkGroup.prototype.getModuleIndex = util.deprecate(
  541. ChunkGroup.prototype.getModulePreOrderIndex,
  542. "ChunkGroup.getModuleIndex was renamed to getModulePreOrderIndex",
  543. "DEP_WEBPACK_CHUNK_GROUP_GET_MODULE_INDEX"
  544. );
  545. ChunkGroup.prototype.getModuleIndex2 = util.deprecate(
  546. ChunkGroup.prototype.getModulePostOrderIndex,
  547. "ChunkGroup.getModuleIndex2 was renamed to getModulePostOrderIndex",
  548. "DEP_WEBPACK_CHUNK_GROUP_GET_MODULE_INDEX_2"
  549. );
  550. module.exports = ChunkGroup;