@parcel/graph 3.2.1-dev.3186 → 3.2.1-dev.3187

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -17,6 +17,15 @@ function _nullthrows() {
17
17
  }
18
18
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
19
19
  const ALL_EDGE_TYPES = exports.ALL_EDGE_TYPES = -1;
20
+
21
+ /**
22
+ * Internal type used for queue iterative DFS implementation.
23
+ */
24
+
25
+ /**
26
+ * Options for DFS traversal.
27
+ */
28
+
20
29
  class Graph {
21
30
  constructor(opts) {
22
31
  this.nodes = (opts === null || opts === void 0 ? void 0 : opts.nodes) || [];
@@ -179,7 +188,7 @@ class Graph {
179
188
  if (type === ALL_EDGE_TYPES && enter && (typeof visit === 'function' || !visit.exit)) {
180
189
  return this.dfsFast(enter, startNodeId);
181
190
  } else {
182
- return this.dfs({
191
+ return this.dfsNew({
183
192
  visit,
184
193
  startNodeId,
185
194
  getChildren: nodeId => this.getNodeIdsConnectedFrom(nodeId, type)
@@ -301,6 +310,108 @@ class Graph {
301
310
  }
302
311
  this._visited = visited;
303
312
  }
313
+
314
+ /**
315
+ * Iterative implementation of DFS that supports all use-cases.
316
+ */
317
+ dfsNew({
318
+ visit,
319
+ startNodeId,
320
+ getChildren
321
+ }) {
322
+ let traversalStartNode = (0, _nullthrows().default)(startNodeId !== null && startNodeId !== void 0 ? startNodeId : this.rootNodeId, 'A start node is required to traverse');
323
+ this._assertHasNodeId(traversalStartNode);
324
+ let visited;
325
+ if (!this._visited || this._visited.capacity < this.nodes.length) {
326
+ this._visited = new _BitSet.BitSet(this.nodes.length);
327
+ visited = this._visited;
328
+ } else {
329
+ visited = this._visited;
330
+ visited.clear();
331
+ }
332
+ // Take shared instance to avoid re-entrancy issues.
333
+ this._visited = null;
334
+ let stopped = false;
335
+ let skipped = false;
336
+ let actions = {
337
+ skipChildren() {
338
+ skipped = true;
339
+ },
340
+ stop() {
341
+ stopped = true;
342
+ }
343
+ };
344
+ const queue = [{
345
+ nodeId: traversalStartNode,
346
+ context: null
347
+ }];
348
+ while (queue.length !== 0) {
349
+ const command = queue.pop();
350
+ if (command.exit != null) {
351
+ let {
352
+ nodeId,
353
+ context,
354
+ exit
355
+ } = command;
356
+ let newContext = exit(nodeId, command.context, actions);
357
+ if (typeof newContext !== 'undefined') {
358
+ // $FlowFixMe[reassign-const]
359
+ context = newContext;
360
+ }
361
+ if (skipped) {
362
+ continue;
363
+ }
364
+ if (stopped) {
365
+ this._visited = visited;
366
+ return context;
367
+ }
368
+ } else {
369
+ let {
370
+ nodeId,
371
+ context
372
+ } = command;
373
+ if (!this.hasNode(nodeId) || visited.has(nodeId)) continue;
374
+ visited.add(nodeId);
375
+ skipped = false;
376
+ let enter = typeof visit === 'function' ? visit : visit.enter;
377
+ if (enter) {
378
+ let newContext = enter(nodeId, context, actions);
379
+ if (typeof newContext !== 'undefined') {
380
+ // $FlowFixMe[reassign-const]
381
+ context = newContext;
382
+ }
383
+ }
384
+ if (skipped) {
385
+ continue;
386
+ }
387
+ if (stopped) {
388
+ this._visited = visited;
389
+ return context;
390
+ }
391
+ if (typeof visit !== 'function' && visit.exit) {
392
+ queue.push({
393
+ nodeId,
394
+ exit: visit.exit,
395
+ context
396
+ });
397
+ }
398
+
399
+ // TODO turn into generator function
400
+ const children = getChildren(nodeId);
401
+ for (let i = children.length - 1; i > -1; i -= 1) {
402
+ const child = children[i];
403
+ if (visited.has(child)) {
404
+ continue;
405
+ }
406
+ queue.push({
407
+ nodeId: child,
408
+ context
409
+ });
410
+ }
411
+ }
412
+ }
413
+ this._visited = visited;
414
+ }
304
415
  dfs({
305
416
  visit,
306
417
  startNodeId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parcel/graph",
3
- "version": "3.2.1-dev.3186+2449643f7",
3
+ "version": "3.2.1-dev.3187+f88048a4b",
4
4
  "description": "Blazing fast, zero configuration web application bundler",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -22,5 +22,5 @@
22
22
  "dependencies": {
23
23
  "nullthrows": "^1.1.1"
24
24
  },
25
- "gitHead": "2449643f7b1774d2e9f8c4bd13d6fa8ac66f23e6"
25
+ "gitHead": "f88048a4b5e7b0d65ea484365b568ad423a8bf45"
26
26
  }
@@ -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
@@ -28,6 +28,51 @@ export type SerializedGraph<TNode, TEdgeType: number = 1> = {|
28
28
  export type AllEdgeTypes = -1;
29
29
  export const ALL_EDGE_TYPES: AllEdgeTypes = -1;
30
30
 
31
+ type DFSCommandVisit<TContext> = {|
32
+ nodeId: NodeId,
33
+ context: TContext | null,
34
+ |};
35
+
36
+ type DFSCommandExit<TContext> = {|
37
+ nodeId: NodeId,
38
+ exit: GraphTraversalCallback<NodeId, TContext>,
39
+ context: TContext | null,
40
+ |};
41
+
42
+ /**
43
+ * Internal type used for queue iterative DFS implementation.
44
+ */
45
+ type DFSCommand<TContext> =
46
+ | DFSCommandVisit<TContext>
47
+ | DFSCommandExit<TContext>;
48
+
49
+ /**
50
+ * Options for DFS traversal.
51
+ */
52
+ type DFSParams<TContext> = {|
53
+ visit: GraphVisitor<NodeId, TContext>,
54
+ /**
55
+ * Custom function to get next entries to visit.
56
+ *
57
+ * This can be a performance bottle-neck as arrays are created on every node visit.
58
+ *
59
+ * @deprecated This will be replaced by a static `traversalType` set of orders in the future
60
+ *
61
+ * Currently this is only used in 3 ways:
62
+ *
63
+ * - Traversing down the tree (normal DFS)
64
+ * - Traversing up the tree (ancestors)
65
+ * - Filtered version of traversal; which does not need to exist at the DFS level as the visitor
66
+ * can handle filtering
67
+ * - Sorted traversal of BundleGraph entries, which does not have a clear use-case, but may
68
+ * not be safe to remove
69
+ *
70
+ * Only due to the latter we aren't replacing this.
71
+ */
72
+ getChildren: (nodeId: NodeId) => Array<NodeId>,
73
+ startNodeId?: ?NodeId,
74
+ |};
75
+
31
76
  export default class Graph<TNode, TEdgeType: number = 1> {
32
77
  nodes: Array<TNode | null>;
33
78
  adjacencyList: AdjacencyList<TEdgeType>;
@@ -289,7 +334,7 @@ export default class Graph<TNode, TEdgeType: number = 1> {
289
334
  ) {
290
335
  return this.dfsFast(enter, startNodeId);
291
336
  } else {
292
- return this.dfs({
337
+ return this.dfsNew({
293
338
  visit,
294
339
  startNodeId,
295
340
  getChildren: nodeId => this.getNodeIdsConnectedFrom(nodeId, type),
@@ -449,15 +494,117 @@ export default class Graph<TNode, TEdgeType: number = 1> {
449
494
  return;
450
495
  }
451
496
 
497
+ /**
498
+ * Iterative implementation of DFS that supports all use-cases.
499
+ */
500
+ dfsNew<TContext>({
501
+ visit,
502
+ startNodeId,
503
+ getChildren,
504
+ }: DFSParams<TContext>): ?TContext {
505
+ let traversalStartNode = nullthrows(
506
+ startNodeId ?? this.rootNodeId,
507
+ 'A start node is required to traverse',
508
+ );
509
+ this._assertHasNodeId(traversalStartNode);
510
+
511
+ let visited;
512
+ if (!this._visited || this._visited.capacity < this.nodes.length) {
513
+ this._visited = new BitSet(this.nodes.length);
514
+ visited = this._visited;
515
+ } else {
516
+ visited = this._visited;
517
+ visited.clear();
518
+ }
519
+ // Take shared instance to avoid re-entrancy issues.
520
+ this._visited = null;
521
+
522
+ let stopped = false;
523
+ let skipped = false;
524
+ let actions: TraversalActions = {
525
+ skipChildren() {
526
+ skipped = true;
527
+ },
528
+ stop() {
529
+ stopped = true;
530
+ },
531
+ };
532
+
533
+ const queue: DFSCommand<TContext>[] = [
534
+ {nodeId: traversalStartNode, context: null},
535
+ ];
536
+ while (queue.length !== 0) {
537
+ const command = queue.pop();
538
+
539
+ if (command.exit != null) {
540
+ let {nodeId, context, exit} = command;
541
+ let newContext = exit(nodeId, command.context, actions);
542
+ if (typeof newContext !== 'undefined') {
543
+ // $FlowFixMe[reassign-const]
544
+ context = newContext;
545
+ }
546
+
547
+ if (skipped) {
548
+ continue;
549
+ }
550
+
551
+ if (stopped) {
552
+ this._visited = visited;
553
+ return context;
554
+ }
555
+ } else {
556
+ let {nodeId, context} = command;
557
+ if (!this.hasNode(nodeId) || visited.has(nodeId)) continue;
558
+ visited.add(nodeId);
559
+
560
+ skipped = false;
561
+ let enter = typeof visit === 'function' ? visit : visit.enter;
562
+ if (enter) {
563
+ let newContext = enter(nodeId, context, actions);
564
+ if (typeof newContext !== 'undefined') {
565
+ // $FlowFixMe[reassign-const]
566
+ context = newContext;
567
+ }
568
+ }
569
+
570
+ if (skipped) {
571
+ continue;
572
+ }
573
+
574
+ if (stopped) {
575
+ this._visited = visited;
576
+ return context;
577
+ }
578
+
579
+ if (typeof visit !== 'function' && visit.exit) {
580
+ queue.push({
581
+ nodeId,
582
+ exit: visit.exit,
583
+ context,
584
+ });
585
+ }
586
+
587
+ // TODO turn into generator function
588
+ const children = getChildren(nodeId);
589
+ for (let i = children.length - 1; i > -1; i -= 1) {
590
+ const child = children[i];
591
+ if (visited.has(child)) {
592
+ continue;
593
+ }
594
+
595
+ queue.push({nodeId: child, context});
596
+ }
597
+ }
598
+ }
599
+
600
+ this._visited = visited;
601
+ }
602
+
452
603
  dfs<TContext>({
453
604
  visit,
454
605
  startNodeId,
455
606
  getChildren,
456
- }: {|
457
- visit: GraphVisitor<NodeId, TContext>,
458
- getChildren(nodeId: NodeId): Array<NodeId>,
459
- startNodeId?: ?NodeId,
460
- |}): ?TContext {
607
+ }: DFSParams<TContext>): ?TContext {
461
608
  let traversalStartNode = nullthrows(
462
609
  startNodeId ?? this.rootNodeId,
463
610
  '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
  });