ls.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578
  1. const { resolve, relative, sep } = require('path')
  2. const relativePrefix = `.${sep}`
  3. const { EOL } = require('os')
  4. const archy = require('archy')
  5. const chalk = require('chalk')
  6. const Arborist = require('@npmcli/arborist')
  7. const { breadth } = require('treeverse')
  8. const npa = require('npm-package-arg')
  9. const completion = require('./utils/completion/installed-deep.js')
  10. const _depth = Symbol('depth')
  11. const _dedupe = Symbol('dedupe')
  12. const _filteredBy = Symbol('filteredBy')
  13. const _include = Symbol('include')
  14. const _invalid = Symbol('invalid')
  15. const _name = Symbol('name')
  16. const _missing = Symbol('missing')
  17. const _parent = Symbol('parent')
  18. const _problems = Symbol('problems')
  19. const _required = Symbol('required')
  20. const _type = Symbol('type')
  21. const ArboristWorkspaceCmd = require('./workspaces/arborist-cmd.js')
  22. const localeCompare = require('@isaacs/string-locale-compare')('en')
  23. class LS extends ArboristWorkspaceCmd {
  24. /* istanbul ignore next - see test/lib/load-all-commands.js */
  25. static get description () {
  26. return 'List installed packages'
  27. }
  28. /* istanbul ignore next - see test/lib/load-all-commands.js */
  29. static get name () {
  30. return 'ls'
  31. }
  32. /* istanbul ignore next - see test/lib/load-all-commands.js */
  33. static get usage () {
  34. return ['[[<@scope>/]<pkg> ...]']
  35. }
  36. /* istanbul ignore next - see test/lib/load-all-commands.js */
  37. static get params () {
  38. return [
  39. 'all',
  40. 'json',
  41. 'long',
  42. 'parseable',
  43. 'global',
  44. 'depth',
  45. 'omit',
  46. 'link',
  47. 'package-lock-only',
  48. 'unicode',
  49. ...super.params,
  50. ]
  51. }
  52. /* istanbul ignore next - see test/lib/load-all-commands.js */
  53. async completion (opts) {
  54. return completion(this.npm, opts)
  55. }
  56. exec (args, cb) {
  57. this.ls(args).then(() => cb()).catch(cb)
  58. }
  59. async ls (args) {
  60. const all = this.npm.config.get('all')
  61. const color = this.npm.color
  62. const depth = this.npm.config.get('depth')
  63. const dev = this.npm.config.get('dev')
  64. const development = this.npm.config.get('development')
  65. const global = this.npm.config.get('global')
  66. const json = this.npm.config.get('json')
  67. const link = this.npm.config.get('link')
  68. const long = this.npm.config.get('long')
  69. const only = this.npm.config.get('only')
  70. const parseable = this.npm.config.get('parseable')
  71. const prod = this.npm.config.get('prod')
  72. const production = this.npm.config.get('production')
  73. const unicode = this.npm.config.get('unicode')
  74. const packageLockOnly = this.npm.config.get('package-lock-only')
  75. const workspacesEnabled = this.npm.flatOptions.workspacesEnabled
  76. const path = global ? resolve(this.npm.globalDir, '..') : this.npm.prefix
  77. const arb = new Arborist({
  78. global,
  79. ...this.npm.flatOptions,
  80. legacyPeerDeps: false,
  81. path,
  82. })
  83. const tree = await this.initTree({arb, args, packageLockOnly })
  84. // filters by workspaces nodes when using -w <workspace-name>
  85. // We only have to filter the first layer of edges, so we don't
  86. // explore anything that isn't part of the selected workspace set.
  87. let wsNodes
  88. if (this.workspaceNames && this.workspaceNames.length)
  89. wsNodes = arb.workspaceNodes(tree, this.workspaceNames)
  90. const filterBySelectedWorkspaces = edge => {
  91. if (!workspacesEnabled
  92. && edge.from.isProjectRoot
  93. && edge.to.isWorkspace
  94. )
  95. return false
  96. if (!wsNodes || !wsNodes.length)
  97. return true
  98. if (edge.from.isProjectRoot) {
  99. return edge.to &&
  100. edge.to.isWorkspace &&
  101. wsNodes.includes(edge.to.target)
  102. }
  103. return true
  104. }
  105. const seenItems = new Set()
  106. const seenNodes = new Map()
  107. const problems = new Set()
  108. // defines special handling of printed depth when filtering with args
  109. const filterDefaultDepth = depth === null ? Infinity : depth
  110. const depthToPrint = (all || args.length)
  111. ? filterDefaultDepth
  112. : (depth || 0)
  113. // add root node of tree to list of seenNodes
  114. seenNodes.set(tree.path, tree)
  115. // tree traversal happens here, using treeverse.breadth
  116. const result = await breadth({
  117. tree,
  118. // recursive method, `node` is going to be the current elem (starting from
  119. // the `tree` obj) that was just visited in the `visit` method below
  120. // `nodeResult` is going to be the returned `item` from `visit`
  121. getChildren (node, nodeResult) {
  122. const seenPaths = new Set()
  123. const workspace = node.isWorkspace
  124. const currentDepth = workspace ? 0 : node[_depth]
  125. const shouldSkipChildren =
  126. !(node instanceof Arborist.Node) || (currentDepth > depthToPrint)
  127. return (shouldSkipChildren)
  128. ? []
  129. : [...(node.target).edgesOut.values()]
  130. .filter(filterBySelectedWorkspaces)
  131. .filter(filterByEdgesTypes({
  132. currentDepth,
  133. dev,
  134. development,
  135. link,
  136. prod,
  137. production,
  138. only,
  139. }))
  140. .map(mapEdgesToNodes({ seenPaths }))
  141. .concat(appendExtraneousChildren({ node, seenPaths }))
  142. .sort(sortAlphabetically)
  143. .map(augmentNodesWithMetadata({
  144. args,
  145. currentDepth,
  146. nodeResult,
  147. seenNodes,
  148. }))
  149. },
  150. // visit each `node` of the `tree`, returning an `item` - these are
  151. // the elements that will be used to build the final output
  152. visit (node) {
  153. node[_problems] = getProblems(node, { global })
  154. const item = json
  155. ? getJsonOutputItem(node, { global, long })
  156. : parseable
  157. ? null
  158. : getHumanOutputItem(node, { args, color, global, long })
  159. // loop through list of node problems to add them to global list
  160. if (node[_include]) {
  161. for (const problem of node[_problems])
  162. problems.add(problem)
  163. }
  164. seenItems.add(item)
  165. // return a promise so we don't blow the stack
  166. return Promise.resolve(item)
  167. },
  168. })
  169. // handle the special case of a broken package.json in the root folder
  170. const [rootError] = tree.errors.filter(e =>
  171. e.code === 'EJSONPARSE' && e.path === resolve(path, 'package.json'))
  172. this.npm.output(
  173. json
  174. ? jsonOutput({ path, problems, result, rootError, seenItems })
  175. : parseable
  176. ? parseableOutput({ seenNodes, global, long })
  177. : humanOutput({ color, result, seenItems, unicode })
  178. )
  179. // if filtering items, should exit with error code on no results
  180. if (result && !result[_include] && args.length)
  181. process.exitCode = 1
  182. if (rootError) {
  183. throw Object.assign(
  184. new Error('Failed to parse root package.json'),
  185. { code: 'EJSONPARSE' }
  186. )
  187. }
  188. const shouldThrow = problems.size &&
  189. ![...problems].every(problem => problem.startsWith('extraneous:'))
  190. if (shouldThrow) {
  191. throw Object.assign(
  192. new Error([...problems].join(EOL)),
  193. { code: 'ELSPROBLEMS' }
  194. )
  195. }
  196. }
  197. async initTree ({ arb, args, packageLockOnly }) {
  198. const tree = await (
  199. packageLockOnly
  200. ? arb.loadVirtual()
  201. : arb.loadActual()
  202. )
  203. tree[_include] = args.length === 0
  204. tree[_depth] = 0
  205. return tree
  206. }
  207. }
  208. module.exports = LS
  209. const isGitNode = (node) => {
  210. if (!node.resolved)
  211. return
  212. try {
  213. const { type } = npa(node.resolved)
  214. return type === 'git' || type === 'hosted'
  215. } catch (err) {
  216. return false
  217. }
  218. }
  219. const isOptional = (node) =>
  220. node[_type] === 'optional' || node[_type] === 'peerOptional'
  221. const isExtraneous = (node, { global }) =>
  222. node.extraneous && !global
  223. const getProblems = (node, { global }) => {
  224. const problems = new Set()
  225. if (node[_missing] && !isOptional(node))
  226. problems.add(`missing: ${node.pkgid}, required by ${node[_missing]}`)
  227. if (node[_invalid])
  228. problems.add(`invalid: ${node.pkgid} ${node.path}`)
  229. if (isExtraneous(node, { global }))
  230. problems.add(`extraneous: ${node.pkgid} ${node.path}`)
  231. return problems
  232. }
  233. // annotates _parent and _include metadata into the resulting
  234. // item obj allowing for filtering out results during output
  235. const augmentItemWithIncludeMetadata = (node, item) => {
  236. item[_parent] = node[_parent]
  237. item[_include] = node[_include]
  238. // append current item to its parent.nodes which is the
  239. // structure expected by archy in order to print tree
  240. if (node[_include]) {
  241. // includes all ancestors of included node
  242. let p = node[_parent]
  243. while (p) {
  244. p[_include] = true
  245. p = p[_parent]
  246. }
  247. }
  248. return item
  249. }
  250. const getHumanOutputItem = (node, { args, color, global, long }) => {
  251. const { pkgid, path } = node
  252. const workspacePkgId = color ? chalk.green(pkgid) : pkgid
  253. let printable = node.isWorkspace ? workspacePkgId : pkgid
  254. // special formatting for top-level package name
  255. if (node.isRoot) {
  256. const hasNoPackageJson = !Object.keys(node.package).length
  257. if (hasNoPackageJson || global)
  258. printable = path
  259. else
  260. printable += `${long ? EOL : ' '}${path}`
  261. }
  262. const highlightDepName =
  263. color && args.length && node[_filteredBy]
  264. const missingColor = isOptional(node)
  265. ? chalk.yellow.bgBlack
  266. : chalk.red.bgBlack
  267. const missingMsg = `UNMET ${isOptional(node) ? 'OPTIONAL ' : ''}DEPENDENCY`
  268. const targetLocation = node.root
  269. ? relative(node.root.realpath, node.realpath)
  270. : node.targetLocation
  271. const invalid = node[_invalid]
  272. ? `invalid: ${node[_invalid]}`
  273. : ''
  274. const label =
  275. (
  276. node[_missing]
  277. ? (color ? missingColor(missingMsg) : missingMsg) + ' '
  278. : ''
  279. ) +
  280. `${highlightDepName ? chalk.yellow.bgBlack(printable) : printable}` +
  281. (
  282. node[_dedupe]
  283. ? ' ' + (color ? chalk.gray('deduped') : 'deduped')
  284. : ''
  285. ) +
  286. (
  287. invalid
  288. ? ' ' + (color ? chalk.red.bgBlack(invalid) : invalid)
  289. : ''
  290. ) +
  291. (
  292. isExtraneous(node, { global })
  293. ? ' ' + (color ? chalk.green.bgBlack('extraneous') : 'extraneous')
  294. : ''
  295. ) +
  296. (isGitNode(node) ? ` (${node.resolved})` : '') +
  297. (node.isLink ? ` -> ${relativePrefix}${targetLocation}` : '') +
  298. (long ? `${EOL}${node.package.description || ''}` : '')
  299. return augmentItemWithIncludeMetadata(node, { label, nodes: [] })
  300. }
  301. const getJsonOutputItem = (node, { global, long }) => {
  302. const item = {}
  303. if (node.version)
  304. item.version = node.version
  305. if (node.resolved)
  306. item.resolved = node.resolved
  307. item[_name] = node.name
  308. // special formatting for top-level package name
  309. const hasPackageJson =
  310. node && node.package && Object.keys(node.package).length
  311. if (node.isRoot && hasPackageJson)
  312. item.name = node.package.name || node.name
  313. if (long && !node[_missing]) {
  314. item.name = item[_name]
  315. const { dependencies, ...packageInfo } = node.package
  316. Object.assign(item, packageInfo)
  317. item.extraneous = false
  318. item.path = node.path
  319. item._dependencies = {
  320. ...node.package.dependencies,
  321. ...node.package.optionalDependencies,
  322. }
  323. item.devDependencies = node.package.devDependencies || {}
  324. item.peerDependencies = node.package.peerDependencies || {}
  325. }
  326. // augment json output items with extra metadata
  327. if (isExtraneous(node, { global }))
  328. item.extraneous = true
  329. if (node[_invalid])
  330. item.invalid = node[_invalid]
  331. if (node[_missing] && !isOptional(node)) {
  332. item.required = node[_required]
  333. item.missing = true
  334. }
  335. if (node[_include] && node[_problems] && node[_problems].size)
  336. item.problems = [...node[_problems]]
  337. return augmentItemWithIncludeMetadata(node, item)
  338. }
  339. const filterByEdgesTypes = ({
  340. currentDepth,
  341. dev,
  342. development,
  343. link,
  344. prod,
  345. production,
  346. only,
  347. }) => {
  348. // filter deps by type, allows for: `npm ls --dev`, `npm ls --prod`,
  349. // `npm ls --link`, `npm ls --only=dev`, etc
  350. const filterDev = currentDepth === 0 &&
  351. (dev || development || /^dev(elopment)?$/.test(only))
  352. const filterProd = currentDepth === 0 &&
  353. (prod || production || /^prod(uction)?$/.test(only))
  354. const filterLink = currentDepth === 0 && link
  355. return (edge) =>
  356. (filterDev ? edge.dev : true) &&
  357. (filterProd ? (!edge.dev && !edge.peer && !edge.peerOptional) : true) &&
  358. (filterLink ? (edge.to && edge.to.isLink) : true)
  359. }
  360. const appendExtraneousChildren = ({ node, seenPaths }) =>
  361. // extraneous children are not represented
  362. // in edges out, so here we add them to the list:
  363. [...node.children.values()]
  364. .filter(i => !seenPaths.has(i.path) && i.extraneous)
  365. const mapEdgesToNodes = ({ seenPaths }) => (edge) => {
  366. let node = edge.to
  367. // if the edge is linking to a missing node, we go ahead
  368. // and create a new obj that will represent the missing node
  369. if (edge.missing || (edge.optional && !node)) {
  370. const { name, spec } = edge
  371. const pkgid = `${name}@${spec}`
  372. node = { name, pkgid, [_missing]: edge.from.pkgid }
  373. }
  374. // keeps track of a set of seen paths to avoid the edge case in which a tree
  375. // item would appear twice given that it's a children of an extraneous item,
  376. // so it's marked extraneous but it will ALSO show up in edgesOuts of
  377. // its parent so it ends up as two diff nodes if we don't track it
  378. if (node.path)
  379. seenPaths.add(node.path)
  380. node[_required] = edge.spec || '*'
  381. node[_type] = edge.type
  382. if (edge.invalid) {
  383. const spec = JSON.stringify(node[_required])
  384. const from = edge.from.location || 'the root project'
  385. node[_invalid] = (node[_invalid] ? node[_invalid] + ', ' : '') +
  386. (`${spec} from ${from}`)
  387. }
  388. return node
  389. }
  390. const filterByPositionalArgs = (args, { node }) =>
  391. args.length > 0 ? args.some(
  392. (spec) => (node.satisfies && node.satisfies(spec))
  393. ) : true
  394. const augmentNodesWithMetadata = ({
  395. args,
  396. currentDepth,
  397. nodeResult,
  398. seenNodes,
  399. }) => (node) => {
  400. // if the original edge was a deduped dep, treeverse will fail to
  401. // revisit that node in tree traversal logic, so we make it so that
  402. // we have a diff obj for deduped nodes:
  403. if (seenNodes.has(node.path)) {
  404. const { realpath, root } = node
  405. const targetLocation = root ? relative(root.realpath, realpath)
  406. : node.targetLocation
  407. node = {
  408. name: node.name,
  409. version: node.version,
  410. pkgid: node.pkgid,
  411. package: node.package,
  412. path: node.path,
  413. isLink: node.isLink,
  414. realpath: node.realpath,
  415. targetLocation,
  416. [_type]: node[_type],
  417. [_invalid]: node[_invalid],
  418. [_missing]: node[_missing],
  419. // if it's missing, it's not deduped, it's just missing
  420. [_dedupe]: !node[_missing],
  421. }
  422. } else {
  423. // keeps track of already seen nodes in order to check for dedupes
  424. seenNodes.set(node.path, node)
  425. }
  426. // _parent is going to be a ref to a treeverse-visited node (returned from
  427. // getHumanOutputItem, getJsonOutputItem, etc) so that we have an easy
  428. // shortcut to place new nodes in their right place during tree traversal
  429. node[_parent] = nodeResult
  430. // _include is the property that allow us to filter based on position args
  431. // e.g: `npm ls foo`, `npm ls simple-output@2`
  432. // _filteredBy is used to apply extra color info to the item that
  433. // was used in args in order to filter
  434. node[_filteredBy] = node[_include] =
  435. filterByPositionalArgs(args, { node: seenNodes.get(node.path) })
  436. // _depth keeps track of how many levels deep tree traversal currently is
  437. // so that we can `npm ls --depth=1`
  438. node[_depth] = currentDepth + 1
  439. return node
  440. }
  441. const sortAlphabetically = ({ pkgid: a }, { pkgid: b }) => localeCompare(a, b)
  442. const humanOutput = ({ color, result, seenItems, unicode }) => {
  443. // we need to traverse the entire tree in order to determine which items
  444. // should be included (since a nested transitive included dep will make it
  445. // so that all its ancestors should be displayed)
  446. // here is where we put items in their expected place for archy output
  447. for (const item of seenItems) {
  448. if (item[_include] && item[_parent])
  449. item[_parent].nodes.push(item)
  450. }
  451. if (!result.nodes.length)
  452. result.nodes = ['(empty)']
  453. const archyOutput = archy(result, '', { unicode })
  454. return color ? chalk.reset(archyOutput) : archyOutput
  455. }
  456. const jsonOutput = ({ path, problems, result, rootError, seenItems }) => {
  457. if (problems.size)
  458. result.problems = [...problems]
  459. if (rootError) {
  460. result.problems = [
  461. ...(result.problems || []),
  462. ...[`error in ${path}: Failed to parse root package.json`],
  463. ]
  464. result.invalid = true
  465. }
  466. // we need to traverse the entire tree in order to determine which items
  467. // should be included (since a nested transitive included dep will make it
  468. // so that all its ancestors should be displayed)
  469. // here is where we put items in their expected place for json output
  470. for (const item of seenItems) {
  471. // append current item to its parent item.dependencies obj in order
  472. // to provide a json object structure that represents the installed tree
  473. if (item[_include] && item[_parent]) {
  474. if (!item[_parent].dependencies)
  475. item[_parent].dependencies = {}
  476. item[_parent].dependencies[item[_name]] = item
  477. }
  478. }
  479. return JSON.stringify(result, null, 2)
  480. }
  481. const parseableOutput = ({ global, long, seenNodes }) => {
  482. let out = ''
  483. for (const node of seenNodes.values()) {
  484. if (node.path && node[_include]) {
  485. out += node.path
  486. if (long) {
  487. out += `:${node.pkgid}`
  488. out += node.path !== node.realpath ? `:${node.realpath}` : ''
  489. out += isExtraneous(node, { global }) ? ':EXTRANEOUS' : ''
  490. out += node[_invalid] ? ':INVALID' : ''
  491. }
  492. out += EOL
  493. }
  494. }
  495. return out.trim()
  496. }