Team Ai
Datasetpublic

codekingpro/portable-devtools

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