• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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