@instana/shared-metrics 1.129.0 → 1.131.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instana/shared-metrics",
3
- "version": "1.129.0",
3
+ "version": "1.131.1",
4
4
  "description": "Internal metrics plug-in package for Node.js monitoring with Instana",
5
5
  "author": {
6
6
  "name": "Bastian Krol",
@@ -26,7 +26,7 @@
26
26
  "scripts": {
27
27
  "audit": "bin/prepare-audit.sh && npm audit --production; AUDIT_RESULT=$?; git checkout package-lock.json; exit $AUDIT_RESULT",
28
28
  "test": "npm run test:mocha",
29
- "test:mocha": "mocha --sort --reporter mocha-multi --reporter-options spec=-,xunit=../../test-results/shared-metris/results.xml $(find test -iname '*test.js')",
29
+ "test:mocha": "mocha --sort --reporter mocha-multi --reporter-options spec=-,xunit=../../test-results/shared-metris/results.xml $(find test -iname '*test.js' -not -path '*node_modules*')",
30
30
  "test:debug": "WITH_STDOUT=true npm run test:mocha",
31
31
  "lint": "eslint src test",
32
32
  "verify": "npm run lint && npm test",
@@ -48,6 +48,10 @@
48
48
  {
49
49
  "name": "Emelia Smith",
50
50
  "email": "ThisIsMissEm@users.noreply.github.com"
51
+ },
52
+ {
53
+ "name": "Richard Gebhardt",
54
+ "email": "gebhardt@us.ibm.com"
51
55
  }
52
56
  ],
53
57
  "bugs": {
@@ -55,11 +59,11 @@
55
59
  },
56
60
  "license": "MIT",
57
61
  "dependencies": {
58
- "@instana/core": "1.129.0",
62
+ "@instana/core": "1.131.1",
59
63
  "detect-libc": "^1.0.3",
60
64
  "event-loop-lag": "^1.4.0",
61
65
  "recursive-copy": "^2.0.13",
62
- "tar": "^5.0.5"
66
+ "tar": "^5.0.9"
63
67
  },
64
68
  "devDependencies": {
65
69
  "eslint": "^7.30.0",
@@ -74,5 +78,5 @@
74
78
  "gcstats.js": "1.0.0",
75
79
  "node-gyp": "^7.1.2"
76
80
  },
77
- "gitHead": "6a7eab071c83970e6e57914bf61f1a1fe1bb3e59"
81
+ "gitHead": "d08894ed069a3c0f6f22e36f21ab522459fae3b8"
78
82
  }
@@ -12,6 +12,9 @@ const { applicationUnderMonitoring } = require('@instana/core').util;
12
12
 
13
13
  let logger = require('@instana/core').logger.getLogger('metrics');
14
14
 
15
+ const CountDownLatch = require('./util/CountDownLatch');
16
+ const { DependencyDistanceCalculator, MAX_DEPTH } = require('./util/DependencyDistanceCalculator');
17
+
15
18
  /**
16
19
  * @param {import('@instana/core/src/logger').GenericLogger} _logger
17
20
  */
@@ -19,20 +22,31 @@ exports.setLogger = function setLogger(_logger) {
19
22
  logger = _logger;
20
23
  };
21
24
 
25
+ /** @type {number} */
26
+ exports.MAX_DEPENDENCIES = 750;
27
+
28
+ /** @type {string} */
22
29
  exports.payloadPrefix = 'dependencies';
30
+
31
+ /** @type {Object.<string, *>} */
32
+ const preliminaryPayload = {};
33
+
23
34
  /** @type {Object.<string, *>} */
24
35
  exports.currentPayload = {};
25
36
 
26
- const MAX_ATTEMPTS = 20;
37
+ exports.MAX_ATTEMPTS = 20;
38
+
27
39
  const DELAY = 1000;
28
40
  let attempts = 0;
29
41
 
