Skip to content

Commit eac9b02

Browse files
authored
Merge pull request #19 from pkgjs/feat-7
Detect node support of dependencies
2 parents 0cf79dc + b5da479 commit eac9b02

23 files changed

+1037
-72
lines changed

.gitignore

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
**/node_modules
2-
**/package-lock.json
3-
**/npm-shrinkwrap.json
2+
/package-lock.json
3+
/npm-shrinkwrap.json
44

55
coverage.*
66
*.log*

.labrc.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use strict';
2+
3+
exports.globals = [
4+
'Symbol(__RESOLVED_TEMP_DIRECTORY__)'
5+
].join(',');

README.md

+57-6
Original file line numberDiff line numberDiff line change
@@ -5,45 +5,63 @@ List the Node.js versions supported by the package/repository
55
## Usage (command line)
66

77
```
8-
$ npx detect-node-support [path]
8+
$ npx detect-node-support [options] <path>
99
```
1010

1111
Prints the supported Node.js versions for the package at the specified path. When the path is not a git repository - tries to read the git repository from `package.json` and tries to detect the versions listed in the repository as well.
1212

1313
When `path` is omitted, tries to detect the versions for `cwd`.
1414

1515
```
16-
$ npx detect-node-support [package name]
16+
$ npx detect-node-support [options] <package name>
1717
```
1818

1919
Prints supported Node.js versions for the package from the registry.
2020

2121
```
22-
$ npx detect-node-support [repository git URL]
22+
$ npx detect-node-support [options] <repository git URL>
2323
```
2424

2525
Prints supported Node.js versions for the package at the git URL.
2626

27+
### Options
28+
29+
* `--deep` - when used with --deps, include indirect dependencies
30+
* `--deps` - include the support information of direct production dependencies
31+
* `--dev` - when used with --deps, include dev dependencies
32+
2733
## Usage (library)
2834

2935
```
30-
const result = await require('detect-node-support').detect({ path });
36+
const result = await require('detect-node-support').detect({ path }, options);
3137
```
3238

3339
`path` should be a folder in the local file system. When the path is not a git repository - tries to read the git repository from `package.json` and tries to detect the versions listed in the repository as well.
3440

3541
```
36-
const result = await require('detect-node-support').detect({ packageName });
42+
const result = await require('detect-node-support').detect({ packageName }, options);
3743
```
3844

3945
`packageName` is a string name for the package in the registry.
4046

4147
```
42-
const result = await require('detect-node-support').detect({ repository });
48+
const result = await require('detect-node-support').detect({ repository }, options);
4349
```
4450

4551
`repository` is a URL for a git repository.
4652

53+
```
54+
const result = await require('detect-node-support').detect(what, options);
55+
```
56+
57+
`what` is a string containing either a package name, or a local path, or a reference to a git repository.
58+
59+
### Options
60+
61+
- `deep: false` - when `true` and used `deps: true`, include indirect dependencies
62+
- `deps: false` - when `true`, include the support information of all dependencies.
63+
- `dev: false` - when `true` and used with `deps: true`, include dev dependencies
64+
4765
### Result
4866

