Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
outdated.js286 linesDownload Raw Back to commands
1const { resolve } = require('node:path')
2const { stripVTControlCharacters } = require('node:util')
3const pacote = require('pacote')
4const table = require('text-table')
5const npa = require('npm-package-arg')
6const pickManifest = require('npm-pick-manifest')
7const { output } = require('proc-log')
8const localeCompare = require('@isaacs/string-locale-compare')('en')
9const ArboristWorkspaceCmd = require('../arborist-cmd.js')
10
11const safeNpa = (spec) => {
12  try {
13    return npa(spec)
14  } catch {
15    return null
16  }
17}
18
19// This string is load bearing and is shared with Arborist
20const MISSING = 'MISSING'
21
22class Outdated extends ArboristWorkspaceCmd {
23  static description = 'Check for outdated packages'
24  static name = 'outdated'
25  static usage = ['[<package-spec> ...]']
26  static params = [
27    'all',
28    'json',
29    'long',
30    'parseable',
31    'global',
32    'workspace',
33    'before',
34  ]
35
36  #tree
37  #list = []
38  #edges = new Set()
39  #filterSet
40
41  async exec (args) {
42    const Arborist = require('@npmcli/arborist')
43    const arb = new Arborist({
44      ...this.npm.flatOptions,
45      path: this.npm.global ? resolve(this.npm.globalDir, '..') : this.npm.prefix,
46    })
47    this.#tree = await arb.loadActual()
48
49    if (this.workspaceNames?.length) {
50      this.#filterSet = arb.workspaceDependencySet(
51        this.#tree,
52        this.workspaceNames,
53        this.npm.flatOptions.includeWorkspaceRoot
54      )
55    } else if (!this.npm.flatOptions.workspacesEnabled) {
56      this.#filterSet = arb.excludeWorkspacesDependencySet(this.#tree)
57    }
58
59    if (args.length) {
60      for (const arg of args) {
61        // specific deps
62        this.#getEdges(this.#tree.inventory.query('name', arg), 'edgesIn')
63      }
64    } else {
65      if (this.npm.config.get('all')) {
66        // all deps in tree
67        this.#getEdges(this.#tree.inventory.values(), 'edgesOut')
68      }
69      // top-level deps
70      this.#getEdges()
71    }
72
73    await Promise.all([...this.#edges].map((e) => this.#getOutdatedInfo(e)))
74
75    // sorts list alphabetically by name and then dependent
76    const outdated = this.#list
77      .sort((a, b) => localeCompare(a.name, b.name) || localeCompare(a.dependent, b.dependent))
78
79    if (outdated.length) {
80      process.exitCode = 1
81    }
82
83    if (this.npm.config.get('json')) {
84      output.buffer(this.#json(outdated))
85      return
86    }
87
88    const res = this.npm.config.get('parseable')
89      ? this.#parseable(outdated)
90      : this.#pretty(outdated)
91
92    if (res) {
93      output.standard(res)
94    }
95  }
96
97  #getEdges (nodes, type) {
98    // when no nodes are provided then it should only read direct deps from the root node and its workspaces direct dependencies
99    if (!nodes) {
100      this.#getEdgesOut(this.#tree)
101      this.#getWorkspacesEdges()
102      return
103    }
104
105    for (const node of nodes) {
106      if (type === 'edgesOut') {
107        this.#getEdgesOut(node)
108      } else {
109        this.#getEdgesIn(node)
110      }
111    }
112  }
113
114  #getEdgesIn (node) {
115    for (const edge of node.edgesIn) {
116      this.#trackEdge(edge)
117    }
118  }
119
120  #getEdgesOut (node) {
121    // TODO: normalize usage of edges and avoid looping through nodes here
122    const edges = this.npm.global ? node.children.values() : node.edgesOut.values()
123    for (const edge of edges) {
124      this.#trackEdge(edge)
125    }
126  }
127
128  #trackEdge (edge) {
129    if (edge.from && this.#filterSet?.size > 0 && !this.#filterSet.has(edge.from.target)) {
130      return
131    }
132    this.#edges.add(edge)
133  }
134
135  #getWorkspacesEdges () {
136    if (this.npm.global) {
137      return
138    }
139
140    for (const edge of this.#tree.edgesOut.values()) {
141      if (edge?.to?.target?.isWorkspace) {
142        this.#getEdgesOut(edge.to.target)
143      }
144    }
145  }
146
147  async #getPackument (spec) {
148    return pacote.packument(spec, {
149      ...this.npm.flatOptions,
150      fullMetadata: this.npm.config.get('long'),
151      preferOnline: true,
152    })
153  }
154
155  async #getOutdatedInfo (edge) {
156    const alias = safeNpa(edge.spec)?.subSpec
157    const spec = npa(alias ? alias.name : edge.name)
158    const node = edge.to || edge
159    const { path, location, package: { version: current } = {} } = node
160
161    const type = edge.optional ? 'optionalDependencies'
162      : edge.peer ? 'peerDependencies'
163      : edge.dev ? 'devDependencies'
164      : 'dependencies'
165
166    for (const omitType of this.npm.flatOptions.omit) {
167      if (node[omitType]) {
168        return
169      }
170    }
171
172    // deps different from prod not currently on disk are not included in the output
173    if (edge.error === MISSING && type !== 'dependencies') {
174      return
175    }
176
177    // if it's not a range, version, or tag, skip it
178    if (!safeNpa(`${edge.name}@${edge.spec}`)?.registry) {
179      return null
180    }
181
182    try {
183      const packument = await this.#getPackument(spec)
184      const expected = alias ? alias.fetchSpec : edge.spec
185      const wanted = pickManifest(packument, expected, this.npm.flatOptions)
186      const latest = pickManifest(packument, '*', this.npm.flatOptions)
187      if (!current || current !== wanted.version || wanted.version !== latest.version) {
188        this.#list.push({
189          name: alias ? edge.spec.replace('npm', edge.name) : edge.name,
190          path,
191          type,
192          current,
193          location,
194          wanted: wanted.version,
195          latest: latest.version,
196          workspaceDependent: edge.from?.isWorkspace ? edge.from.pkgid : null,
197          dependedByLocation: edge.from?.name
198            ? edge.from?.location
199            : 'global',
200          dependent: edge.from?.name ?? 'global',
201          homepage: packument.homepage,
202        })
203      }
204    } catch (err) {
205      // silently catch and ignore ETARGET, E403 & E404 errors
206      // deps are just skipped
207      if (!['ETARGET', 'E403', 'E404'].includes(err.code)) {
208        throw err
209      }
210    }
211  }
212
213  // formatting functions
214
215  #pretty (list) {
216    if (!list.length) {
217      return
218    }
219
220    const long = this.npm.config.get('long')
221    const { bold, yellow, red, cyan, blue } = this.npm.chalk
222
223    return table([
224      [
225        'Package',
226        'Current',
227        'Wanted',
228        'Latest',
229        'Location',
230        'Depended by',
231        ...long ? ['Package Type', 'Homepage', 'Depended By Location'] : [],
232      ].map(h => bold.underline(h)),
233      ...list.map((d) => [
234        d.current === d.wanted ? yellow(d.name) : red(d.name),
235        d.current ?? 'MISSING',
236        cyan(d.wanted),
237        blue(d.latest),
238        d.location ?? '-',
239        d.workspaceDependent ? blue(d.workspaceDependent) : d.dependent,
240        ...long ? [d.type, blue(d.homepage ?? ''), d.dependedByLocation] : [],
241      ]),
242    ], {
243      align: ['l', 'r', 'r', 'r', 'l'],
244      stringLength: s => stripVTControlCharacters(s).length,
245    })
246  }
247
248  // --parseable creates output like this:
249  // <fullpath>:<name@wanted>:<name@installed>:<name@latest>:<dependedby>
250  #parseable (list) {
251    return list.map(d => [
252      d.path,
253      `${d.name}@${d.wanted}`,
254      d.current ? `${d.name}@${d.current}` : 'MISSING',
255      `${d.name}@${d.latest}`,
256      d.dependent,
257      ...this.npm.config.get('long') ? [d.type, d.homepage, d.dependedByLocation] : [],
258    ].join(':')).join('\n')
259  }
260
261  #json (list) {
262    // TODO(BREAKING_CHANGE): this should just return an array.
263    // It's a list and turning it into an object with keys is lossy since multiple items in the list could have the same key. For now we hack that by only changing top level values into arrays if they have multiple outdated items
264    return list.reduce((acc, d) => {
265      const dep = {
266        current: d.current,
267        wanted: d.wanted,
268        latest: d.latest,
269        dependent: d.dependent,
270        location: d.path,
271        ...this.npm.config.get('long') ? {
272          type: d.type,
273          homepage: d.homepage,
274          dependedByLocation: d.dependedByLocation } : {},
275      }
276      acc[d.name] = acc[d.name]
277        // If this item already has an outdated dep then we turn it into an array
278        ? (Array.isArray(acc[d.name]) ? acc[d.name] : [acc[d.name]]).concat(dep)
279        : dep
280      return acc
281    }, {})
282  }
283}
284
285module.exports = Outdated
286 
codekingpro/portable-devtools · Team Ai