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.

513 lines
12 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 SortableSet = require("./util/SortableSet");
  7. const compareLocations = require("./compareLocations");
  8. /** @typedef {import("./Chunk")} Chunk */
  9. /** @typedef {import("./Module")} Module */
  10. /** @typedef {import("./ModuleReason")} ModuleReason */
  11. /** @typedef {{module: Module, loc: TODO, request: string}} OriginRecord */
  12. /** @typedef {string|{name: string}} ChunkGroupOptions */
  13. let debugId = 5000;
  14. /**
  15. * @template T
  16. * @param {SortableSet<T>} set set to convert to array.
  17. * @returns {T[]} the array format of existing set
  18. */
  19. const getArray = set => Array.from(set);
  20. /**
  21. * A convenience method used to sort chunks based on their id's
  22. * @param {ChunkGroup} a first sorting comparator
  23. * @param {ChunkGroup} b second sorting comparator
  24. * @returns {1|0|-1} a sorting index to determine order
  25. */
  26. const sortById = (a, b) => {
  27. if (a.id < b.id) return -1;
  28. if (b.id < a.id) return 1;
  29. return 0;
  30. };
  31. /**
  32. * @param {OriginRecord} a the first comparator in sort
  33. * @param {OriginRecord} b the second comparator in sort
  34. * @returns {1|-1|0} returns sorting order as index
  35. */
  36. const sortOrigin = (a, b) => {
  37. const aIdent = a.module ? a.module.identifier() : "";
  38. const bIdent = b.module ? b.module.identifier() : "";
  39. if (aIdent < bIdent) return -1;
  40. if (aIdent > bIdent) return 1;
  41. return compareLocations(a.loc, b.loc);
  42. };
  43. class ChunkGroup {
  44. /**
  45. * Creates an instance of ChunkGroup.
  46. * @param {ChunkGroupOptions=} options chunk group options passed to chunkGroup
  47. */
  48. constructor(options) {
  49. if (typeof options === "string") {
  50. options = { name: options };
  51. } else if (!options) {
  52. options = { name: undefined };
  53. }
  54. /** @type {number} */
  55. this.groupDebugId = debugId++;
  56. this.options = options;
  57. /** @type {SortableSet<ChunkGroup>} */
  58. this._children = new SortableSet(undefined, sortById);
  59. this._parents = new SortableSet(undefined, sortById);
  60. this._blocks = new SortableSet();
  61. /** @type {Chunk[]} */
  62. this.chunks = [];
  63. /** @type {OriginRecord[]} */
  64. this.origins = [];
  65. /** Indices in top-down order */
  66. /** @private @type {Map<Module, number>} */
  67. this._moduleIndices = new Map();
  68. /** Indices in bottom-up order */
  69. /** @private @type {Map<Module, number>} */
  70. this._moduleIndices2 = new Map();
  71. }
  72. /**
  73. * when a new chunk is added to a chunkGroup, addingOptions will occur.
  74. * @param {ChunkGroupOptions} options the chunkGroup options passed to addOptions
  75. * @returns {void}
  76. */
  77. addOptions(options) {
  78. for (const key of Object.keys(options)) {
  79. if (this.options[key] === undefined) {
  80. this.options[key] = options[key];
  81. } else if (this.options[key] !== options[key]) {
  82. if (key.endsWith("Order")) {
  83. this.options[key] = Math.max(this.options[key], options[key]);
  84. } else {
  85. throw new Error(
  86. `ChunkGroup.addOptions: No option merge strategy for ${key}`
  87. );
  88. }
  89. }
  90. }
  91. }
  92. /**
  93. * returns the name of current ChunkGroup
  94. * @returns {string|undefined} returns the ChunkGroup name
  95. */
  96. get name() {
  97. return this.options.name;
  98. }
  99. /**
  100. * sets a new name for current ChunkGroup
  101. * @param {string} value the new name for ChunkGroup
  102. * @returns {void}
  103. */
  104. set name(value) {
  105. this.options.name = value;
  106. }
  107. /**
  108. * get a uniqueId for ChunkGroup, made up of its member Chunk debugId's
  109. * @returns {string} a unique concatenation of chunk debugId's
  110. */
  111. get debugId() {
  112. return Array.from(this.chunks, x => x.debugId).join("+");
  113. }
  114. /**
  115. * get a unique id for ChunkGroup, made up of its member Chunk id's
  116. * @returns {string} a unique concatenation of chunk ids
  117. */
  118. get id() {
  119. return Array.from(this.chunks, x => x.id).join("+");
  120. }
  121. /**
  122. * Performs an unshift of a specific chunk
  123. * @param {Chunk} chunk chunk being unshifted
  124. * @returns {boolean} returns true if attempted chunk shift is accepted
  125. */
  126. unshiftChunk(chunk) {
  127. const oldIdx = this.chunks.indexOf(chunk);
  128. if (oldIdx > 0) {
  129. this.chunks.splice(oldIdx, 1);
  130. this.chunks.unshift(chunk);
  131. } else if (oldIdx < 0) {
  132. this.chunks.unshift(chunk);
  133. return true;
  134. }
  135. return false;
  136. }
  137. /**
  138. * inserts a chunk before another existing chunk in group
  139. * @param {Chunk} chunk Chunk being inserted
  140. * @param {Chunk} before Placeholder/target chunk marking new chunk insertion point
  141. * @returns {boolean} return true if insertion was successful
  142. */
  143. insertChunk(chunk, before) {
  144. const oldIdx = this.chunks.indexOf(chunk);
  145. const idx = this.chunks.indexOf(before);
  146. if (idx < 0) {
  147. throw new Error("before chunk not found");
  148. }
  149. if (oldIdx >= 0 && oldIdx > idx) {
  150. this.chunks.splice(oldIdx, 1);
  151. this.chunks.splice(idx, 0, chunk);
  152. } else if (oldIdx < 0) {
  153. this.chunks.splice(idx, 0, chunk);
  154. return true;
  155. }
  156. return false;
  157. }
  158. /**
  159. * add a chunk into ChunkGroup. Is pushed on or prepended
  160. * @param {Chunk} chunk chunk being pushed into ChunkGroupS
  161. * @returns {boolean} returns true if chunk addition was successful.
  162. */
  163. pushChunk(chunk) {
  164. const oldIdx = this.chunks.indexOf(chunk);
  165. if (oldIdx >= 0) {
  166. return false;
  167. }
  168. this.chunks.push(chunk);
  169. return true;
  170. }
  171. /**
  172. * @param {Chunk} oldChunk chunk to be replaced
  173. * @param {Chunk} newChunk New chunk that will be replaced with
  174. * @returns {boolean} returns true if the replacement was successful
  175. */
  176. replaceChunk(oldChunk, newChunk) {
  177. const oldIdx = this.chunks.indexOf(oldChunk);
  178. if (oldIdx < 0) return false;
  179. const newIdx = this.chunks.indexOf(newChunk);
  180. if (newIdx < 0) {
  181. this.chunks[oldIdx] = newChunk;
  182. return true;
  183. }
  184. if (newIdx < oldIdx) {
  185. this.chunks.splice(oldIdx, 1);
  186. return true;
  187. } else if (newIdx !== oldIdx) {
  188. this.chunks[oldIdx] = newChunk;
  189. this.chunks.splice(newIdx, 1);
  190. return true;
  191. }
  192. }
  193. removeChunk(chunk) {
  194. const idx = this.chunks.indexOf(chunk);
  195. if (idx >= 0) {
  196. this.chunks.splice(idx, 1);
  197. return true;
  198. }
  199. return false;
  200. }
  201. isInitial() {
  202. return false;
  203. }
  204. addChild(chunk) {
  205. if (this._children.has(chunk)) {
  206. return false;
  207. }
  208. this._children.add(chunk);
  209. return true;
  210. }
  211. getChildren() {
  212. return this._children.getFromCache(getArray);
  213. }
  214. getNumberOfChildren() {
  215. return this._children.size;
  216. }
  217. get childrenIterable() {
  218. return this._children;
  219. }
  220. removeChild(chunk) {
  221. if (!this._children.has(chunk)) {
  222. return false;
  223. }
  224. this._children.delete(chunk);
  225. chunk.removeParent(this);
  226. return true;
  227. }
  228. addParent(parentChunk) {
  229. if (!this._parents.has(parentChunk)) {
  230. this._parents.add(parentChunk);
  231. return true;
  232. }
  233. return false;
  234. }
  235. getParents() {
  236. return this._parents.getFromCache(getArray);
  237. }
  238. setParents(newParents) {
  239. this._parents.clear();
  240. for (const p of newParents) {
  241. this._parents.add(p);
  242. }
  243. }
  244. getNumberOfParents() {
  245. return this._parents.size;
  246. }
  247. hasParent(parent) {
  248. return this._parents.has(parent);
  249. }
  250. get parentsIterable() {
  251. return this._parents;
  252. }
  253. removeParent(chunk) {
  254. if (this._parents.delete(chunk)) {
  255. chunk.removeChunk(this);
  256. return true;
  257. }
  258. return false;
  259. }
  260. /**
  261. * @returns {Array} - an array containing the blocks
  262. */
  263. getBlocks() {
  264. return this._blocks.getFromCache(getArray);
  265. }
  266. getNumberOfBlocks() {
  267. return this._blocks.size;
  268. }
  269. hasBlock(block) {
  270. return this._blocks.has(block);
  271. }
  272. get blocksIterable() {
  273. return this._blocks;
  274. }
  275. addBlock(block) {
  276. if (!this._blocks.has(block)) {
  277. this._blocks.add(block);
  278. return true;
  279. }
  280. return false;
  281. }
  282. addOrigin(module, loc, request) {
  283. this.origins.push({
  284. module,
  285. loc,
  286. request
  287. });
  288. }
  289. containsModule(module) {
  290. for (const chunk of this.chunks) {
  291. if (chunk.containsModule(module)) return true;
  292. }
  293. return false;
  294. }
  295. getFiles() {
  296. const files = new Set();
  297. for (const chunk of this.chunks) {
  298. for (const file of chunk.files) {
  299. files.add(file);
  300. }
  301. }
  302. return Array.from(files);
  303. }
  304. /**
  305. * @param {string=} reason reason for removing ChunkGroup
  306. * @returns {void}
  307. */
  308. remove(reason) {
  309. // cleanup parents
  310. for (const parentChunkGroup of this._parents) {
  311. // remove this chunk from its parents
  312. parentChunkGroup._children.delete(this);
  313. // cleanup "sub chunks"
  314. for (const chunkGroup of this._children) {
  315. /**
  316. * remove this chunk as "intermediary" and connect
  317. * it "sub chunks" and parents directly
  318. */
  319. // add parent to each "sub chunk"
  320. chunkGroup.addParent(parentChunkGroup);
  321. // add "sub chunk" to parent
  322. parentChunkGroup.addChild(chunkGroup);
  323. }
  324. }
  325. /**
  326. * we need to iterate again over the children
  327. * to remove this from the child's parents.
  328. * This can not be done in the above loop
  329. * as it is not guaranteed that `this._parents` contains anything.
  330. */
  331. for (const chunkGroup of this._children) {
  332. // remove this as parent of every "sub chunk"
  333. chunkGroup._parents.delete(this);
  334. }
  335. // cleanup blocks
  336. for (const block of this._blocks) {
  337. block.chunkGroup = null;
  338. }
  339. // remove chunks
  340. for (const chunk of this.chunks) {
  341. chunk.removeGroup(this);
  342. }
  343. }
  344. sortItems() {
  345. this.origins.sort(sortOrigin);
  346. this._parents.sort();
  347. this._children.sort();
  348. }
  349. /**
  350. * Sorting predicate which allows current ChunkGroup to be compared against another.
  351. * Sorting values are based off of number of chunks in ChunkGroup.
  352. *
  353. * @param {ChunkGroup} otherGroup the chunkGroup to compare this against
  354. * @returns {-1|0|1} sort position for comparison
  355. */
  356. compareTo(otherGroup) {
  357. if (this.chunks.length > otherGroup.chunks.length) return -1;
  358. if (this.chunks.length < otherGroup.chunks.length) return 1;
  359. const a = this.chunks[Symbol.iterator]();
  360. const b = otherGroup.chunks[Symbol.iterator]();
  361. // eslint-disable-next-line no-constant-condition
  362. while (true) {
  363. const aItem = a.next();
  364. const bItem = b.next();
  365. if (aItem.done) return 0;
  366. const cmp = aItem.value.compareTo(bItem.value);
  367. if (cmp !== 0) return cmp;
  368. }
  369. }
  370. getChildrenByOrders() {
  371. const lists = new Map();
  372. for (const childGroup of this._children) {
  373. // TODO webpack 5 remove this check for options
  374. if (typeof childGroup.options === "object") {
  375. for (const key of Object.keys(childGroup.options)) {
  376. if (key.endsWith("Order")) {
  377. const name = key.substr(0, key.length - "Order".length);
  378. let list = lists.get(name);
  379. if (list === undefined) {
  380. lists.set(name, (list = []));
  381. }
  382. list.push({
  383. order: childGroup.options[key],
  384. group: childGroup
  385. });
  386. }
  387. }
  388. }
  389. }
  390. const result = Object.create(null);
  391. for (const [name, list] of lists) {
  392. list.sort((a, b) => {
  393. const cmp = b.order - a.order;
  394. if (cmp !== 0) return cmp;
  395. // TODO webpack 5 remove this check of compareTo
  396. if (a.group.compareTo) {
  397. return a.group.compareTo(b.group);
  398. }
  399. return 0;
  400. });
  401. result[name] = list.map(i => i.group);
  402. }
  403. return result;
  404. }
  405. /**
  406. * Sets the top-down index of a module in this ChunkGroup
  407. * @param {Module} module module for which the index should be set
  408. * @param {number} index the index of the module
  409. * @returns {void}
  410. */
  411. setModuleIndex(module, index) {
  412. this._moduleIndices.set(module, index);
  413. }
  414. /**
  415. * Gets the top-down index of a module in this ChunkGroup
  416. * @param {Module} module the module
  417. * @returns {number} index
  418. */
  419. getModuleIndex(module) {
  420. return this._moduleIndices.get(module);
  421. }
  422. /**
  423. * Sets the bottom-up index of a module in this ChunkGroup
  424. * @param {Module} module module for which the index should be set
  425. * @param {number} index the index of the module
  426. * @returns {void}
  427. */
  428. setModuleIndex2(module, index) {
  429. this._moduleIndices2.set(module, index);
  430. }
  431. /**
  432. * Gets the bottom-up index of a module in this ChunkGroup
  433. * @param {Module} module the module
  434. * @returns {number} index
  435. */
  436. getModuleIndex2(module) {
  437. return this._moduleIndices2.get(module);
  438. }
  439. checkConstraints() {
  440. const chunk = this;
  441. for (const child of chunk._children) {
  442. if (!child._parents.has(chunk)) {
  443. throw new Error(
  444. `checkConstraints: child missing parent ${chunk.debugId} -> ${child.debugId}`
  445. );
  446. }
  447. }
  448. for (const parentChunk of chunk._parents) {
  449. if (!parentChunk._children.has(chunk)) {
  450. throw new Error(
  451. `checkConstraints: parent missing child ${parentChunk.debugId} <- ${chunk.debugId}`
  452. );
  453. }
  454. }
  455. }
  456. }
  457. module.exports = ChunkGroup;