30
42
  exports.activate = function activate() {
31
43
  attempts++;
44
+
45
+ const started = Date.now();
32
46
  applicationUnderMonitoring.getMainPackageJsonPathStartingAtMainModule((err, packageJsonPath) => {
33
47
  if (err) {
34
48
  return logger.warn('Failed to determine main package.json. Reason: %s %s ', err.message, err.stack);
35
- } else if (!packageJsonPath && attempts < MAX_ATTEMPTS) {
49
+ } else if (!packageJsonPath && attempts < exports.MAX_ATTEMPTS) {
36
50
  logger.debug('Main package.json could not be found. Will try again later.');
37
51
  setTimeout(exports.activate, DELAY).unref();
38
52
  return;
@@ -48,7 +62,8 @@ exports.activate = function activate() {
48
62
  'Neither the package.json file nor the node_modules folder could be found. Stopping dependency analysis.'
49
63
  );
50
64
  }
51
- addDependenciesFromDir(path.join(nodeModulesFolder));
65
+
66
+ addAllDependencies(path.join(nodeModulesFolder), started, null);
52
67
  });
53
68
  return;
54
69
  }
@@ -59,76 +74,198 @@ exports.activate = function activate() {
59
74
  } else {
60
75
  dependencyDir = path.join(path.dirname(packageJsonPath), 'node_modules');
61
76
  }
62
- addDependenciesFromDir(dependencyDir);
77
+ addAllDependencies(dependencyDir, started, packageJsonPath);
63
78
  });
64
79
  };
65
80
 
66
81
  /**
82
+ * Finds all installed modules in the given dependencyDir (say, /path/to/app/node_modules) and saves the dependency with
83
+ * the associated version into preliminaryPayload.
84
+ *
67
85
  * @param {string} dependencyDir
86
+ * @param {number} started
87
+ * @param {string} packageJsonPath
68
88
  */
