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

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
@@ -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
  });