UNPKG

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