UNPKG

21.3 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 AsyncDependencyToInitialChunkError = require("./AsyncDependencyToInitialChunkError");
9const GraphHelpers = require("./GraphHelpers");
10
11/** @typedef {import("./AsyncDependenciesBlock")} AsyncDependenciesBlock */
12/** @typedef {import("./Chunk")} Chunk */
13/** @typedef {import("./ChunkGroup")} ChunkGroup */
14/** @typedef {import("./Compilation")} Compilation */
15/** @typedef {import("./DependenciesBlock")} DependenciesBlock */
16/** @typedef {import("./Dependency")} Dependency */
17/** @typedef {import("./Entrypoint")} Entrypoint */
18/** @typedef {import("./Module")} Module */
19
20/**
21 * @typedef {Object} QueueItem
22 * @property {number} action
23 * @property {DependenciesBlock} block
24 * @property {Module} module
25 * @property {Chunk} chunk
26 * @property {ChunkGroup} chunkGroup
27 */
28
29/**
30 * @typedef {Object} ChunkGroupInfo
31 * @property {ChunkGroup} chunkGroup the chunk group
32 * @property {Set<Module>} minAvailableModules current minimal set of modules available at this point
33 * @property {boolean} minAvailableModulesOwned true, if minAvailableModules is owned and can be modified
34 * @property {Set<Module>[]} availableModulesToBeMerged enqueued updates to the minimal set of available modules
35 * @property {QueueItem[]} skippedItems queue items that were skipped because module is already available in parent chunks (need to reconsider when minAvailableModules is shrinking)
36 * @property {Set<Module>} resultingAvailableModules set of modules available including modules from this chunk group
37 * @property {Set<ChunkGroup>} children set of children chunk groups, that will be revisited when availableModules shrink
38 */
39
40/**
41 * @typedef {Object} ChunkGroupDep
42 * @property {AsyncDependenciesBlock} block referencing block
43 * @property {ChunkGroup} chunkGroup referenced chunk group
44 */
45
46/**
47 * @template T
48 * @param {Set<T>} a first set
49 * @param {Set<T>} b second set
50 * @returns {number} cmp
51 */
52const bySetSize = (a, b) => {
53 return b.size - a.size;
54};
55
56/**
57 * Extracts simplified info from the modules and their dependencies
58 * @param {Compilation} compilation the compilation
59 * @returns {Map<DependenciesBlock, { modules: Iterable<Module>, blocks: AsyncDependenciesBlock[]}>} the mapping block to modules and inner blocks
60 */
61const extraceBlockInfoMap = compilation => {
62 /** @type {Map<DependenciesBlock, { modules: Iterable<Module>, blocks: AsyncDependenciesBlock[]}>} */
63 const blockInfoMap = new Map();
64
65 /**
66 * @param {Dependency} d dependency to iterate over
67 * @returns {void}
68 */
69 const iteratorDependency = d => {
70 // We skip Dependencies without Reference
71 const ref = compilation.getDependencyReference(currentModule, d);
72 if (!ref) {
73 return;
74 }
75 // We skip Dependencies without Module pointer
76 const refModule = ref.module;
77 if (!refModule) {
78 return;
79 }
80 // We skip weak Dependencies
81 if (ref.weak) {
82 return;
83 }
84
85 blockInfoModules.add(refModule);
86 };
87
88 /**
89 * @param {AsyncDependenciesBlock} b blocks to prepare
90 * @returns {void}
91 */
92 const iteratorBlockPrepare = b => {
93 blockInfoBlocks.push(b);
94 blockQueue.push(b);
95 };
96
97 /** @type {Module} */
98 let currentModule;
99 /** @type {DependenciesBlock} */
100 let block;
101 /** @type {DependenciesBlock[]} */
102 let blockQueue;
103 /** @type {Set<Module>} */
104 let blockInfoModules;
105 /** @type {AsyncDependenciesBlock[]} */
106 let blockInfoBlocks;
107
108 for (const module of compilation.modules) {
109 blockQueue = [module];
110 currentModule = module;
111 while (blockQueue.length > 0) {
112 block = blockQueue.pop();
113 blockInfoModules = new Set();
114 blockInfoBlocks = [];
115
116 if (block.variables) {
117 for (const variable of block.variables) {
118 for (const dep of variable.dependencies) iteratorDependency(dep);
119 }
120 }
121
122 if (block.dependencies) {
123 for (const dep of block.dependencies) iteratorDependency(dep);
124 }
125
126 if (block.blocks) {
127 for (const b of block.blocks) iteratorBlockPrepare(b);
128 }
129
130 const blockInfo = {
131 modules: blockInfoModules,
132 blocks: blockInfoBlocks
133 };
134 blockInfoMap.set(block, blockInfo);
135 }
136 }
137
138 return blockInfoMap;
139};
140
141/**
142 *
143 * @param {Compilation} compilation the compilation
144 * @param {Entrypoint[]} inputChunkGroups input groups
145 * @param {Map<ChunkGroup, ChunkGroupInfo>} chunkGroupInfoMap mapping from chunk group to available modules
146 * @param {Map<ChunkGroup, ChunkGroupDep[]>} chunkDependencies dependencies for chunk groups
147 * @param {Set<DependenciesBlock>} blocksWithNestedBlocks flag for blocks that have nested blocks
148 * @param {Set<ChunkGroup>} allCreatedChunkGroups filled with all chunk groups that are created here
149 */
150const visitModules = (
151 compilation,
152 inputChunkGroups,
153 chunkGroupInfoMap,
154 chunkDependencies,
155 blocksWithNestedBlocks,
156 allCreatedChunkGroups
157) => {
158 const logger = compilation.getLogger("webpack.buildChunkGraph.visitModules");
159 const { namedChunkGroups } = compilation;
160
161 logger.time("prepare");
162 const blockInfoMap = extraceBlockInfoMap(compilation);
163
164 /** @type {Map<ChunkGroup, { index: number, index2: number }>} */
165 const chunkGroupCounters = new Map();
166 for (const chunkGroup of inputChunkGroups) {
167 chunkGroupCounters.set(chunkGroup, {
168 index: 0,
169 index2: 0
170 });
171 }
172
173 let nextFreeModuleIndex = 0;
174 let nextFreeModuleIndex2 = 0;
175
176 /** @type {Map<DependenciesBlock, ChunkGroup>} */
177 const blockChunkGroups = new Map();
178
179 const ADD_AND_ENTER_MODULE = 0;
180 const ENTER_MODULE = 1;
181 const PROCESS_BLOCK = 2;
182 const LEAVE_MODULE = 3;
183
184 /**
185 * @param {QueueItem[]} queue the queue array (will be mutated)
186 * @param {ChunkGroup} chunkGroup chunk group
187 * @returns {QueueItem[]} the queue array again
188 */
189 const reduceChunkGroupToQueueItem = (queue, chunkGroup) => {
190 for (const chunk of chunkGroup.chunks) {
191 const module = chunk.entryModule;
192 queue.push({
193 action: ENTER_MODULE,
194 block: module,
195 module,
196 chunk,
197 chunkGroup
198 });
199 }
200 chunkGroupInfoMap.set(chunkGroup, {
201 chunkGroup,
202 minAvailableModules: new Set(),
203 minAvailableModulesOwned: true,
204 availableModulesToBeMerged: [],
205 skippedItems: [],
206 resultingAvailableModules: undefined,
207 children: undefined
208 });
209 return queue;
210 };
211
212 // Start with the provided modules/chunks
213 /** @type {QueueItem[]} */
214 let queue = inputChunkGroups
215 .reduce(reduceChunkGroupToQueueItem, [])
216 .reverse();
217 /** @type {Map<ChunkGroup, Set<ChunkGroup>>} */
218 const queueConnect = new Map();
219 /** @type {Set<ChunkGroupInfo>} */
220 const outdatedChunkGroupInfo = new Set();
221 /** @type {QueueItem[]} */
222 let queueDelayed = [];
223
224 logger.timeEnd("prepare");
225
226 /** @type {Module} */
227 let module;
228 /** @type {Chunk} */
229 let chunk;
230 /** @type {ChunkGroup} */
231 let chunkGroup;
232 /** @type {DependenciesBlock} */
233 let block;
234 /** @type {Set<Module>} */
235 let minAvailableModules;
236 /** @type {QueueItem[]} */
237 let skippedItems;
238
239 // For each async Block in graph
240 /**
241 * @param {AsyncDependenciesBlock} b iterating over each Async DepBlock
242 * @returns {void}
243 */
244 const iteratorBlock = b => {
245 // 1. We create a chunk for this Block
246 // but only once (blockChunkGroups map)
247 let c = blockChunkGroups.get(b);
248 if (c === undefined) {
249 c = namedChunkGroups.get(b.chunkName);
250 if (c && c.isInitial()) {
251 compilation.errors.push(
252 new AsyncDependencyToInitialChunkError(b.chunkName, module, b.loc)
253 );
254 c = chunkGroup;
255 } else {
256 c = compilation.addChunkInGroup(
257 b.groupOptions || b.chunkName,
258 module,
259 b.loc,
260 b.request
261 );
262 chunkGroupCounters.set(c, { index: 0, index2: 0 });
263 blockChunkGroups.set(b, c);
264 allCreatedChunkGroups.add(c);
265 }
266 } else {
267 // TODO webpack 5 remove addOptions check
268 if (c.addOptions) c.addOptions(b.groupOptions);
269 c.addOrigin(module, b.loc, b.request);
270 }
271
272 // 2. We store the Block+Chunk mapping as dependency for the chunk
273 let deps = chunkDependencies.get(chunkGroup);
274 if (!deps) chunkDependencies.set(chunkGroup, (deps = []));
275 deps.push({
276 block: b,
277 chunkGroup: c
278 });
279
280 // 3. We create/update the chunk group info
281 let connectList = queueConnect.get(chunkGroup);
282 if (connectList === undefined) {
283 connectList = new Set();
284 queueConnect.set(chunkGroup, connectList);
285 }
286 connectList.add(c);
287
288 // 4. We enqueue the DependenciesBlock for traversal
289 queueDelayed.push({
290 action: PROCESS_BLOCK,
291 block: b,
292 module: module,
293 chunk: c.chunks[0],
294 chunkGroup: c
295 });
296 };
297
298 // Iterative traversal of the Module graph
299 // Recursive would be simpler to write but could result in Stack Overflows
300 while (queue.length) {
301 logger.time("visiting");
302 while (queue.length) {
303 const queueItem = queue.pop();
304 module = queueItem.module;
305 block = queueItem.block;
306 chunk = queueItem.chunk;
307 if (chunkGroup !== queueItem.chunkGroup) {
308 chunkGroup = queueItem.chunkGroup;
309 const chunkGroupInfo = chunkGroupInfoMap.get(chunkGroup);
310 minAvailableModules = chunkGroupInfo.minAvailableModules;
311 skippedItems = chunkGroupInfo.skippedItems;
312 }
313
314 switch (queueItem.action) {
315 case ADD_AND_ENTER_MODULE: {
316 if (minAvailableModules.has(module)) {
317 // already in parent chunks
318 // skip it for now, but enqueue for rechecking when minAvailableModules shrinks
319 skippedItems.push(queueItem);
320 break;
321 }
322 // We connect Module and Chunk when not already done
323 if (chunk.addModule(module)) {
324 module.addChunk(chunk);
325 } else {
326 // already connected, skip it
327 break;
328 }
329 }
330 // fallthrough
331 case ENTER_MODULE: {
332 if (chunkGroup !== undefined) {
333 const index = chunkGroup.getModuleIndex(module);
334 if (index === undefined) {
335 chunkGroup.setModuleIndex(
336 module,
337 chunkGroupCounters.get(chunkGroup).index++
338 );
339 }
340 }
341
342 if (module.index === null) {
343 module.index = nextFreeModuleIndex++;
344 }
345
346 queue.push({
347 action: LEAVE_MODULE,
348 block,
349 module,
350 chunk,
351 chunkGroup
352 });
353 }
354 // fallthrough
355 case PROCESS_BLOCK: {
356 // get prepared block info
357 const blockInfo = blockInfoMap.get(block);
358
359 // Buffer items because order need to be reverse to get indicies correct
360 const skipBuffer = [];
361 const queueBuffer = [];
362 // Traverse all referenced modules
363 for (const refModule of blockInfo.modules) {
364 if (chunk.containsModule(refModule)) {
365 // skip early if already connected
366 continue;
367 }
368 if (minAvailableModules.has(refModule)) {
369 // already in parent chunks, skip it for now
370 skipBuffer.push({
371 action: ADD_AND_ENTER_MODULE,
372 block: refModule,
373 module: refModule,
374 chunk,
375 chunkGroup
376 });
377 continue;
378 }
379 // enqueue the add and enter to enter in the correct order
380 // this is relevant with circular dependencies
381 queueBuffer.push({
382 action: ADD_AND_ENTER_MODULE,
383 block: refModule,
384 module: refModule,
385 chunk,
386 chunkGroup
387 });
388 }
389 // Add buffered items in reversed order
390 for (let i = skipBuffer.length - 1; i >= 0; i--) {
391 skippedItems.push(skipBuffer[i]);
392 }
393 for (let i = queueBuffer.length - 1; i >= 0; i--) {
394 queue.push(queueBuffer[i]);
395 }
396
397 // Traverse all Blocks
398 for (const block of blockInfo.blocks) iteratorBlock(block);
399
400 if (blockInfo.blocks.length > 0 && module !== block) {
401 blocksWithNestedBlocks.add(block);
402 }
403 break;
404 }
405 case LEAVE_MODULE: {
406 if (chunkGroup !== undefined) {
407 const index = chunkGroup.getModuleIndex2(module);
408 if (index === undefined) {
409 chunkGroup.setModuleIndex2(
410 module,
411 chunkGroupCounters.get(chunkGroup).index2++
412 );
413 }
414 }
415
416 if (module.index2 === null) {
417 module.index2 = nextFreeModuleIndex2++;
418 }
419 break;
420 }
421 }
422 }
423 logger.timeEnd("visiting");
424
425 while (queueConnect.size > 0) {
426 logger.time("calculating available modules");
427
428 // Figure out new parents for chunk groups
429 // to get new available modules for these children
430 for (const [chunkGroup, targets] of queueConnect) {
431 const info = chunkGroupInfoMap.get(chunkGroup);
432 let minAvailableModules = info.minAvailableModules;
433
434 // 1. Create a new Set of available modules at this points
435 const resultingAvailableModules = new Set(minAvailableModules);
436 for (const chunk of chunkGroup.chunks) {
437 for (const m of chunk.modulesIterable) {
438 resultingAvailableModules.add(m);
439 }
440 }
441 info.resultingAvailableModules = resultingAvailableModules;
442 if (info.children === undefined) {
443 info.children = targets;
444 } else {
445 for (const target of targets) {
446 info.children.add(target);
447 }
448 }
449
450 // 2. Update chunk group info
451 for (const target of targets) {
452 let chunkGroupInfo = chunkGroupInfoMap.get(target);
453 if (chunkGroupInfo === undefined) {
454 chunkGroupInfo = {
455 chunkGroup: target,
456 minAvailableModules: undefined,
457 minAvailableModulesOwned: undefined,
458 availableModulesToBeMerged: [],
459 skippedItems: [],
460 resultingAvailableModules: undefined,
461 children: undefined
462 };
463 chunkGroupInfoMap.set(target, chunkGroupInfo);
464 }
465 chunkGroupInfo.availableModulesToBeMerged.push(
466 resultingAvailableModules
467 );
468 outdatedChunkGroupInfo.add(chunkGroupInfo);
469 }
470 }
471 queueConnect.clear();
472 logger.timeEnd("calculating available modules");
473
474 if (outdatedChunkGroupInfo.size > 0) {
475 logger.time("merging available modules");
476 // Execute the merge
477 for (const info of outdatedChunkGroupInfo) {
478 const availableModulesToBeMerged = info.availableModulesToBeMerged;
479 let cachedMinAvailableModules = info.minAvailableModules;
480
481 // 1. Get minimal available modules
482 // It doesn't make sense to traverse a chunk again with more available modules.
483 // This step calculates the minimal available modules and skips traversal when
484 // the list didn't shrink.
485 if (availableModulesToBeMerged.length > 1) {
486 availableModulesToBeMerged.sort(bySetSize);
487 }
488 let changed = false;
489 for (const availableModules of availableModulesToBeMerged) {
490 if (cachedMinAvailableModules === undefined) {
491 cachedMinAvailableModules = availableModules;
492 info.minAvailableModules = cachedMinAvailableModules;
493 info.minAvailableModulesOwned = false;
494 changed = true;
495 } else {
496 if (info.minAvailableModulesOwned) {
497 // We own it and can modify it
498 for (const m of cachedMinAvailableModules) {
499 if (!availableModules.has(m)) {
500 cachedMinAvailableModules.delete(m);
501 changed = true;
502 }
503 }
504 } else {
505 for (const m of cachedMinAvailableModules) {
506 if (!availableModules.has(m)) {
507 // cachedMinAvailableModules need to be modified
508 // but we don't own it
509 // construct a new Set as intersection of cachedMinAvailableModules and availableModules
510 /** @type {Set<Module>} */
511 const newSet = new Set();
512 const iterator = cachedMinAvailableModules[
513 Symbol.iterator
514 ]();
515 /** @type {IteratorResult<Module>} */
516 let it;
517 while (!(it = iterator.next()).done) {
518 const module = it.value;
519 if (module === m) break;
520 newSet.add(module);
521 }
522 while (!(it = iterator.next()).done) {
523 const module = it.value;
524 if (availableModules.has(module)) {
525 newSet.add(module);
526 }
527 }
528 cachedMinAvailableModules = newSet;
529 info.minAvailableModulesOwned = true;
530 info.minAvailableModules = newSet;
531
532 // Update the cache from the first queue
533 // if the chunkGroup is currently cached
534 if (chunkGroup === info.chunkGroup) {
535 minAvailableModules = cachedMinAvailableModules;
536 }
537
538 changed = true;
539 break;
540 }
541 }
542 }
543 }
544 }
545 availableModulesToBeMerged.length = 0;
546 if (!changed) continue;
547
548 // 2. Reconsider skipped items
549 for (const queueItem of info.skippedItems) {
550 queue.push(queueItem);
551 }
552 info.skippedItems.length = 0;
553
554 // 3. Reconsider children chunk groups
555 if (info.children !== undefined) {
556 const chunkGroup = info.chunkGroup;
557 for (const c of info.children) {
558 let connectList = queueConnect.get(chunkGroup);
559 if (connectList === undefined) {
560 connectList = new Set();
561 queueConnect.set(chunkGroup, connectList);
562 }
563 connectList.add(c);
564 }
565 }
566 }
567 outdatedChunkGroupInfo.clear();
568 logger.timeEnd("merging available modules");
569 }
570 }
571
572 // Run queueDelayed when all items of the queue are processed
573 // This is important to get the global indicing correct
574 // Async blocks should be processed after all sync blocks are processed
575 if (queue.length === 0) {
576 const tempQueue = queue;
577 queue = queueDelayed.reverse();
578 queueDelayed = tempQueue;
579 }
580 }
581};
582
583/**
584 *
585 * @param {Set<DependenciesBlock>} blocksWithNestedBlocks flag for blocks that have nested blocks
586 * @param {Map<ChunkGroup, ChunkGroupDep[]>} chunkDependencies dependencies for chunk groups
587 * @param {Map<ChunkGroup, ChunkGroupInfo>} chunkGroupInfoMap mapping from chunk group to available modules
588 */
589const connectChunkGroups = (
590 blocksWithNestedBlocks,
591 chunkDependencies,
592 chunkGroupInfoMap
593) => {
594 /** @type {Set<Module>} */
595 let resultingAvailableModules;
596
597 /**
598 * Helper function to check if all modules of a chunk are available
599 *
600 * @param {ChunkGroup} chunkGroup the chunkGroup to scan
601 * @param {Set<Module>} availableModules the comparitor set
602 * @returns {boolean} return true if all modules of a chunk are available
603 */
604 const areModulesAvailable = (chunkGroup, availableModules) => {
605 for (const chunk of chunkGroup.chunks) {
606 for (const module of chunk.modulesIterable) {
607 if (!availableModules.has(module)) return false;
608 }
609 }
610 return true;
611 };
612
613 // For each edge in the basic chunk graph
614 /**
615 * @param {ChunkGroupDep} dep the dependency used for filtering
616 * @returns {boolean} used to filter "edges" (aka Dependencies) that were pointing
617 * to modules that are already available. Also filters circular dependencies in the chunks graph
618 */
619 const filterFn = dep => {
620 const depChunkGroup = dep.chunkGroup;
621 // TODO is this needed?
622 if (blocksWithNestedBlocks.has(dep.block)) return true;
623 if (areModulesAvailable(depChunkGroup, resultingAvailableModules)) {
624 return false; // break all modules are already available
625 }
626 return true;
627 };
628
629 // For all deps, check if chunk groups need to be connected
630 for (const [chunkGroup, deps] of chunkDependencies) {
631 if (deps.length === 0) continue;
632
633 // 1. Get info from chunk group info map
634 const info = chunkGroupInfoMap.get(chunkGroup);
635 resultingAvailableModules = info.resultingAvailableModules;
636
637 // 2. Foreach edge
638 for (let i = 0; i < deps.length; i++) {
639 const dep = deps[i];
640
641 // Filter inline, rather than creating a new array from `.filter()`
642 // TODO check if inlining filterFn makes sense here
643 if (!filterFn(dep)) {
644 continue;
645 }
646 const depChunkGroup = dep.chunkGroup;
647 const depBlock = dep.block;
648
649 // 5. Connect block with chunk
650 GraphHelpers.connectDependenciesBlockAndChunkGroup(
651 depBlock,
652 depChunkGroup
653 );
654
655 // 6. Connect chunk with parent
656 GraphHelpers.connectChunkGroupParentAndChild(chunkGroup, depChunkGroup);
657 }
658 }
659};
660
661/**
662 * Remove all unconnected chunk groups
663 * @param {Compilation} compilation the compilation
664 * @param {Iterable<ChunkGroup>} allCreatedChunkGroups all chunk groups that where created before
665 */
666const cleanupUnconnectedGroups = (compilation, allCreatedChunkGroups) => {
667 for (const chunkGroup of allCreatedChunkGroups) {
668 if (chunkGroup.getNumberOfParents() === 0) {
669 for (const chunk of chunkGroup.chunks) {
670 const idx = compilation.chunks.indexOf(chunk);
671 if (idx >= 0) compilation.chunks.splice(idx, 1);
672 chunk.remove("unconnected");
673 }
674 chunkGroup.remove("unconnected");
675 }
676 }
677};
678
679/**
680 * This method creates the Chunk graph from the Module graph
681 * @param {Compilation} compilation the compilation
682 * @param {Entrypoint[]} inputChunkGroups chunk groups which are processed
683 * @returns {void}
684 */
685const buildChunkGraph = (compilation, inputChunkGroups) => {
686 // SHARED STATE
687
688 /** @type {Map<ChunkGroup, ChunkGroupDep[]>} */
689 const chunkDependencies = new Map();
690
691 /** @type {Set<ChunkGroup>} */
692 const allCreatedChunkGroups = new Set();
693
694 /** @type {Map<ChunkGroup, ChunkGroupInfo>} */
695 const chunkGroupInfoMap = new Map();
696
697 /** @type {Set<DependenciesBlock>} */
698 const blocksWithNestedBlocks = new Set();
699
700 // PART ONE
701
702 visitModules(
703 compilation,
704 inputChunkGroups,
705 chunkGroupInfoMap,
706 chunkDependencies,
707 blocksWithNestedBlocks,
708 allCreatedChunkGroups
709 );
710
711 // PART TWO
712
713 connectChunkGroups(
714 blocksWithNestedBlocks,
715 chunkDependencies,
716 chunkGroupInfoMap
717 );
718
719 // Cleaup work
720
721 cleanupUnconnectedGroups(compilation, allCreatedChunkGroups);
722};
723
724module.exports = buildChunkGraph;