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