metro 0.80.2 → 0.80.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,17 +37,14 @@ import type {
37
37
  GraphInputOptions,
38
38
  MixedOutput,
39
39
  Module,
40
+ ModuleData,
40
41
  Options,
41
42
  TransformInputOptions,
42
- TransformResultDependency,
43
43
  } from './types.flow';
44
44
 
45
- import {
46
- deriveAbsolutePathFromContext,
47
- fileMatchesContext,
48
- } from '../lib/contextModule';
45
+ import {fileMatchesContext} from '../lib/contextModule';
49
46
  import CountingSet from '../lib/CountingSet';
50
- import * as path from 'path';
47
+ import {buildSubgraph} from './buildSubgraph';
51
48
 
52
49
  const invariant = require('invariant');
53
50
  const nullthrows = require('nullthrows');
@@ -80,17 +77,18 @@ export type Result<T> = {
80
77
  * files have been modified. This allows to return the added modules before the
81
78
  * modified ones (which is useful for things like Hot Module Reloading).
82
79
  **/
83
- type Delta = $ReadOnly<{
80
+ type Delta<T> = $ReadOnly<{
84
81
  // `added` and `deleted` are mutually exclusive.
85
- // Internally, a module can be in both `modified` and (either) `added` or
86
- // `deleted`. We fix this up before returning the delta to the client.
82
+ // Internally, a module can be in both `touched` and (either) `added` or
83
+ // `deleted`. Before returning the result, we'll calculate
84
+ // modified = touched - added - deleted.
87
85
  added: Set<string>,
88
- modified: Set<string>,
86
+ touched: Set<string>,
89
87
  deleted: Set<string>,
90
88
 
91
- // A place to temporarily track inverse dependencies for a module while it is
92
- // being processed but has not been added to `graph.dependencies` yet.
93
- earlyInverseDependencies: Map<string, CountingSet<string>>,
89
+ updatedModuleData: $ReadOnlyMap<string, ModuleData<T>>,
90
+ baseModuleData: Map<string, ModuleData<T>>,
91
+ errors: $ReadOnlyMap<string, Error>,
94
92
  }>;
95
93
 
96
94
  type InternalOptions<T> = $ReadOnly<{
@@ -122,6 +120,14 @@ function getInternalOptions<T>({
122
120
  };
123
121
  }
124
122
 
123
+ function isWeakOrLazy<T>(
124
+ dependency: Dependency,
125
+ options: InternalOptions<T>,
126
+ ): boolean {
127
+ const asyncType = dependency.data.data.asyncType;
128
+ return asyncType === 'weak' || (asyncType != null && options.lazy);
129
+ }
130
+
125
131
  export class Graph<T = MixedOutput> {
126
132
  +entryPoints: $ReadOnlySet<string>;
127
133
  +transformOptions: TransformInputOptions;
@@ -165,87 +171,124 @@ export class Graph<T = MixedOutput> {
165
171
  paths: $ReadOnlyArray<string>,
166
172
  options: Options<T>,
167
173
  ): Promise<Result<T>> {
168
- const delta = {
169
- added: new Set<string>(),
170
- modified: new Set<string>(),
171
- deleted: new Set<string>(),
172
- earlyInverseDependencies: new Map<string, CountingSet<string>>(),
173
- };
174
-
175
174
  const internalOptions = getInternalOptions(options);
176
175
 
177
- // Record the paths that are part of the dependency graph before we start
178
- // traversing - we'll use this to ensure we don't report modules modified
179
- // that only exist as part of the graph mid-traversal, and to eliminate
180
- // modules that end up in the same state that they started from the delta.
181
- const originalModules = new Map<string, Module<T>>();
182
- for (const path of paths) {
183
- const originalModule = this.dependencies.get(path);
184
- if (originalModule) {
185
- originalModules.set(path, originalModule);
176
+ const modifiedPathsInBaseGraph = new Set(
177
+ paths.filter(path => this.dependencies.has(path)),
178
+ );
179
+
180
+ const allModifiedPaths = new Set(paths);
181
+
182
+ const delta = await this._buildDelta(
183
+ modifiedPathsInBaseGraph,
184
+ internalOptions,
185
+ // Traverse new or modified paths
186
+ absolutePath =>
187
+ !this.dependencies.has(absolutePath) ||
188
+ allModifiedPaths.has(absolutePath),
189
+ );
190
+
191
+ // If we have errors we might need to roll back any changes - take
192
+ // snapshots of all modified modules at the base state. We'll also snapshot
193
+ // unmodified modules that become unreachable as they are released, so that
194
+ // we have everything we need to restore the graph to base.
195
+ if (delta.errors.size > 0) {
196
+ for (const modified of modifiedPathsInBaseGraph) {
197
+ delta.baseModuleData.set(
198
+ modified,
199
+ this._moduleSnapshot(nullthrows(this.dependencies.get(modified))),
200
+ );
186
201
  }
187
202
  }
188
203
 
189
- for (const [path] of originalModules) {
190
- // Traverse over modules that are part of the dependency graph.
191
- //
192
- // Note: A given path may not be part of the graph *at this time*, in
193
- // particular it may have been removed since we started traversing, but
194
- // in that case the path will be visited if and when we add it back to
195
- // the graph in a subsequent iteration.
196
- if (this.dependencies.has(path)) {
197
- await this._traverseDependenciesForSingleFile(
198
- path,
199
- delta,
200
- internalOptions,
201
- );
204
+ // Commit changes in a subtractive pass and then an additive pass - this
205
+ // ensures that any errors encountered on the additive pass would also be
206
+ // encountered on a fresh build (implying legitimate errors in the graph,
207
+ // rather than an error in a module that's no longer reachable).
208
+ for (const modified of modifiedPathsInBaseGraph) {
209
+ // Skip this module if it has errors. Hopefully it will be removed -
210
+ // if not, we'll throw during the additive pass.
211
+ if (delta.errors.has(modified)) {
212
+ continue;
213
+ }
214
+ const module = this.dependencies.get(modified);
215
+ // The module may have already been released from the graph - we'll readd
216
+ // it if necessary later.
217
+ if (module == null) {
218
+ continue;
202
219
  }
220
+ // Process the transform result and dependency removals. This should
221
+ // never encounter an error.
222
+ this._recursivelyCommitModule(modified, delta, internalOptions, {
223
+ onlyRemove: true,
224
+ });
203
225
  }
204
226
 
227
+ // Ensure we have released any unreachable modules before the additive
228
+ // pass.
205
229
  this._collectCycles(delta, internalOptions);
206
230
 
231
+ // Additive pass - any errors we encounter here should be thrown after
232
+ // rolling back the commit.
233
+ try {
234
+ for (const modified of modifiedPathsInBaseGraph) {
235
+ const module = this.dependencies.get(modified);
236
+ // The module may have already been released from the graph (it may yet
237
+ // be readded via another dependency).
238
+ if (module == null) {
239
+ continue;
240
+ }
241
+
242
+ this._recursivelyCommitModule(modified, delta, internalOptions);
243
+ }
244
+ } catch (error) {
245
+ // Roll back to base before re-throwing.
246
+ const rollbackDelta: Delta<T> = {
247
+ added: delta.added,
248
+ deleted: delta.deleted,
249
+ touched: new Set(),
250
+ updatedModuleData: delta.baseModuleData,
251
+ baseModuleData: new Map(),
252
+ errors: new Map(),
253
+ };
254
+ for (const modified of modifiedPathsInBaseGraph) {
255
+ const module = this.dependencies.get(modified);
256
+ // The module may have already been released from the graph (it may yet
257
+ // be readded via another dependency).
258
+ if (module == null) {
259
+ continue;
260
+ }
261
+ // Set the module and descendants back to base state.
262
+ this._recursivelyCommitModule(modified, rollbackDelta, internalOptions);
263
+ }
264
+ // Collect cycles again after rolling back. There's no need if we're
265
+ // not rolling back, because we have not removed any edges.
266
+ this._collectCycles(delta, internalOptions);
267
+
268
+ // Cheap check to validate the rollback.
269
+ invariant(
270
+ rollbackDelta.added.size === 0 && rollbackDelta.deleted.size === 0,
271
+ 'attempted to roll back a graph commit but there were still changes',
272
+ );
273
+
274
+ // Re-throw the transform or resolution error originally seen by
275
+ // `buildSubgraph`.
276
+ throw error;
277
+ }
278
+
207
279
  const added = new Map<string, Module<T>>();
208
280
  for (const path of delta.added) {
209
281
  added.set(path, nullthrows(this.dependencies.get(path)));
210
282
  }
211
283
 
212
284
  const modified = new Map<string, Module<T>>();
213
-
214
- // A path in delta.modified has been processed during this traversal,
215
- // but may not actually differ, may be new, or may have been deleted after
216
- // processing. The actually-modified modules are the intersection of
217
- // delta.modified with the pre-existing paths, minus modules deleted.
218
- for (const [path, originalModule] of originalModules) {
219
- invariant(
220
- !delta.added.has(path),
221
- 'delta.added has %s, but this path was already in the graph.',
222
- path,
223
- );
224
- if (delta.modified.has(path)) {
225
- // It's expected that a module may be both modified and subsequently
226
- // deleted - we'll only return it as deleted.
227
- if (!delta.deleted.has(path)) {
228
- // If a module existed before and has not been deleted, it must be
229
- // in the dependencies map.
230
- const newModule = nullthrows(this.dependencies.get(path));
231
- if (
232
- // Module.dependencies is mutable, so it's not obviously the case
233
- // that referential equality implies no modification. However, we
234
- // only mutate dependencies in two cases:
235
- // 1. Within _processModule. In that case, we always mutate a new
236
- // module and set a new reference in this.dependencies.
237
- // 2. During _releaseModule, when recursively removing
238
- // dependencies. In that case, we immediately discard the module
239
- // object.
240
- // TODO: Refactor for more explicit immutability
241
- newModule !== originalModule ||
242
- transfromOutputMayDiffer(newModule, originalModule) ||
243
- // $FlowFixMe[incompatible-call]
244
- !allDependenciesEqual(newModule, originalModule)
245
- ) {
246
- modified.set(path, newModule);
247
- }
248
- }
285
+ for (const path of modifiedPathsInBaseGraph) {
286
+ if (
287
+ delta.touched.has(path) &&
288
+ !delta.deleted.has(path) &&
289
+ !delta.added.has(path)
290
+ ) {
291
+ modified.set(path, nullthrows(this.dependencies.get(path)));
249
292
  }
250
293
  }
251
294
 
@@ -257,13 +300,6 @@ export class Graph<T = MixedOutput> {
257
300
  }
258
301
 
259
302
  async initialTraverseDependencies(options: Options<T>): Promise<Result<T>> {
260
- const delta = {
261
- added: new Set<string>(),
262
- modified: new Set<string>(),
263
- deleted: new Set<string>(),
264
- earlyInverseDependencies: new Map<string, CountingSet<string>>(),
265
- };
266
-
267
303
  const internalOptions = getInternalOptions(options);
268
304
 
269
305
  invariant(
@@ -280,11 +316,20 @@ export class Graph<T = MixedOutput> {
280
316
  this.#gc.color.set(path, 'black');
281
317
  }
282
318
 
283
- await Promise.all(
284
- [...this.entryPoints].map((path: string) =>
285
- this._traverseDependenciesForSingleFile(path, delta, internalOptions),
286
- ),
287
- );
319
+ const delta = await this._buildDelta(this.entryPoints, internalOptions);
320
+
321
+ if (delta.errors.size > 0) {
322
+ // If we encountered any errors during traversal, throw one of them.
323
+ // Since errors are encountered in a non-deterministic order, even on
324
+ // fresh builds, it's valid to arbitrarily pick the first.
325
+ throw delta.errors.values().next().value;
326
+ }
327
+
328
+ for (const path of this.entryPoints) {
329
+ // We have already thrown on userland errors in the delta, so any error
330
+ // encountered here would be exceptional and fatal.
331
+ this._recursivelyCommitModule(path, delta, internalOptions);
332
+ }
288
333
 
289
334
  this.reorderGraph({
290
335
  shallow: options.shallow,
@@ -297,57 +342,86 @@ export class Graph<T = MixedOutput> {
297
342
  };
298
343
  }
299
344
 
300
- async _traverseDependenciesForSingleFile(
301
- path: string,
302
- delta: Delta,
345
+ async _buildDelta(
346
+ pathsToVisit: $ReadOnlySet<string>,
303
347
  options: InternalOptions<T>,
304
- ): Promise<void> {
305
- options.onDependencyAdd();
306
-
307
- await this._processModule(path, delta, options);
348
+ moduleFilter?: (path: string) => boolean,
349
+ ): Promise<Delta<T>> {
350
+ const subGraph = await buildSubgraph(pathsToVisit, this.#resolvedContexts, {
351
+ resolve: options.resolve,
352
+ transform: async (absolutePath, requireContext) => {
353
+ options.onDependencyAdd();
354
+ const result = await options.transform(absolutePath, requireContext);
355
+ options.onDependencyAdded();
356
+ return result;
357
+ },
358
+ shouldTraverse: (dependency: Dependency) => {
359
+ if (options.shallow || isWeakOrLazy(dependency, options)) {
360
+ return false;
361
+ }
362
+ return moduleFilter == null || moduleFilter(dependency.absolutePath);
363
+ },
364
+ });
308
365
 
309
- options.onDependencyAdded();
366
+ return {
367
+ added: new Set(),
368
+ touched: new Set(),
369
+ deleted: new Set(),
370
+ updatedModuleData: subGraph.moduleData,
371
+ baseModuleData: new Map(),
372
+ errors: subGraph.errors,
373
+ };
310
374
  }
311
375
 
312
- async _processModule(
376
+ _recursivelyCommitModule(
313
377
  path: string,
314
- delta: Delta,
378
+ delta: Delta<T>,
315
379
  options: InternalOptions<T>,
316
- ): Promise<Module<T>> {
317
- const resolvedContext = this.#resolvedContexts.get(path);
318
-
319
- // Transform the file via the given option.
320
- // TODO: Unbind the transform method from options
321
- const result = await options.transform(path, resolvedContext);
322
-
323
- // Get the absolute path of all sub-dependencies (some of them could have been
324
- // moved but maintain the same relative path).
325
- const currentDependencies = this._resolveDependencies(
326
- path,
327
- result.dependencies,
328
- options,
329
- );
380
+ commitOptions: $ReadOnly<{
381
+ onlyRemove: boolean,
382
+ }> = {onlyRemove: false},
383
+ ): Module<T> {
384
+ if (delta.errors.has(path)) {
385
+ throw delta.errors.get(path);
386
+ }
330
387
 
331
388
  const previousModule = this.dependencies.get(path);
389
+ const currentModule: ModuleData<T> = nullthrows(
390
+ delta.updatedModuleData.get(path) ?? delta.baseModuleData.get(path),
391
+ );
332
392
 
333
393
  const previousDependencies = previousModule?.dependencies ?? new Map();
394
+ const {
395
+ dependencies: currentDependencies,
396
+ resolvedContexts,
397
+ ...transformResult
398
+ } = currentModule;
334
399
 
335
400
  const nextModule = {
336
401
  ...(previousModule ?? {
337
- inverseDependencies:
338
- delta.earlyInverseDependencies.get(path) ?? new CountingSet(),
402
+ inverseDependencies: new CountingSet(),
339
403
  path,
340
404
  }),
405
+ ...transformResult,
341
406
  dependencies: new Map(previousDependencies),
342
- getSource: result.getSource,
343
- output: result.output,
344
- unstable_transformResultKey: result.unstable_transformResultKey,
345
407
  };
346
408
 
347
409
  // Update the module information.
348
410
  this.dependencies.set(nextModule.path, nextModule);
349
411
 
350
- // Diff dependencies (1/2): remove dependencies that have changed or been removed.
412
+ if (previousModule == null) {
413
+ // If the module is not currently in the graph, it is either new or was
414
+ // released earlier in the commit.
415
+ if (delta.deleted.has(path)) {
416
+ // Mark the addition by clearing a prior deletion.
417
+ delta.deleted.delete(path);
418
+ } else {
419
+ // Mark the addition in the added set.
420
+ delta.added.add(path);
421
+ }
422
+ }
423
+
424
+ // Diff dependencies (1/3): remove dependencies that have changed or been removed.
351
425
  let dependenciesRemoved = false;
352
426
  for (const [key, prevDependency] of previousDependencies) {
353
427
  const curDependency = currentDependencies.get(key);
@@ -360,41 +434,62 @@ export class Graph<T = MixedOutput> {
360
434
  }
361
435
  }
362
436
 
363
- // Diff dependencies (2/2): add dependencies that have changed or been added.
364
- const addDependencyPromises = [];
365
- for (const [key, curDependency] of currentDependencies) {
366
- const prevDependency = previousDependencies.get(key);
367
- if (
368
- !prevDependency ||
369
- !dependenciesEqual(prevDependency, curDependency, options)
370
- ) {
371
- addDependencyPromises.push(
372
- this._addDependency(nextModule, key, curDependency, delta, options),
373
- );
437
+ // Diff dependencies (2/3): add dependencies that have changed or been added.
438
+ let dependenciesAdded = false;
439
+ if (!commitOptions.onlyRemove) {
440
+ for (const [key, curDependency] of currentDependencies) {
441
+ const prevDependency = previousDependencies.get(key);
442
+ if (
443
+ !prevDependency ||
444
+ !dependenciesEqual(prevDependency, curDependency, options)
445
+ ) {
446
+ dependenciesAdded = true;
447
+ this._addDependency(
448
+ nextModule,
449
+ key,
450
+ curDependency,
451
+ resolvedContexts.get(key),
452
+ delta,
453
+ options,
454
+ );
455
+ }
374
456
  }
375
457
  }
376
458
 
459
+ // Diff dependencies (3/3): detect changes in the ordering of dependency
460
+ // keys, which must be committed even if no other changes were made.
461
+ const previousDependencyKeys = [...previousDependencies.keys()];
462
+ const dependencyKeysChangedOrReordered =
463
+ currentDependencies.size !== previousDependencies.size ||
464
+ [...currentDependencies.keys()].some(
465
+ (currentKey, index) => currentKey !== previousDependencyKeys[index],
466
+ );
467
+
377
468
  if (
378
- previousModule &&
379
- !transfromOutputMayDiffer(previousModule, nextModule) &&
469
+ previousModule != null &&
470
+ !transformOutputMayDiffer(previousModule, nextModule) &&
380
471
  !dependenciesRemoved &&
381
- addDependencyPromises.length === 0
472
+ !dependenciesAdded &&
473
+ !dependencyKeysChangedOrReordered
382
474
  ) {
383
475
  // We have not operated on nextModule, so restore previousModule
384
- // to aid diffing.
476
+ // to aid diffing. Don't add this path to delta.touched.
385
477
  this.dependencies.set(previousModule.path, previousModule);
386
478
  return previousModule;
387
479
  }
388
480
 
389
- delta.modified.add(path);
481
+ delta.touched.add(path);
390
482
 
391
- await Promise.all(addDependencyPromises);
483
+ // Replace dependencies with the correctly-ordered version, matching the
484
+ // transform output. Because this assignment does not add or remove edges,
485
+ // it does NOT invalidate any of the garbage collection state.
392
486
 
393
- // Replace dependencies with the correctly-ordered version. As long as all
394
- // the above promises have resolved, this will be the same map but without
395
- // the added nondeterminism of promise resolution order. Because this
396
- // assignment does not add or remove edges, it does NOT invalidate any of the
397
- // garbage collection state.
487
+ // A subtractive pass only partially commits modules, so our dependencies
488
+ // are not generally complete yet. We'll address ordering in the next pass
489
+ // after processing additions.
490
+ if (commitOptions.onlyRemove) {
491
+ return nextModule;
492
+ }
398
493
 
399
494
  // Catch obvious errors with a cheap assertion.
400
495
  invariant(
@@ -402,18 +497,19 @@ export class Graph<T = MixedOutput> {
402
497
  'Failed to add the correct dependencies',
403
498
  );
404
499
 
405
- nextModule.dependencies = currentDependencies;
500
+ nextModule.dependencies = new Map(currentDependencies);
406
501
 
407
502
  return nextModule;
408
503
  }
409
504
 
410
- async _addDependency(
505
+ _addDependency(
411
506
  parentModule: Module<T>,
412
507
  key: string,
413
508
  dependency: Dependency,
414
- delta: Delta,
509
+ requireContext: ?RequireContext,
510
+ delta: Delta<T>,
415
511
  options: InternalOptions<T>,
416
- ): Promise<void> {
512
+ ): void {
417
513
  const path = dependency.absolutePath;
418
514
 
419
515
  // The module may already exist, in which case we just need to update some
@@ -431,38 +527,38 @@ export class Graph<T = MixedOutput> {
431
527
  this._incrementImportBundleReference(dependency, parentModule);
432
528
  } else {
433
529
  if (!module) {
434
- // Add a new node to the graph.
435
- const earlyInverseDependencies =
436
- delta.earlyInverseDependencies.get(path);
437
- if (earlyInverseDependencies) {
438
- // This module is being transformed at the moment in parallel, so we
439
- // should only mark its parent as an inverse dependency.
440
- earlyInverseDependencies.add(parentModule.path);
441
- } else {
442
- if (delta.deleted.has(path)) {
443
- // Mark the addition by clearing a prior deletion.
444
- delta.deleted.delete(path);
445
- } else {
446
- // Mark the addition in the added set.
447
- delta.added.add(path);
530
+ try {
531
+ module = this._recursivelyCommitModule(path, delta, options);
532
+ } catch (error) {
533
+ // If we couldn't add this module but it was added to the graph
534
+ // before failing on a sub-dependency, it may be orphaned. Mark it as
535
+ // a possible garbage root.
536
+ const module = this.dependencies.get(path);
537
+ if (module) {
538
+ if (module.inverseDependencies.size > 0) {
539
+ this._markAsPossibleCycleRoot(module);
540
+ } else {
541
+ this._releaseModule(module, delta, options);
542
+ }
448
543
  }
449
- delta.earlyInverseDependencies.set(path, new CountingSet());
450
-
451
- options.onDependencyAdd();
452
- module = await this._processModule(path, delta, options);
453
- options.onDependencyAdded();
454
-
455
- this.dependencies.set(module.path, module);
544
+ throw error;
456
545
  }
457
546
  }
458
- if (module) {
459
- // We either added a new node to the graph, or we're updating an existing one.
460
- module.inverseDependencies.add(parentModule.path);
461
- this._markModuleInUse(module);
462
- }
547
+
548
+ // We either added a new node to the graph, or we're updating an existing one.
549
+ module.inverseDependencies.add(parentModule.path);
550
+ this._markModuleInUse(module);
463
551
  }
464
552
 
465
- // Always update the parent's dependency map.
553
+ if (requireContext) {
554
+ this.#resolvedContexts.set(path, requireContext);
555
+ } else {
556
+ // This dependency may have existed previously as a require.context -
557
+ // clean it up.
558
+ this.#resolvedContexts.delete(path);
559
+ }
560
+
561
+ // Update the parent's dependency map unless we failed to add a dependency.
466
562
  // This means the parent's dependencies can get desynced from
467
563
  // inverseDependencies and the other fields in the case of lazy edges.
468
564
  // Not an optimal representation :(
@@ -473,7 +569,7 @@ export class Graph<T = MixedOutput> {
473
569
  parentModule: Module<T>,
474
570
  key: string,
475
571
  dependency: Dependency,
476
- delta: Delta,
572
+ delta: Delta<T>,
477
573
  options: InternalOptions<T>,
478
574
  ): void {
479
575
  parentModule.dependencies.delete(key);
@@ -538,84 +634,6 @@ export class Graph<T = MixedOutput> {
538
634
  yield* this.#importBundleNodes.get(filePath)?.inverseDependencies ?? [];
539
635
  }
540
636
 
541
- _resolveDependencies(
542
- parentPath: string,
543
- dependencies: $ReadOnlyArray<TransformResultDependency>,
544
- options: InternalOptions<T>,
545
- ): Map<string, Dependency> {
546
- const maybeResolvedDeps = new Map<
547
- string,
548
- void | {absolutePath: string, data: TransformResultDependency},
549
- >();
550
- for (const dep of dependencies) {
551
- let resolvedDep;
552
-
553
- // `require.context`
554
- const {contextParams} = dep.data;
555
- if (contextParams) {
556
- // Ensure the filepath has uniqueness applied to ensure multiple `require.context`
557
- // statements can be used to target the same file with different properties.
558
- const from = path.join(parentPath, '..', dep.name);
559
- const absolutePath = deriveAbsolutePathFromContext(from, contextParams);
560
-
561
- const resolvedContext: RequireContext = {
562
- from,
563
- mode: contextParams.mode,
564
- recursive: contextParams.recursive,
565
- filter: new RegExp(
566
- contextParams.filter.pattern,
567
- contextParams.filter.flags,
568
- ),
569
- };
570
-
571
- this.#resolvedContexts.set(absolutePath, resolvedContext);
572
-
573
- resolvedDep = {
574
- absolutePath,
575
- data: dep,
576
- };
577
- } else {
578
- try {
579
- resolvedDep = {
580
- absolutePath: options.resolve(parentPath, dep).filePath,
581
- data: dep,
582
- };
583
-
584
- // This dependency may have existed previously as a require.context -
585
- // clean it up.
586
- this.#resolvedContexts.delete(resolvedDep.absolutePath);
587
- } catch (error) {
588
- // Ignore unavailable optional dependencies. They are guarded
589
- // with a try-catch block and will be handled during runtime.
590
- if (dep.data.isOptional !== true) {
591
- throw error;
592
- }
593
- }
594
- }
595
-
596
- const key = dep.data.key;
597
- if (maybeResolvedDeps.has(key)) {
598
- throw new Error(
599
- `resolveDependencies: Found duplicate dependency key '${key}' in ${parentPath}`,
600
- );
601
- }
602
- maybeResolvedDeps.set(key, resolvedDep);
603
- }
604
-
605
- const resolvedDeps = new Map<string, Dependency>();
606
- // Return just the dependencies we successfully resolved.
607
- // FIXME: This has a bad bug affecting all dependencies *after* an unresolved
608
- // optional dependency. We'll need to propagate the nulls all the way to the
609
- // serializer and the require() runtime to keep the dependency map from being
610
- // desynced from the contents of the module.
611
- for (const [key, resolvedDep] of maybeResolvedDeps) {
612
- if (resolvedDep) {
613
- resolvedDeps.set(key, resolvedDep);
614
- }
615
- }
616
- return resolvedDeps;
617
- }
618
-
619
637
  /**
620
638
  * Re-traverse the dependency graph in DFS order to reorder the modules and
621
639
  * guarantee the same order between runs. This method mutates the passed graph.
@@ -716,18 +734,54 @@ export class Graph<T = MixedOutput> {
716
734
  options: InternalOptions<T>,
717
735
  ): Iterator<Module<T>> {
718
736
  for (const dependency of module.dependencies.values()) {
719
- const asyncType = dependency.data.data.asyncType;
720
- if (asyncType === 'weak' || (options.lazy && asyncType != null)) {
737
+ if (isWeakOrLazy(dependency, options)) {
721
738
  continue;
722
739
  }
723
740
  yield nullthrows(this.dependencies.get(dependency.absolutePath));
724
741
  }
725
742
  }
726
743
 
744
+ _moduleSnapshot(module: Module<T>): ModuleData<T> {
745
+ const {dependencies, getSource, output, unstable_transformResultKey} =
746
+ module;
747
+
748
+ const resolvedContexts: Map<string, RequireContext> = new Map();
749
+ for (const [key, dependency] of dependencies) {
750
+ const resolvedContext = this.#resolvedContexts.get(
751
+ dependency.absolutePath,
752
+ );
753
+ if (resolvedContext != null) {
754
+ resolvedContexts.set(key, resolvedContext);
755
+ }
756
+ }
757
+ return {
758
+ dependencies: new Map(dependencies),
759
+ resolvedContexts,
760
+ getSource,
761
+ output,
762
+ unstable_transformResultKey,
763
+ };
764
+ }
765
+
727
766
  // Delete an unreachable module (and its outbound edges) from the graph
728
767
  // immediately.
729
768
  // Called when the reference count of a module has reached 0.
730
- _releaseModule(module: Module<T>, delta: Delta, options: InternalOptions<T>) {
769
+ _releaseModule(
770
+ module: Module<T>,
771
+ delta: Delta<T>,
772
+ options: InternalOptions<T>,
773
+ ) {
774
+ if (
775
+ !delta.updatedModuleData.has(module.path) &&
776
+ !delta.baseModuleData.has(module.path)
777
+ ) {
778
+ // Before releasing a module, take a snapshot of the data we might need
779
+ // to reintroduce it to the graph later in this commit. As it is not
780
+ // already present in updatedModuleData we can infer it has not been modified,
781
+ // so the transform output and dependencies we copy here are current.
782
+ delta.baseModuleData.set(module.path, this._moduleSnapshot(module));
783
+ }
784
+
731
785
  for (const [key, dependency] of module.dependencies) {
732
786
  this._removeDependency(module, key, dependency, delta, options);
733
787
  }
@@ -736,7 +790,7 @@ export class Graph<T = MixedOutput> {
736
790
  }
737
791
 
738
792
  // Delete an unreachable module from the graph.
739
- _freeModule(module: Module<T>, delta: Delta) {
793
+ _freeModule(module: Module<T>, delta: Delta<T>) {
740
794
  if (delta.added.has(module.path)) {
741
795
  // Mark the deletion by clearing a prior addition.
742
796
  delta.added.delete(module.path);
@@ -749,7 +803,6 @@ export class Graph<T = MixedOutput> {
749
803
  // Clean up all the state associated with this module in order to correctly
750
804
  // re-add it if we encounter it again.
751
805
  this.dependencies.delete(module.path);
752
- delta.earlyInverseDependencies.delete(module.path);
753
806
  this.#gc.possibleCycleRoots.delete(module.path);
754
807
  this.#gc.color.delete(module.path);
755
808
  this.#resolvedContexts.delete(module.path);
@@ -757,14 +810,14 @@ export class Graph<T = MixedOutput> {
757
810
 
758
811
  // Mark a module as a possible cycle root
759
812
  _markAsPossibleCycleRoot(module: Module<T>) {
760
- if (nullthrows(this.#gc.color.get(module.path)) !== 'purple') {
813
+ if (this.#gc.color.get(module.path) !== 'purple') {
761
814
  this.#gc.color.set(module.path, 'purple');
762
815
  this.#gc.possibleCycleRoots.add(module.path);
763
816
  }
764
817
  }
765
818
 
766
819
  // Collect any unreachable cycles in the graph.
767
- _collectCycles(delta: Delta, options: InternalOptions<T>) {
820
+ _collectCycles(delta: Delta<T>, options: InternalOptions<T>) {
768
821
  // Mark recursively from roots (trial deletion)
769
822
  for (const path of this.#gc.possibleCycleRoots) {
770
823
  const module = nullthrows(this.dependencies.get(path));
@@ -836,7 +889,7 @@ export class Graph<T = MixedOutput> {
836
889
  }
837
890
  }
838
891
 
839
- _collectWhite(module: Module<T>, delta: Delta) {
892
+ _collectWhite(module: Module<T>, delta: Delta<T>) {
840
893
  const color = nullthrows(this.#gc.color.get(module.path));
841
894
  if (color === 'white' && !this.#gc.possibleCycleRoots.has(module.path)) {
842
895
  this.#gc.color.set(module.path, 'black');
@@ -867,23 +920,6 @@ function dependenciesEqual(
867
920
  );
868
921
  }
869
922
 
870
- function allDependenciesEqual<T>(
871
- a: Module<T>,
872
- b: Module<T>,
873
- options: $ReadOnly<{lazy: boolean, ...}>,
874
- ): boolean {
875
- if (a.dependencies.size !== b.dependencies.size) {
876
- return false;
877
- }
878
- for (const [key, depA] of a.dependencies) {
879
- const depB = b.dependencies.get(key);
880
- if (!depB || !dependenciesEqual(depA, depB, options)) {
881
- return false;
882
- }
883
- }
884
- return true;
885
- }
886
-
887
923
  function contextParamsEqual(
888
924
  a: ?RequireContextParams,
889
925
  b: ?RequireContextParams,
@@ -900,7 +936,7 @@ function contextParamsEqual(
900
936
  );
901
937
  }
902
938
 
903
- function transfromOutputMayDiffer<T>(a: Module<T>, b: Module<T>): boolean {
939
+ function transformOutputMayDiffer<T>(a: Module<T>, b: Module<T>): boolean {
904
940
  return (
905
941
  a.unstable_transformResultKey == null ||
906
942
  a.unstable_transformResultKey !== b.unstable_transformResultKey