Skip to content

Commit 45f2d95

Browse files
authored
feat: support npm lockfile v2 (#124)
* feat: support lockfile v2 * test: add test for lockfile v2 * chore: bump deps * chore: add @types/node * test: make standard happy * test: Travis CI => GitHub CI * chore: add root package info to output * chore: remove engine requirements
1 parent 9e7ef8a commit 45f2d95

File tree

12 files changed

+3294
-1164
lines changed

12 files changed

+3294
-1164
lines changed

.github/workflows/node.js.yml

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Node.js CI
2+
3+
on:
4+
push:
5+
branches: [ master ]
6+
pull_request:
7+
branches: [ master ]
8+
9+
jobs:
10+
build:
11+
12+
runs-on: ubuntu-latest
13+
14+
strategy:
15+
matrix:
16+
node-version: [10.x, 12.x, 14.x]
17+
18+
steps:
19+
- uses: actions/checkout@v2
20+
- name: Use Node.js ${{ matrix.node-version }}
21+
uses: actions/setup-node@v1
22+
with:
23+
node-version: ${{ matrix.node-version }}
24+
- run: npm ci
25+
- run: npm run build --if-present
26+
- run: npm test

.travis.yml

-9
This file was deleted.

bin/npm-why

+21-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
#! /usr/bin/env node
22

3+
const path = require('path')
34
const mri = require('mri')
45
const kleur = require('kleur')
5-
const main = require('../cli-main.js')
6+
7+
const { collectReasons, printReasons } = require('../index.js')
68

79
const help = `
810
${kleur.bold('npm-why')} - Identifies why a package has been installed.
@@ -38,3 +40,21 @@ if (args.help) {
3840
const pkg = args._[0]
3941
main(dir, pkg)
4042
}
43+
44+
/**
45+
* Run npm-why in a directory
46+
*
47+
* @param {String} dir Workding directory
48+
* @param {String} packageName The package to lookup
49+
*/
50+
async function main (dir, packageName) {
51+
const reasons = await collectReasons(dir, packageName)
52+
53+
if (!reasons.length) {
54+
console.log(`\n No one requires ${kleur.blue(packageName)}.`)
55+
} else {
56+
console.log(`\n Who required ${kleur.blue(packageName)}:\n`)
57+
printReasons(reasons, path.parse(dir).name)
58+
console.log('\n')
59+
}
60+
}

cli-main.js

-47
This file was deleted.

index.js

+53-37
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,69 @@
1-
const logicalTree = require('npm-logical-tree')
1+
const kleur = require('kleur')
2+
const Arborist = require('@npmcli/arborist')
23

34
/**
4-
* Find who depend upon a target package.
5+
* Run npm-why in a directory
56
*
6-
* @param {Object} pkg package.json
7-
* @param {Object} pkgLock package-lock.json
8-
* @param {String} target target package name to lookup
7+
* @param {String} dir Workding directory
8+
* @param {String} packageName The package to lookup
99
*/
10-
module.exports = function (pkg, pkgLock, name) {
11-
const tree = logicalTree(pkg, pkgLock)
12-
13-
// Find target deps
14-
const targets = new Set()
15-
tree.forEach((dep, callback) => {
16-
dep.name === name && targets.add(dep)
17-
callback()
18-
})
19-
20-
// Find requiredBy for those targets
21-
const results = []
22-
targets.forEach(t => {
23-
lookupDependents(t, tree, [], results)
24-
})
25-
return results
26-
}
10+
async function collectReasons (dir, packageName) {
11+
const arb = new Arborist({ path: dir })
12+
const tree = await arb.loadVirtual()
13+
const pkgs = tree.children.keys()
2714

28-
function lookupDependents (target, root, breadcrumb = [], results) {
29-
breadcrumb = breadcrumb.concat(target)
15+
let reasons = []
3016

31-
if (target.requiredBy.size === 0) {
32-
return results.push(formatBreadcrumb(breadcrumb))
17+
// find all versions of <packageName>
18+
for (const name of pkgs) {
19+
if (name === packageName) {
20+
const reasonChain = buildDependentsTree(tree.children.get(name))
21+
reasons = reasons.concat(reasonChain.leafs)
22+
}
3323
}
3424

35-
if (breadcrumb.includes(root)) {
36-
return results.push(formatBreadcrumb(breadcrumb))
25+
// prepand root package info to every reason chain
26+
const root = { name: tree.name, version: tree.version }
27+
reasons = reasons.map(chain => chain.concat(root))
28+
29+
return reasons
30+
}
31+
32+
function buildDependentsTree (node, paths = [], leafs = []) {
33+
const { name, version, edgesIn } = node
34+
35+
// when leaf node
36+
if (edgesIn.size === 0) {
37+
leafs.push(paths)
38+
return { name, version, leafs }
3739
}
3840

39-
if (breadcrumb.slice(0, -1).includes(target)) {
40-
return // Recursive dependency
41+
// prevent dead loops
42+
if (paths.includes(name)) {
43+
return { name, version, leafs }
4144
}
4245

43-
target.requiredBy.forEach(dependent => {
44-
lookupDependents(dependent, root, breadcrumb, results)
46+
const dependents = Array.from(edgesIn).map(edge => {
47+
const pkg = { name, version }
48+
return buildDependentsTree(edge.from, paths.concat(pkg), leafs)
4549
})
50+
51+
return { name, version, leafs, dependents }
52+
}
53+
54+
function printReasons (reasons, rootPackage) {
55+
reasons.map(reason => {
56+
const versionTag = kleur.dim('@' + reason[0].version)
57+
58+
const line = reason.reverse().map(pkg => {
59+
return kleur.blue(pkg.name || rootPackage)
60+
}).join(' > ')
61+
62+
return line + versionTag
63+
}).sort().forEach(line => console.log(' ', line))
4664
}
4765

48-
function formatBreadcrumb (bc) {
49-
return bc.map(piece => ({
50-
name: piece.name,
51-
version: piece.version
52-
}))
66+
module.exports = {
67+
collectReasons,
68+
printReasons
5369
}

0 commit comments

Comments
 (0)