UNPKG

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