@instana/shared-metrics 1.128.0 → 1.131.0
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 +9 -5
- package/src/dependencies.js +178 -41
- package/src/directDependencies.js +3 -0
- package/src/index.js +4 -3
- package/src/util/CountDownLatch.js +34 -0
- package/src/util/DependencyDistanceCalculator.js +250 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@instana/shared-metrics",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.131.0",
|
|
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.
|
|
62
|
+
"@instana/core": "1.131.0",
|
|
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.
|
|
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": "
|
|
81
|
+
"gitHead": "c143ddafc50f8305684638f5abff0ee847830408"
|
|
78
82
|
}
|
package/src/dependencies.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
70
|
-
|
|
71
|
-
if (
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
.
|
|
77
|
-
(
|
|
78
|
-
|
|
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
|
-
*
|
|
105
|
-
*
|
|
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,
|
|
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
|
-
|
|
182
|
+
countDownLatch.countDown();
|
|
183
|
+
logger.debug(`No package.json at ${packageJsonPath}, ignoring this directory.`);
|
|
184
|
+
return;
|
|
113
185
|
} else if (err) {
|
|
114
|
-
|
|
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
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
|
40
|
-
* @property {
|
|
41
|
-
* @property {
|
|
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
|
+
};
|