4967
- Throws if the `path` / `repository` does not have a `package.json`
@@ -88,6 +106,39 @@ const result = {
88106
"lts/*": "12.14.0",
89107
"invalid-specifier": false
90108
}
109+
},
110+
111+
// only present when explicitly requested
112+
"dependencies": {
113+
114+
// will contain a support object for every unique dependency in the tree
115+
// note that the `version` will be the _latest_ version available in the registry
116+
// see below for the actual versions installed
117+
"support": [
118+
{
119+
"name": "dependency-A"
120+
/*... other fields ...*/
121+
},
122+
{
123+
"name": "dependency-B"
124+
/*... other fields ...*/
125+
}
126+
],
127+
128+
// will contain a list of unique versions for each dependency found in the dependency tree
129+
"versions": {
130+
"dependency-A": ["0.0.10", "1.2.5"],
131+
"dependency-B": ["0.5.3", "1.0.0"],
132+
"dependency-C": ["7.8.9"]
133+
},
134+
135+
// will contain a list of errors that were encountered while resolving dependency support information
136+
"errors": {
137+
"dependency-C": {
138+
// the `message` will always be either a string or `null`
139+
"message": "Failed to download some information or something"
140+
}
141+
}
91142
}
92143
}
93144
```

bin/detect-node-support

+37-30
Original file line numberDiff line numberDiff line change
@@ -2,58 +2,65 @@
22

33
'use strict';
44

5+
const Debug = require('debug');
56
const Fs = require('fs');
6-
const NodeSupport = require('..');
7-
const { URL } = require('url');
7+
const Minimist = require('minimist');
8+
const Tmp = require('tmp');
9+
const Util = require('util');
810

11+
const NodeSupport = require('..');
912

1013
const internals = {};
1114

12-
internals.help = () => {
13-
14-
return `Usage: detect-node-support [ path | Github URL | npm package name ]`;
15-
};
1615

16+
internals.log = Debug('detect-node-support');
1717

18-
internals.autoDetect = (what) => {
1918

20-
try {
21-
var url = new URL(what);
22-
}
23-
catch (err) {
24-
if (err.code !== 'ERR_INVALID_URL') {
25-
throw err;
26-
}
27-
}
19+
internals.help = () => {
2820

29-
if (url) {
30-
return NodeSupport.detect({ repository: url.href });
31-
}
21+
return `
22+
Usage: detect-node-support [--deps] [--dev] [--deep] [--json] <what>
3223
33-
if (Fs.existsSync(what)) {
34-
return NodeSupport.detect({ path: what });
35-
}
24+
<what> can be an npm package name, or a Github URL, or a path
25+
with a package.json.
3626
37-
if (what.includes('/') && !what.startsWith('@')) {
38-
return NodeSupport.detect({ repository: `https://github.com./${what}` });
39-
}
40-
41-
return NodeSupport.detect({ packageName: what });
27+
Options:
28+
--deep When used with --deps, include indirect dependencies
29+
--deps Include the support information of direct production dependencies
30+
--dev When used with --deps, include direct dev dependencies
31+
--json Print JSON formatted output
32+
`;
4233
};
4334

44-
exports.main = async (nodeBin, thisBin, what) => {
35+
exports.main = async ({ _: [what], deps, deep, dev, json }) => {
4536

4637
if (!what) {
4738
console.log(internals.help());
4839
return;
4940
}
5041

51-
const result = await internals.autoDetect(what);
42+
if (!deps && (deep || dev)) {
43+
console.log('--deep and --dev can only be used together with --deps\n');
44+
console.log(internals.help());
45+
return;
46+
}
5247

53-
console.log(result);
48+
const result = await NodeSupport.detect(what, { deps, deep, dev });
49+
50+
const jsonOutput = JSON.stringify(result, null, ' ');
51+
const tmpFile = Tmp.fileSync({ postfix: '.json' });
52+
Fs.writeFileSync(tmpFile.name, jsonOutput);
53+
54+
if (json) {
55+
console.log(jsonOutput);
56+
}
57+
else {
58+
console.log(Util.inspect(result, false, null, true));
59+
console.log(`Full output available in ${tmpFile.name}`);
60+
}
5461
};
5562

56-
exports.main(...process.argv)
63+
exports.main(Minimist(process.argv.slice(2), { boolean: ['deps', 'dev', 'deep', 'json'] }))
5764
.catch((err) => {
5865

5966
console.error(err);

lib/deps.js

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
'use strict';
2+
3+
const Debug = require('debug');
4+
const { Arborist } = require('@npmcli/arborist');
5+
const Fs = require('fs');
6+
const Path = require('path');
7+
const Tmp = require('tmp');
8+
9+
const Package = require('./package');
10+
const Utils = require('./utils');
11+
12+
const internals = {};
13+
14+
15+
internals.log = Debug('detect-node-support');
16+
17+
18+
internals.walk = (node, callback) => {
19+
20+
callback(node);
21+
22+
node.children.forEach((child) => {
23+
24+
internals.walk(child, callback);
25+
});
26+
};
27+
28+
29+
internals.resolve = async ({ packageJson, lockfile }, options) => {
30+
31+
const { deep, dev } = options;
32+
33+
const tmpDir = Tmp.dirSync({ unsafeCleanup: true });
34+
const path = tmpDir.name;
35+
36+
Fs.writeFileSync(Path.join(path, 'package.json'), JSON.stringify(packageJson, null, ' '));
37+
38+
if (lockfile) {
39+
Fs.writeFileSync(Path.join(path, 'package-lock.json'), JSON.stringify(lockfile, null, ' '));
40+
}
41+
42+
const direct = new Set();
43+
['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'].forEach((depType) => {
44+
45+
if (!packageJson[depType]) {
46+
return;
47+
}
48+
49+
Object.keys(packageJson[depType]).forEach((dep) => direct.add(dep));
50+
});
51+
52+
const arborist = new Arborist({ path });
53+
54+
await arborist.buildIdealTree();
55+
56+
const map = {};
57+
58+
internals.walk(arborist.idealTree, (node) => {
59+
60+
if (node.isRoot) {
61+
return;
62+
}
63+
64+
if (!dev && node.dev) {
65+
// only include dev deps when `options.dev` flag is set
66+
return;
67+
}
68+
69+
if (!deep && !node.parent.isRoot) {
70+
// only include deep deps when `options.deep` flag is set
71+
return;
72+
}
73+
74+
if (!deep && !direct.has(node.name)) {
75+
// only include deep deps when `options.deep` flag is set
76+
// workaround for https://github.com./npm/arborist/issues/60
77+
return;
78+
}
79+
80+
map[node.name] = map[node.name] || new Set();
81+
map[node.name].add(node.package.version);
82+
});
83+
84+
const result = {};
85+
86+
for (const name of Object.keys(map).sort()) {
87+
result[name] = [...map[name]];
88+
}
89+
90+
tmpDir.removeCallback();
91+
92+
return result;
93+
};
94+
95+
internals.tryLoad = async (loadFile, filename) => {
96+
97+
try {
98+
return await loadFile(filename, { json: true });
99+
}
100+
catch (err) {
101+
if (err.code !== 'ENOENT') {
102+
throw err;
103+
}
104+
}
105+
};
106+
107+
exports.detect = async ({ packageJson, loadFile }, options) => {
108+
109+
const lockfile = (await internals.tryLoad(loadFile, 'package-lock.json')) || (await internals.tryLoad(loadFile, 'npm-shrinkwrap.json'));
110+
internals.log(lockfile ? 'Lock file present' : 'Lock file missing - things will be a bit slower');
111+
112+
const versions = await internals.resolve({ packageJson, lockfile }, options);
113+
114+
const support = [];
115+
const errors = {};
116+
let hasErrors = false;
117+
118+
const packages = Object.keys(versions).sort();
119+
const n = packages.length;
120+
121+
for (let i = 0; i < n; ++i) {
122+
123+
const packageName = packages[i];
124+
internals.log(`Resolving dependency ${i + 1} of ${n}: ${packageName}`);
125+
126+
try {
127+
const { result } = await Package.detect({ packageName });
128+
support.push(result);
129+
}
130+
catch (err) {
131+
hasErrors = true;
132+
errors[packageName] = {
133+
message: Utils.getErrorMessage(err)
134+
};
135+
}
136+
}
137+
138+
const result = { support, versions };
139+
140+
if (hasErrors) {
141+
result.errors = errors;
142+
}
143+
144+
return result;
145+
};

lib/engines.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
'use strict';
22

3-
exports.detect = ({ engines }) => {
3+
exports.detect = ({ packageJson }) => {
44

5-
if (engines) {
5+
if (packageJson.engines) {
66

77
return {
8-
engines: engines.node
8+
engines: packageJson.engines.node
99
};
1010
}
1111
};

0 commit comments

Comments
 (0)