1const { explainNode } = require('../utils/explain-dep.js') 2const npa = require('npm-package-arg') 3const semver = require('semver') 4const { relative, resolve } = require('path') 5const validName = require('validate-npm-package-name') 6const ArboristWorkspaceCmd = require('../arborist-cmd.js') 7 8class Explain extends ArboristWorkspaceCmd { 9 static description = 'Explain installed packages' 10 static name = 'explain' 11 static usage = ['<package-spec>'] 12 static params = [ 13 'json', 14 'workspace', 15 ] 16 17 static ignoreImplicitWorkspace = false 18 19 // TODO 20 /* istanbul ignore next */ 21 static async completion (opts, npm) { 22 const completion = require('../utils/completion/installed-deep.js') 23 return completion(npm, opts) 24 } 25 26 async exec (args) { 27 if (!args.length) { 28 throw this.usageError() 29 } 30 31 const Arborist = require('@npmcli/arborist') 32 const arb = new Arborist({ path: this.npm.prefix, ...this.npm.flatOptions }) 33 const tree = await arb.loadActual() 34 35 if (this.npm.flatOptions.workspacesEnabled 36 && this.workspaceNames 37 && this.workspaceNames.length 38 ) { 39 this.filterSet = arb.workspaceDependencySet(tree, this.workspaceNames) 40 } else if (!this.npm.flatOptions.workspacesEnabled) { 41 this.filterSet = 42 arb.excludeWorkspacesDependencySet(tree) 43 } 44 45 const nodes = new Set() 46 for (const arg of args) { 47 for (const node of this.getNodes(tree, arg)) { 48 const filteredOut = this.filterSet 49 && this.filterSet.size > 0 50 && !this.filterSet.has(node) 51 if (!filteredOut) { 52 nodes.add(node) 53 } 54 } 55 } 56 if (nodes.size === 0) { 57 throw new Error(`No dependencies found matching ${args.join(', ')}`) 58 } 59 60 const expls = [] 61 for (const node of nodes) { 62 const { extraneous, dev, optional, devOptional, peer, inBundle, overridden } = node 63 const expl = node.explain() 64 if (extraneous) { 65 expl.extraneous = true 66 } else { 67 expl.dev = dev 68 expl.optional = optional 69 expl.devOptional = devOptional 70 expl.peer = peer 71 expl.bundled = inBundle 72 expl.overridden = overridden 73 } 74 expls.push(expl) 75 } 76 77 if (this.npm.flatOptions.json) { 78 this.npm.output(JSON.stringify(expls, null, 2)) 79 } else { 80 this.npm.output(expls.map(expl => { 81 return explainNode(expl, Infinity, this.npm.chalk) 82 }).join('\n\n')) 83 } 84 } 85 86 getNodes (tree, arg) { 87 // if it's just a name, return packages by that name 88 const { validForOldPackages: valid } = validName(arg) 89 if (valid) { 90 return tree.inventory.query('packageName', arg) 91 } 92 93 // if it's a location, get that node 94 const maybeLoc = arg.replace(/\\/g, '/').replace(/\/+$/, '') 95 const nodeByLoc = tree.inventory.get(maybeLoc) 96 if (nodeByLoc) { 97 return [nodeByLoc] 98 } 99 100 // maybe a path to a node_modules folder 101 const maybePath = relative(this.npm.prefix, resolve(maybeLoc)) 102 .replace(/\\/g, '/').replace(/\/+$/, '') 103 const nodeByPath = tree.inventory.get(maybePath) 104 if (nodeByPath) { 105 return [nodeByPath] 106 } 107 108 // otherwise, try to select all matching nodes 109 try { 110 return this.getNodesByVersion(tree, arg) 111 } catch (er) { 112 return [] 113 } 114 } 115 116 getNodesByVersion (tree, arg) { 117 const spec = npa(arg, this.npm.prefix) 118 if (spec.type !== 'version' && spec.type !== 'range') { 119 return [] 120 } 121 122 return tree.inventory.filter(node => { 123 return node.package.name === spec.name && 124 semver.satisfies(node.package.version, spec.rawSpec) 125 }) 126 } 127} 128module.exports = Explain 129