@parcel/graph 3.2.1-dev.3195 → 3.2.1-dev.3198

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.
@@ -240,7 +240,7 @@ class AdjacencyList {
240
240
  *
241
241
  * Note that this method does not increment the node count
242
242
  * (that only happens in `addEdge`), it _may_ preemptively resize
243
- * the nodes array if it is at capacity, under the asumption that
243
+ * the nodes array if it is at capacity, under the assumption that
244
244
  * at least 1 edge to or from this new node will be added.
245
245
  *
246
246
  * Returns the id of the added node.
package/lib/Graph.js CHANGED
@@ -7,6 +7,13 @@ exports.default = exports.ALL_EDGE_TYPES = void 0;
7
7
  exports.mapVisitor = mapVisitor;
8
8
  var _types = require("./types");
9
9
  var _AdjacencyList = _interopRequireDefault(require("./AdjacencyList"));
10
+ function _featureFlags() {
11
+ const data = require("@parcel/feature-flags");
12
+ _featureFlags = function () {
13
+ return data;
14
+ };
15
+ return data;
16
+ }
10
17
  var _BitSet = require("./BitSet");
11
18
  function _nullthrows() {
12
19
  const data = _interopRequireDefault(require("nullthrows"));
@@ -17,6 +24,15 @@ function _nullthrows() {
17
24
  }
18
25
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
19
26
  const ALL_EDGE_TYPES = exports.ALL_EDGE_TYPES = -1;
27
+
28
+ /**
29
+ * Internal type used for queue iterative DFS implementation.
30
+ */
31
+
32
+ /**
33
+ * Options for DFS traversal.
34
+ */
35
+
20
36
  class Graph {
21
37
  constructor(opts) {
22
38
  this.nodes = (opts === null || opts === void 0 ? void 0 : opts.nodes) || [];
@@ -179,7 +195,11 @@ class Graph {
179
195
  if (type === ALL_EDGE_TYPES && enter && (typeof visit === 'function' || !visit.exit)) {
180
196
  return this.dfsFast(enter, startNodeId);
181
197
  } else {
182
- return this.dfs({
198
+ return (0, _featureFlags().getFeatureFlag)('dfsFasterRefactor') ? this.dfsNew({
199
+ visit,
200
+ startNodeId,
201
+ getChildren: nodeId => this.getNodeIdsConnectedFrom(nodeId, type)
202
+ }) : this.dfs({
183
203
  visit,
184
204
  startNodeId,
185
205
  getChildren: nodeId => this.getNodeIdsConnectedFrom(nodeId, type)
@@ -301,6 +321,114 @@ class Graph {
301
321
  }
302
322
  this._visited = visited;
303
323
  }
324
+
325
+ /**
326
+ * Iterative implementation of DFS that supports all use-cases.
327
+ *
328
+ * This replaces `dfs` and will replace `dfsFast`.
329
+ */
330
+ dfsNew({
331
+ visit,
332
+ startNodeId,
333
+ getChildren
334
+ }) {
335
+ let traversalStartNode = (0, _nullthrows().default)(startNodeId ?? this.rootNodeId, 'A start node is required to traverse');
336
+ this._assertHasNodeId(traversalStartNode);
337
+ let visited;
338
+ if (!this._visited || this._visited.capacity < this.nodes.length) {
339
+ this._visited = new _BitSet.BitSet(this.nodes.length);
340
+ visited = this._visited;
341
+ } else {
342
+ visited = this._visited;
343
+ visited.clear();
344
+ }
345
+ // Take shared instance to avoid re-entrancy issues.
346
+ this._visited = null;
347
+ let stopped = false;
348
+ let skipped = false;
349
+ let actions = {
350
+ skipChildren() {
351
+ skipped = true;
352
+ },
353
+ stop() {
354
+ stopped = true;
355
+ }
356
+ };
357
+ const queue = [{
358
+ nodeId: traversalStartNode,
359
+ context: null
360
+ }];
361
+ const enter = typeof visit === 'function' ? visit : visit.enter;
362
+ while (queue.length !== 0) {
363
+ const command = queue.pop();
364
+ if (command.exit != null) {
365
+ let {
366
+ nodeId,
367
+ context,
368
+ exit
369
+ } = command;
370
+ let newContext = exit(nodeId, command.context, actions);
371
+ if (typeof newContext !== 'undefined') {
372
+ // $FlowFixMe[reassign-const]
373
+ context = newContext;
374
+ }
375
+ if (skipped) {
376
+ continue;
377
+ }
378
+ if (stopped) {
379
+ this._visited = visited;
380
+ return context;
381
+ }
382
+ } else {
383
+ let {
384
+ nodeId,
385
+ context
386
+ } = command;
387
+ if (!this.hasNode(nodeId) || visited.has(nodeId)) continue;
388
+ visited.add(nodeId);
389
+ skipped = false;
390
+ if (enter) {
391
+ let newContext = enter(nodeId, context, actions);
392
+ if (typeof newContext !== 'undefined') {
393
+ // $FlowFixMe[reassign-const]
394
+ context = newContext;
395
+ }
396
+ }
397
+ if (skipped) {
398
+ continue;
399
+ }
400
+ if (stopped) {
401
+ this._visited = visited;
402
+ return context;
403
+ }
404
+ if (typeof visit !== 'function' && visit.exit) {
405
+ queue.push({
406
+ nodeId,
407
+ exit: visit.exit,
408
+ context
409
+ });
410
+ }
411
+
412
+ // TODO turn into generator function
413
+ const children = getChildren(nodeId);
414
+ for (let i = children.length - 1; i > -1; i -= 1) {
415
+ const child = children[i];
416
+ if (visited.has(child)) {
417
+ continue;
418
+ }
419
+ queue.push({
420
+ nodeId: child,
421
+ context
422
+ });
423
+ }
424
+ }
425
+ }
426
+ this._visited = visited;
427
+ }
428
+
429
+ /**
430
+ * @deprecated Will be replaced by `dfsNew`
431
+ */
304
432
  dfs({
305
433
  visit,
306
434
  startNodeId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parcel/graph",
3
- "version": "3.2.1-dev.3195+7afdafedd",
3
+ "version": "3.2.1-dev.3198+507fb5b10",
4
4
  "description": "Blazing fast, zero configuration web application bundler",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -20,7 +20,8 @@
20
20
  "node": ">= 16.0.0"
21
21
  },
22
22
  "dependencies": {
23
+ "@parcel/feature-flags": "2.12.1-dev.3198+507fb5b10",
23
24
  "nullthrows": "^1.1.1"
24
25
  },
25
- "gitHead": "7afdafedd7f01d82b9868ec004b3966a88006300"
26
+ "gitHead": "507fb5b101d8563900af0da395b47bd6116850bd"
26
27
  }
@@ -30,7 +30,7 @@ export type AdjacencyListOptions<TEdgeType> = {|
30
30
  minGrowFactor?: number,
31
31
  /** The size after which to grow the capacity by the minimum factor. */
32
32
  peakCapacity?: number,
33
- /** The percentage of deleted edges above which the capcity should shink. */
33
+ /** The percentage of deleted edges above which the capacity should shrink. */
34
34
  unloadFactor?: number,
35
35
  /** The amount by which to shrink the capacity. */
36
36
  shrinkFactor?: number,
@@ -328,7 +328,7 @@ export default class AdjacencyList<TEdgeType: number = 1> {
328
328
  *
329
329
  * Note that this method does not increment the node count
330
330
  * (that only happens in `addEdge`), it _may_ preemptively resize
331
- * the nodes array if it is at capacity, under the asumption that
331
+ * the nodes array if it is at capacity, under the assumption that
332
332
  * at least 1 edge to or from this new node will be added.
333
333
  *
334
334
  * Returns the id of the added node.
package/src/Graph.js CHANGED
@@ -3,6 +3,7 @@
3
3
  import {fromNodeId} from './types';
4
4
  import AdjacencyList, {type SerializedAdjacencyList} from './AdjacencyList';
5
5
  import type {Edge, NodeId} from './types';
6
+ import {getFeatureFlag} from '@parcel/feature-flags';
6
7
  import type {
7
8
  TraversalActions,
8
9
  GraphVisitor,
@@ -28,6 +29,51 @@ export type SerializedGraph<TNode, TEdgeType: number = 1> = {|
28
29
  export type AllEdgeTypes = -1;
29
30
  export const ALL_EDGE_TYPES: AllEdgeTypes = -1;
30
31
 
32
+ type DFSCommandVisit<TContext> = {|
33
+ nodeId: NodeId,
34
+ context: TContext | null,
35
+ |};
36
+
37
+ type DFSCommandExit<TContext> = {|
38
+ nodeId: NodeId,
39
+ exit: GraphTraversalCallback<NodeId, TContext>,
40
+ context: TContext | null,
41
+ |};
42
+
43
+ /**
44
+ * Internal type used for queue iterative DFS implementation.
45
+ */
46
+ type DFSCommand<TContext> =
47
+ | DFSCommandVisit<TContext>
48
+ | DFSCommandExit<TContext>;
49
+
50
+ /**
51
+ * Options for DFS traversal.
52
+ */
53
+ export type DFSParams<TContext> = {|
54
+ visit: GraphVisitor<NodeId, TContext>,
55
+ /**
56
+ * Custom function to get next entries to visit.
57
+ *
58
+ * This can be a performance bottleneck as arrays are created on every node visit.
59
+ *
60
+ * @deprecated This will be replaced by a static `traversalType` set of orders in the future
61
+ *
62
+ * Currently, this is only used in 3 ways:
63
+ *
64
+ * - Traversing down the tree (normal DFS)
65
+ * - Traversing up the tree (ancestors)
66
+ * - Filtered version of traversal; which does not need to exist at the DFS level as the visitor
67
+ * can handle filtering
68
+ * - Sorted traversal of BundleGraph entries, which does not have a clear use-case, but may
69
+ * not be safe to remove
70
+ *
71
+ * Only due to the latter we aren't replacing this.
72
+ */
73
+ getChildren: (nodeId: NodeId) => Array<NodeId>,
74
+ startNodeId?: ?NodeId,
75
+ |};
76
+
31
77
  export default class Graph<TNode, TEdgeType: number = 1> {
32
78
  nodes: Array<TNode | null>;
33
79
  adjacencyList: AdjacencyList<TEdgeType>;
@@ -289,11 +335,17 @@ export default class Graph<TNode, TEdgeType: number = 1> {
289
335
  ) {
290
336
  return this.dfsFast(enter, startNodeId);
291
337
  } else {
292
- return this.dfs({
293
- visit,
294
- startNodeId,
295
- getChildren: nodeId => this.getNodeIdsConnectedFrom(nodeId, type),
296
- });
338
+ return getFeatureFlag('dfsFasterRefactor')
339
+ ? this.dfsNew({
340
+ visit,
341
+ startNodeId,
342
+ getChildren: nodeId => this.getNodeIdsConnectedFrom(nodeId, type),
343
+ })
344
+ : this.dfs({
345
+ visit,
346
+ startNodeId,
347
+ getChildren: nodeId => this.getNodeIdsConnectedFrom(nodeId, type),
348
+ });
297
349
  }
298
350
  }
299
351
 
@@ -449,15 +501,122 @@ export default class Graph<TNode, TEdgeType: number = 1> {
449
501
  return;
450
502
  }
451
503
 
504
+ /**
505
+ * Iterative implementation of DFS that supports all use-cases.
506
+ *
507
+ * This replaces `dfs` and will replace `dfsFast`.
508
+ */
509
+ dfsNew<TContext>({
510
+ visit,
511
+ startNodeId,
512
+ getChildren,
513
+ }: DFSParams<TContext>): ?TContext {
514
+ let traversalStartNode = nullthrows(
515
+ startNodeId ?? this.rootNodeId,
516
+ 'A start node is required to traverse',
517
+ );
518
+ this._assertHasNodeId(traversalStartNode);
519
+
520
+ let visited;
521
+ if (!this._visited || this._visited.capacity < this.nodes.length) {
522
+ this._visited = new BitSet(this.nodes.length);
523
+ visited = this._visited;
524
+ } else {
525
+ visited = this._visited;
526
+ visited.clear();
527
+ }
528
+ // Take shared instance to avoid re-entrancy issues.
529
+ this._visited = null;
530
+
531
+ let stopped = false;
532
+ let skipped = false;
533
+ let actions: TraversalActions = {
534
+ skipChildren() {
535
+ skipped = true;
536
+ },
537
+ stop() {
538
+ stopped = true;
539
+ },
540
+ };
541
+
542
+ const queue: DFSCommand<TContext>[] = [
543
+ {nodeId: traversalStartNode, context: null},
544
+ ];
545
+ const enter = typeof visit === 'function' ? visit : visit.enter;
546
+ while (queue.length !== 0) {
547
+ const command = queue.pop();
548
+
549
+ if (command.exit != null) {
550
+ let {nodeId, context, exit} = command;
551
+ let newContext = exit(nodeId, command.context, actions);
552
+ if (typeof newContext !== 'undefined') {
553
+ // $FlowFixMe[reassign-const]
554
+ context = newContext;
555
+ }
556
+
557
+ if (skipped) {
558
+ continue;
559
+ }
560
+
561
+ if (stopped) {
562
+ this._visited = visited;
563
+ return context;
564
+ }
565
+ } else {
566
+ let {nodeId, context} = command;
567
+ if (!this.hasNode(nodeId) || visited.has(nodeId)) continue;
568
+ visited.add(nodeId);
569
+
570
+ skipped = false;
571
+ if (enter) {
572
+ let newContext = enter(nodeId, context, actions);
573
+ if (typeof newContext !== 'undefined') {
574
+ // $FlowFixMe[reassign-const]
575
+ context = newContext;
576
+ }
577
+ }
578
+
579
+ if (skipped) {
580
+ continue;
581
+ }
582
+
583
+ if (stopped) {
584
+ this._visited = visited;
585
+ return context;
586
+ }
587
+
588
+ if (typeof visit !== 'function' && visit.exit) {
589
+ queue.push({
590
+ nodeId,
591
+ exit: visit.exit,
592
+ context,
593
+ });
594
+ }
595
+
596
+ // TODO turn into generator function
597
+ const children = getChildren(nodeId);
598
+ for (let i = children.length - 1; i > -1; i -= 1) {
599
+ const child = children[i];
600
+ if (visited.has(child)) {
601
+ continue;
602
+ }
603
+
604
+ queue.push({nodeId: child, context});
605
+ }
606
+ }
607
+ }
608
+
609
+ this._visited = visited;
610
+ }
611
+
612
+ /**
613
+ * @deprecated Will be replaced by `dfsNew`
614
+ */
452
615
  dfs<TContext>({
453
616
  visit,
454
617
  startNodeId,
455
618
  getChildren,
456
- }: {|
457
- visit: GraphVisitor<NodeId, TContext>,
458
- getChildren(nodeId: NodeId): Array<NodeId>,
459
- startNodeId?: ?NodeId,
460
- |}): ?TContext {
619
+ }: DFSParams<TContext>): ?TContext {
461
620
  let traversalStartNode = nullthrows(
462
621
  startNodeId ?? this.rootNodeId,
463
622
  'A start node is required to traverse',
@@ -2,9 +2,10 @@
2
2
 
3
3
  import assert from 'assert';
4
4
  import sinon from 'sinon';
5
+ import type {TraversalActions} from '@parcel/types-internal';
5
6
 
6
- import Graph from '../src/Graph';
7
- import {toNodeId} from '../src/types';
7
+ import Graph, {type DFSParams} from '../src/Graph';
8
+ import {toNodeId, type NodeId} from '../src/types';
8
9
 
9
10
  describe('Graph', () => {
10
11
  it('constructor should initialize an empty graph', () => {
@@ -340,4 +341,229 @@ describe('Graph', () => {
340
341
  assert.deepEqual(graph.nodes.filter(Boolean), ['root']);
341
342
  assert.deepStrictEqual(Array.from(graph.getAllEdges()), []);
342
343
  });
344
+
345
+ describe('dfs(...)', () => {
346
+ function testSuite(
347
+ name: string,
348
+ dfsImpl: (graph: Graph<string>, DFSParams<mixed>) => mixed | null | void,
349
+ ) {
350
+ it(`${name} throws if the graph is empty`, () => {
351
+ const graph = new Graph();
352
+ const visit = sinon.stub();
353
+ const getChildren = sinon.stub();
354
+ assert.throws(() => {
355
+ dfsImpl(graph, {
356
+ visit,
357
+ startNodeId: 0,
358
+ getChildren,
359
+ });
360
+ }, /Does not have node 0/);
361
+ });
362
+
363
+ it(`${name} visits a single node`, () => {
364
+ const graph = new Graph();
365
+ graph.addNode('root');
366
+ const visit = sinon.stub();
367
+ const getChildren = () => [];
368
+ dfsImpl(graph, {
369
+ visit,
370
+ startNodeId: 0,
371
+ getChildren,
372
+ });
373
+
374
+ assert(visit.calledOnce);
375
+ });
376
+
377
+ it(`${name} visits all connected nodes in DFS order`, () => {
378
+ const graph = new Graph();
379
+ graph.addNode('0');
380
+ graph.addNode('1');
381
+ graph.addNode('2');
382
+ graph.addNode('3');
383
+ graph.addNode('disconnected-1');
384
+ graph.addNode('disconnected-2');
385
+ graph.addEdge(0, 1);
386
+ graph.addEdge(0, 2);
387
+ graph.addEdge(1, 3);
388
+ graph.addEdge(2, 3);
389
+
390
+ const order = [];
391
+ const visit = (node: NodeId) => {
392
+ order.push(node);
393
+ };
394
+ const getChildren = (node: NodeId) =>
395
+ graph.getNodeIdsConnectedFrom(node);
396
+ dfsImpl(graph, {
397
+ visit,
398
+ startNodeId: 0,
399
+ getChildren,
400
+ });
401
+
402
+ assert.deepEqual(order, [0, 1, 3, 2]);
403
+ });
404
+
405
+ describe(`${name} actions tests`, () => {
406
+ it(`${name} skips children if skip is called on a node`, () => {
407
+ const graph = new Graph();
408
+ graph.addNode('0');
409
+ graph.addNode('1');
410
+ graph.addNode('2');
411
+ graph.addNode('3');
412
+ graph.addNode('disconnected-1');
413
+ graph.addNode('disconnected-2');
414
+ graph.addEdge(0, 1);
415
+ graph.addEdge(1, 2);
416
+ graph.addEdge(0, 3);
417
+
418
+ const order = [];
419
+ const visit = (
420
+ node: NodeId,
421
+ context: mixed | null,
422
+ actions: TraversalActions,
423
+ ) => {
424
+ if (node === 1) actions.skipChildren();
425
+ order.push(node);
426
+ };
427
+ const getChildren = (node: NodeId) =>
428
+ graph.getNodeIdsConnectedFrom(node);
429
+ dfsImpl(graph, {
430
+ visit,
431
+ startNodeId: 0,
432
+ getChildren,
433
+ });
434
+
435
+ assert.deepEqual(order, [0, 1, 3]);
436
+ });
437
+
438
+ it(`${name} stops the traversal if stop is called`, () => {
439
+ const graph = new Graph();
440
+ graph.addNode('0');
441
+ graph.addNode('1');
442
+ graph.addNode('2');
443
+ graph.addNode('3');
444
+ graph.addNode('disconnected-1');
445
+ graph.addNode('disconnected-2');
446
+ graph.addEdge(0, 1);
447
+ graph.addEdge(1, 2);
448
+ graph.addEdge(1, 3);
449
+ graph.addEdge(0, 2);
450
+ graph.addEdge(2, 3);
451
+
452
+ const order = [];
453
+ const visit = (
454
+ node: NodeId,
455
+ context: mixed | null,
456
+ actions: TraversalActions,
457
+ ) => {
458
+ order.push(node);
459
+ if (node === 1) {
460
+ actions.stop();
461
+ return 'result';
462
+ }
463
+ return 'other';
464
+ };
465
+ const getChildren = (node: NodeId) =>
466
+ graph.getNodeIdsConnectedFrom(node);
467
+ const result = dfsImpl(graph, {
468
+ visit,
469
+ startNodeId: 0,
470
+ getChildren,
471
+ });
472
+
473
+ assert.deepEqual(order, [0, 1]);
474
+ assert.equal(result, 'result');
475
+ });
476
+ });
477
+
478
+ describe(`${name} context tests`, () => {
479
+ it(`${name} passes the context between visitors`, () => {
480
+ const graph = new Graph();
481
+ graph.addNode('0');
482
+ graph.addNode('1');
483
+ graph.addNode('2');
484
+ graph.addNode('3');
485
+ graph.addNode('disconnected-1');
486
+ graph.addNode('disconnected-2');
487
+ graph.addEdge(0, 1);
488
+ graph.addEdge(1, 2);
489
+ graph.addEdge(1, 3);
490
+ graph.addEdge(0, 2);
491
+ graph.addEdge(2, 3);
492
+
493
+ const contexts = [];
494
+ const visit = (node: NodeId, context: mixed | null) => {
495
+ contexts.push([node, context]);
496
+ return `node-${node}-created-context`;
497
+ };
498
+ const getChildren = (node: NodeId) =>
499
+ graph.getNodeIdsConnectedFrom(node);
500
+ const result = dfsImpl(graph, {
501
+ visit,
502
+ startNodeId: 0,
503
+ getChildren,
504
+ });
505
+
506
+ assert.deepEqual(contexts, [
507
+ [0, undefined],
508
+ [1, 'node-0-created-context'],
509
+ [2, 'node-1-created-context'],
510
+ [3, 'node-2-created-context'],
511
+ ]);
512
+ assert.equal(result, undefined);
513
+ });
514
+ });
515
+
516
+ describe(`${name} exit visitor tests`, () => {
517
+ it(`${name} calls the exit visitor`, () => {
518
+ const graph = new Graph();
519
+ graph.addNode('0');
520
+ graph.addNode('1');
521
+ graph.addNode('2');
522
+ graph.addNode('3');
523
+ graph.addNode('disconnected-1');
524
+ graph.addNode('disconnected-2');
525
+ graph.addEdge(0, 1);
526
+ graph.addEdge(1, 2);
527
+ graph.addEdge(1, 3);
528
+ graph.addEdge(0, 2);
529
+
530
+ const contexts = [];
531
+ const visit = (node: NodeId, context: mixed | null) => {
532
+ contexts.push([node, context]);
533
+ return `node-${node}-created-context`;
534
+ };
535
+ const visitExit = (node: NodeId, context: mixed | null) => {
536
+ contexts.push(['exit', node, context]);
537
+ return `node-exit-${node}-created-context`;
538
+ };
539
+ const getChildren = (node: NodeId) =>
540
+ graph.getNodeIdsConnectedFrom(node);
541
+ const result = dfsImpl(graph, {
542
+ visit: {
543
+ enter: visit,
544
+ exit: visitExit,
545
+ },
546
+ startNodeId: 0,
547
+ getChildren,
548
+ });
549
+
550
+ assert.deepEqual(contexts, [
551
+ [0, undefined],
552
+ [1, 'node-0-created-context'],
553
+ [2, 'node-1-created-context'],
554
+ ['exit', 2, 'node-2-created-context'],
555
+ [3, 'node-1-created-context'],
556
+ ['exit', 3, 'node-3-created-context'],
557
+ ['exit', 1, 'node-1-created-context'],
558
+ ['exit', 0, 'node-0-created-context'],
559
+ ]);
560
+ assert.equal(result, undefined);
561
+ });
562
+ });
563
+ }
564
+
565
+ testSuite('dfs', (graph, params) => graph.dfs(params));
566
+
567
+ testSuite('dfsNew', (graph, params) => graph.dfsNew(params));
568
+ });
343
569
  });