69
- function addDependenciesFromDir(dependencyDir) {
70
- fs.readdir(dependencyDir, (readDirErr, dependencies) => {
71
- if (readDirErr) {
72
- return logger.warn('Cannot analyse dependencies due to %s', readDirErr.message);
89
+ function addAllDependencies(dependencyDir, started, packageJsonPath) {
90
+ addDependenciesFromDir(dependencyDir, () => {
91
+ if (Object.keys(preliminaryPayload).length <= exports.MAX_DEPENDENCIES) {
92
+ exports.currentPayload = preliminaryPayload;
93
+ logger.debug(`Collection of dependencies took ${Date.now() - started} ms.`);
94
+ return;
73
95
  }
74
96
 
75
- dependencies
76
- .filter(
77
- (
78
- dependency // exclude the .bin directory
79
- ) => dependency !== '.bin'
80
- )
81
- .forEach(dependency => {
82
- if (dependency.indexOf('@') === 0) {
83
- addDependenciesFromDir(path.join(dependencyDir, dependency));
84
- } else {
85
- const fullDirPath = path.join(dependencyDir, dependency);
86
- // Only check directories. For example, yarn adds a .yarn-integrity file to /node_modules/ which we need to
87
- // exclude, otherwise we get a confusing "Failed to identify version of .yarn-integrity dependency due to:
88
- // ENOTDIR: not a directory, open '.../node_modules/.yarn-integrity/package.json'." in the logs.
89
- fs.stat(fullDirPath, (statErr, stats) => {
90
- if (statErr) {
91
- return logger.warn('Cannot analyse dependency %s due to %s', fullDirPath, statErr.message);
92
- }
93
- if (stats.isDirectory()) {
94
- const fullPackageJsonPath = path.join(fullDirPath, 'package.json');
95
- addDependency(dependency, fullPackageJsonPath);
96
- }
97
- });
98
- }
97
+ if (packageJsonPath) {
98
+ new DependencyDistanceCalculator().calculateDistancesFrom(packageJsonPath, distancesFromRoot => {
99
+ logger.debug(`Collection of dependencies took ${Date.now() - started} ms.`);
100
+ limitAndSet(distancesFromRoot);
99
101
  });
102
+ } else {
103
+ logger.debug(`Collection of dependencies took ${Date.now() - started} ms.`);
104
+ limitAndSet();
105
+ }
100
106
  });
101
107
  }
102
108
 
103
109
  /**
104
- * @param {*} dependency
105
- * @param {string} packageJsonPath
110
+ * Finds all installed modules in dependencyDir (say, /path/to/app/node_modules) and saves the dependency with the
111
+ * associated version into preliminaryPayload.
112
+ *
113
+ * @param {string} dependencyDir
114
+ * @param {() => void} callback
115
+ */
116
+ function addDependenciesFromDir(dependencyDir, callback) {
117
+ fs.readdir(dependencyDir, (readDirErr, dependencies) => {
118
+ if (readDirErr || !dependencies) {
119
+ logger.warn('Cannot analyse dependencies due to %s', readDirErr.message);
120
+ callback();
121
+ return;
122
+ }
123
+
124
+ const filteredDependendencies = dependencies.filter(
125
+ (
126
+ dependency // exclude the .bin directory
127
+ ) => dependency !== '.bin'
128
+ );
129
+ if (filteredDependendencies.length === 0) {
130
+ callback();
131
+ return;
132
+ }
133
+
134
+ // This latch fires once all dependencies of the current directory in the node_modules tree have been analysed.
135
+ const countDownLatch = new CountDownLatch(filteredDependendencies.length);
136
+ countDownLatch.once('done', () => {
137
+ callback();
138
+ });
139
+
140
+ filteredDependendencies.forEach(dependency => {
141
+ if (dependency.indexOf('@') === 0) {
142
+ addDependenciesFromDir(path.join(dependencyDir, dependency), () => {
143
+ countDownLatch.countDown();
144
+ });
145
+ } else {
146
+ const fullDirPath = path.join(dependencyDir, dependency);
147
+ // Only check directories. For example, yarn adds a .yarn-integrity file to /node_modules/ which we need to
148
+ // exclude, otherwise we get a confusing "Failed to identify version of .yarn-integrity dependency due to:
149
+ // ENOTDIR: not a directory, open '.../node_modules/.yarn-integrity/package.json'." in the logs.
150
+ fs.stat(fullDirPath, (statErr, stats) => {
151
+ if (statErr) {
152
+ countDownLatch.countDown();
153
+ logger.warn('Cannot analyse dependency %s due to %s', fullDirPath, statErr.message);
154
+ return;
155
+ }
156
+ if (!stats.isDirectory()) {
157
+ countDownLatch.countDown();
158
+ return;
159
+ }
160
+
161
+ addDependency(dependency, fullDirPath, countDownLatch);
162
+ });
163
+ }
164
+ });
165
+ });
166
+ }
167
+
168
+ /**
169
+ * Parses the package.json file in the given directory and then adds the given dependency (with its version) to
170
+ * preliminaryPayload.
171
+ *
172
+ * @param {string} dependency
173
+ * @param {string} dependencyDirPath
174
+ * @param {import('./util/CountDownLatch')} countDownLatch
106
175
  */
107
- function addDependency(dependency, packageJsonPath) {
176
+ function addDependency(dependency, dependencyDirPath, countDownLatch) {
177
+ const packageJsonPath = path.join(dependencyDirPath, 'package.json');
108
178
  fs.readFile(packageJsonPath, { encoding: 'utf8' }, (err, contents) => {
109
179
  if (err && err.code === 'ENOENT') {
110
180
  // This directory does not contain a package json. This happens for example for node_modules/.cache etc.
111
181
  // We can simply ignore this.
112
- return logger.debug(`No package.json at ${packageJsonPath}, ignoring this directory.`);
182
+ countDownLatch.countDown();
183
+ logger.debug(`No package.json at ${packageJsonPath}, ignoring this directory.`);
184
+ return;
113
185
  } else if (err) {
114
- return logger.info(
186
+ countDownLatch.countDown();
187
+ logger.info(
115
188
  'Failed to identify version of %s dependency due to: %s. This means that you will not be ' +
116
189
  'able to see details about this dependency within Instana.',
117
190
  dependency,
118
191
  err.message
119
192
  );
193
+ return;
120
194
  }
121
195
 
122
196
  try {
123
- const pckg = JSON.parse(contents);
124
- exports.currentPayload[pckg.name] = pckg.version;
125
- } catch (subErr) {
197
+ const parsedPackageJson = JSON.parse(contents);
198
+ if (!preliminaryPayload[parsedPackageJson.name]) {
199
+ preliminaryPayload[parsedPackageJson.name] = parsedPackageJson.version;
200
+ }
201
+ } catch (parseErr) {
126
202
  return logger.info(
127
203
  'Failed to identify version of %s dependency due to: %s. This means that you will not be ' +
128
204
  'able to see details about this dependency within Instana.',
129
205
  dependency,
130
- subErr.message
206
+ parseErr.message
131
207
  );
132
208
  }
209
+
210
+ const potentialNestedNodeModulesFolder = path.join(dependencyDirPath, 'node_modules');
211
+ fs.stat(potentialNestedNodeModulesFolder, (statErr, stats) => {
212
+ if (statErr || !stats.isDirectory()) {
213
+ countDownLatch.countDown();
214
+ return;
215
+ }
216
+ addDependenciesFromDir(potentialNestedNodeModulesFolder, () => {
217
+ countDownLatch.countDown();
218
+ });
219
+ });
133
220
  });
134
221
  }
222
+
223
+ /**
224
+ * Limits the collected dependencies to exports.MAX_DEPENDENCIES entries and commits them to exports.currentPayload.
225
+ *
226
+ * @param {Object<string, any>} distances
227
+ */
228
+ function limitAndSet(distances = {}) {
229
+ const keys = Object.keys(preliminaryPayload);
230
+ keys.sort(sortByDistance.bind(null, distances));
231
+
232
+ // After sorting, the most distant (and therefore, most uninteresting) packages are a the start of the array. For
233
+ // packages with the same distance, we sort in a reverse lexicographic order. That means, that if no distances are
234
+ // available at all, packages will be in reverse lexicographical order.
235
+ //
236
+ // At any rate, we start deleting collected depenencies from the payload at index 0, that is, we either remove the
237
+ // most distant ones or the ones that are at the end of the lexicographic order.
238
+ for (let i = 0; i < keys.length - exports.MAX_DEPENDENCIES; i++) {
239
+ delete preliminaryPayload[keys[i]];
240
+ }
241
+ exports.currentPayload = preliminaryPayload;
242
+ }
243
+
244
+ /**
245
+ * Compares the given dependencies by their distance.
246
+ *
247
+ * @param {Object<string, any>} distances
248
+ * @param {string} dependency1
249
+ * @param {string} dependency2
250
+ */
251
+ function sortByDistance(distances, dependency1, dependency2) {
252
+ // To make troubleshooting easier, we always want to include the Instana dependencies, therefore they will be sorted
253
+ // to the end of the array.
254
+ const isInstana1 = dependency1.indexOf('instana') >= 0;
255
+ const isInstana2 = dependency2.indexOf('instana') >= 0;
256
+ if (isInstana1 && isInstana2) {
257
+ return dependency2.localeCompare(dependency1);
258
+ } else if (isInstana1) {
259
+ return 1;
260
+ } else if (isInstana2) {
261
+ return -1;
262
+ }
263
+
264
+ const d1 = distances[dependency1] || MAX_DEPTH + 1;
265
+ const d2 = distances[dependency2] || MAX_DEPTH + 1;
266
+ if (d1 === d2) {
267
+ // for the same distance, sort lexicographically
268
+ return dependency2.localeCompare(dependency1);
269
+ }
270
+ return d2 - d1;
271
+ }
@@ -56,6 +56,7 @@ exports.activate = function activate() {
56
56
  * @param {string} packageJsonPath
57
57
  */
58
58
  function addDirectDependenciesFromMainPackageJson(packageJsonPath) {
59
+ const started = Date.now();
59
60
  fs.readFile(packageJsonPath, { encoding: 'utf8' }, (err, contents) => {
60
61
  if (err) {
61
62
  return logger.debug('Failed to analyze direct dependencies dependency due to: %s.', err.message);
@@ -67,7 +68,9 @@ function addDirectDependenciesFromMainPackageJson(packageJsonPath) {
67
68
  exports.currentPayload.peerDependencies = pckg.peerDependencies || {};
68
69
  exports.currentPayload.optionalDependencies = pckg.optionalDependencies || {};
69
70
  exports.currentPayload[pckg.name] = pckg.version;
71
+ logger.debug(`Collection of direct dependencies took ${Date.now() - started} ms.`);
70
72
  } catch (subErr) {
73
+ logger.debug(`Collection of direct dependencies took ${Date.now() - started} ms.`);
71
74
  return logger.debug('Failed to parse package.json %s dependency due to: %s', packageJsonPath, subErr.message);
72
75
  }
73
76
  });
package/src/index.js CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  'use strict';
7
7
 
8
+ /** @type {Array.<import('@instana/core/src/metrics').InstanaMetricsModule>} */
8
9
  const allMetrics = [
9
10
  require('./activeHandles'),
10
11
  require('./activeRequests'),
@@ -36,9 +37,9 @@ const setLogger = function (logger) {
36
37
 
37
38
  /**
38
39
  * @typedef {Object} InstanaSharedMetrics
39
- * @property {Array.<*>} allMetrics
40
- * @property {*} util
41
- * @property {*} setLogger
40
+ * @property {Array.<import('@instana/core/src/metrics').InstanaMetricsModule>} allMetrics
41
+ * @property {import('./util')} util
42
+ * @property {(logger: import('@instana/core/src/logger').GenericLogger) => void} setLogger
42
43
  */
43
44
 
44
45
  /** @type {InstanaSharedMetrics} */
@@ -0,0 +1,34 @@
1
+ /*
2
+ * (c) Copyright IBM Corp. 2021
3
+ * (c) Copyright Instana Inc. and contributors 2021
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const EventEmitter = require('events');
9
+ const assert = require('assert');
10
+
11
+ class CountDownLatch extends EventEmitter {
12
+ constructor(counter = 1) {
13
+ super();
14
+ assert(counter >= 0);
15
+ this.counter = counter;
16
+ this.doneEmitted = false;
17
+ }
18
+
19
+ countUp(increment = 1) {
20
+ this.counter += increment;
21
+ }
22
+
23
+ countDown(decrement = 1) {
24
+ if (this.counter >= decrement) {
25
+ this.counter -= decrement;
26
+ if (this.counter === 0 && !this.doneEmitted) {
27
+ this.emit('done');
28
+ this.doneEmitted = true;
29
+ }
30
+ }
31
+ }
32
+ }
33
+
34
+ module.exports = CountDownLatch;
@@ -0,0 +1,250 @@
1
+ /*
2
+ * (c) Copyright IBM Corp. 2021
3
+ * (c) Copyright Instana Inc. and contributors 2021
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const assert = require('assert');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const CountDownLatch = require('./CountDownLatch');
13
+
14
+ let logger = require('@instana/core').logger.getLogger('metrics');
15
+
16
+ /**
17
+ * @param {import('@instana/core/src/logger').GenericLogger} _logger
18
+ */
19
+ exports.setLogger = function setLogger(_logger) {
20
+ logger = _logger;
21
+ };
22
+
23
+ class DependencyDistanceCalculator {
24
+ /**
25
+ * Calculates the distance for all dependencies, starting at the given package.json file. Direct dependencies listed
26
+ * in the passed package.json have distance 1. Dependencies of those dependencies have distance 2, and so on. This is
27
+ * calculated by parsing package.json files recursively, traversing the tree of dependencies.
28
+ *
29
+ * @param {string} packageJsonPath the path to the package.json file to examine initially
30
+ * @param {(distances: Object<string, any>) => void} callback
31
+ */
32
+ calculateDistancesFrom(packageJsonPath, callback) {
33
+ this.started = Date.now();
34
+ assert.strictEqual(typeof packageJsonPath, 'string');
35
+ assert.strictEqual(typeof callback, 'function');
36
+ /** @type {Object.<string, any>} */
37
+ this.distancesFromRoot = {};
38
+
39
+ this.globalCountDownLatchAllPackages = new CountDownLatch(0);
40
+ this.globalCountDownLatchAllPackages.once('done', () => {
41
+ logger.debug(`Calculation of dependency distances took ${Date.now() - this.started} ms.`);
42
+ callback(this.distancesFromRoot);
43
+ });
44
+ this._calculateDistances(packageJsonPath, 1);
45
+ }
46
+
47
+ /**
48
+ * Calculates the distances for the dependencies in the given package.json file. Direct dependencies of the
49
+ * application have distance 1. Dependencies of those dependencies have distance 2, and so on. This is calculated by
50
+ * parsing package.json files recursively, traversing the tree of dependencies.
51
+ *
52
+ * @param {string} packageJsonPath The path to the package.json file to examine
53
+ * @param {number} distance The distance from the application along the tree of dependencies
54
+ */
55
+ _calculateDistances(packageJsonPath, distance) {
56
+ if (distance > module.exports.MAX_DEPTH) {
57
+ // Do not descend deeper than maxDepth nesting levels.
58
+ return;
59
+ }
60
+
61
+ // For each package.json that we find in the dependency tree, we initially increase the global count down latch
62
+ // by 3, that is, one for each type of dependencies (normal, optional, peer). Once we have in turn queued all
63
+ // dependencies found in this package.json for a particular dependency type, we decrement the global count down
64
+ // latch by one. When this has happend for all three types of dependencies, the net change for the global latch will
65
+ // be zero, but sub dependencies will already have been incremented the global count down latch.
66
+ this.globalCountDownLatchAllPackages.countUp(3);
67
+
68
+ // Read the associated package.json and parse it.
69
+ fs.readFile(packageJsonPath, { encoding: 'utf8' }, (err, contents) => {
70
+ if (err) {
71
+ logger.debug(
72
+ 'Failed to calculate transitive distances for some dependencies, could not read package.json file at %s: %s.',
73
+ packageJsonPath,
74
+ err.message
75
+ );
76
+
77
+ // If we cannot parse the package.json or if it does not exist, we need to decrement by 3 immediately because we
78
+ // increment the latch by 3 for each node (see above).
79
+ this.globalCountDownLatchAllPackages.countDown(3);
80
+ return;
81
+ }
82
+
83
+ let parsedPackageJson;
84
+ try {
85
+ parsedPackageJson = JSON.parse(contents);
86
+ } catch (parseErr) {
87
+ logger.debug(
88
+ 'Failed to calculate transitive distances for some dependencies, could not parse package.json file at %s: %s',
89
+ packageJsonPath,
90
+ parseErr.message
91
+ );
92
+ this.globalCountDownLatchAllPackages.countDown(3);
93
+ return;
94
+ }
95
+
96
+ // Each call to _calculateDistancesForOneType is guaranteed to decrease the global count down latch by exactly
97
+ // one, to offset the increment of 3 that we did for this node in the dependency tree initially.
98
+ this._calculateDistancesForOneType(parsedPackageJson.dependencies, distance);
99
+ this._calculateDistancesForOneType(parsedPackageJson.peerDependencies, distance);
100
+ this._calculateDistancesForOneType(parsedPackageJson.optionalDependencies, distance);
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Iterates over the given set of dependencies to calculate their distances. The set of dependencies will what is
106
+ * defined in a package.json file for one particular type of dependencys (normal, optional, or peer).
107
+ *
108
+ * @param {Array<string>} dependencies The dependencies to analyze
109
+ * @param {number} distance How far the dependencies are from the root package
110
+ */
111
+ _calculateDistancesForOneType(dependencies, distance) {
112
+ if (!dependencies) {
113
+ this.globalCountDownLatchAllPackages.countDown();
114
+ return;
115
+ }
116
+ const keys = Object.keys(dependencies);
117
+ if (keys.length === 0) {
118
+ this.globalCountDownLatchAllPackages.countDown();
119
+ return;
120
+ }
121
+
122
+ // This local latch is initialized with the number of dependencies of the current package.json file for the
123
+ // particular dependency type (normal dependencies, optional ones, peer dependencies) we are analyzing at the
124
+ // moment. Once all sub dependencies for the current package and type have been either
125
+ //
126
+ // a) scheduled for analysis (and have in turn incremented the global count down latch), or
127
+ // b) have been found to not need further analysis,
128
+ //
129
+ // we consider the current node to be done and reduce the global counter/ accordingly.
130
+ const localCountDownLatchForThisNode = new CountDownLatch(keys.length);
131
+ localCountDownLatchForThisNode.once('done', () => {
132
+ this.globalCountDownLatchAllPackages.countDown();
133
+ });
134
+
135
+ for (let i = 0; i < keys.length; i++) {
136
+ const dependency = keys[i];
137
+ if (this.distancesFromRoot[dependency]) {
138
+ // We have seen this package before. Do not analyze this package again.
139
+ this.distancesFromRoot[dependency] = Math.min(distance, this.distancesFromRoot[dependency]);
140
+ localCountDownLatchForThisNode.countDown();
141
+ continue;
142
+ }
143
+
144
+ // We have not seen this package yet, store the distance for it.
145
+ this.distancesFromRoot[dependency] = distance;
146
+
147
+ // Queue this dependency up for further analysis. The local latch is only decremented after we have incremented
148
+ // the/ global latch for this dependency. This makes sure we do not stop the analysis too early.
149
+ this._handleTransitiveDependency(dependency, distance, localCountDownLatchForThisNode);
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Handles a single dependency found in a package.json file.
155
+ *
156
+ * @param {string} dependency the name of the dependency to analyze
157
+ * @param {number} distance how far this dependency is from the root package
158
+ * @param {import('./CountDownLatch')} localCountDownLatchForThisNode
159
+ */
160
+ _handleTransitiveDependency(dependency, distance, localCountDownLatchForThisNode) {
161
+ let mainModulePath;
162
+ try {
163
+ mainModulePath = require.resolve(dependency);
164
+ } catch (requireResolveErr) {
165
+ // ignore
166
+ logger.debug(
167
+ `Ignoring failure to resolve the path to dependency ${dependency} for dependency distance calculation.`
168
+ );
169
+ }
170
+
171
+ if (!mainModulePath) {
172
+ // Could not find the package.json for this dependency so we cannot analyze it further, which means we are done
173
+ // with it.
174
+ localCountDownLatchForThisNode.countDown();
175
+ logger.debug(`No main module path for dependency ${dependency}.`);
176
+ return;
177
+ }
178
+
179
+ findPackageJsonFor(path.dirname(mainModulePath), (err, packageJsonPath) => {
180
+ if (err) {
181
+ localCountDownLatchForThisNode.countDown();
182
+ logger.debug(
183
+ `Ignoring failure to find the package.json file for dependency ${dependency} for dependency distance ` +
184
+ 'calculation.',
185
+ err
186
+ );
187
+ return;
188
+ }
189
+
190
+ // Recurse one level deeper and queue the next package.json path for analysis.
191
+ this._calculateDistances(packageJsonPath, distance + 1);
192
+
193
+ // After the _calculateDistances call we have "handled" the package associated to packageJsonPath, that is, we
194
+ // have (synchronously) incremented the global latch for it. That is, unless we have hit the depth limit, but even
195
+ // then we consider that package to have been handled). Thus, we can now decrement the local latch for it.
196
+ localCountDownLatchForThisNode.countDown();
197
+ });
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Finds the package.json file for a given directory, by starting in the given directory and then travelling the
203
+ * directory tree upwards until a package.json file is found.
204
+ *
205
+ * @param {string} dir
206
+ * @param {(err: Error, packageJsonPath: string) => void} callback
207
+ */
208
+ function findPackageJsonFor(dir, callback) {
209
+ const pathToCheck = path.join(dir, 'package.json');
210
+ fs.stat(pathToCheck, (err, stats) => {
211
+ if (err) {
212
+ if (err.code === 'ENOENT') {
213
+ return searchInParentDir(dir, findPackageJsonFor, callback);
214
+ } else {
215
+ return process.nextTick(callback, err, null);
216
+ }
217
+ }
218
+
219
+ if (stats.isFile()) {
220
+ return process.nextTick(callback, null, pathToCheck);
221
+ } else {
222
+ return searchInParentDir(dir, findPackageJsonFor, callback);
223
+ }
224
+ });
225
+ }
226
+
227
+ /**
228
+ * Goes to the parent directory of the given dir and executes the function onParentDir on it.
229
+ *
230
+ * @param {string} dir
231
+ * @param {(parentDir: string, callback: Function) => void} onParentDir
232
+ * @param {(err: Error, packageJsonPath: string) => void} callback
233
+ */
234
+ function searchInParentDir(dir, onParentDir, callback) {
235
+ const parentDir = path.resolve(dir, '..');
236
+ if (dir === parentDir) {
237
+ // We have arrived at the root of the file system hierarchy.
238
+ // findPackageJsonFor would have called callback asynchronously,
239
+ // so we use process.nextTick here to make all paths async.
240
+ return process.nextTick(callback, null, null);
241
+ }
242
+
243
+ return onParentDir(parentDir, callback);
244
+ }
245
+
246
+ module.exports = {
247
+ DependencyDistanceCalculator,
248
+ MAX_DEPTH: 15,
249
+ __moduleRefExportedForTest: module
250
+ };