mirror of https://github.com/nodejs/node.git
283 lines
7.7 KiB
JavaScript
283 lines
7.7 KiB
JavaScript
const { resolve } = require('node:path')
|
|
const { stripVTControlCharacters } = require('node:util')
|
|
const pacote = require('pacote')
|
|
const table = require('text-table')
|
|
const npa = require('npm-package-arg')
|
|
const pickManifest = require('npm-pick-manifest')
|
|
const { output } = require('proc-log')
|
|
const localeCompare = require('@isaacs/string-locale-compare')('en')
|
|
const ArboristWorkspaceCmd = require('../arborist-cmd.js')
|
|
|
|
const safeNpa = (spec) => {
|
|
try {
|
|
return npa(spec)
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
// This string is load bearing and is shared with Arborist
|
|
const MISSING = 'MISSING'
|
|
|
|
class Outdated extends ArboristWorkspaceCmd {
|
|
static description = 'Check for outdated packages'
|
|
static name = 'outdated'
|
|
static usage = ['[<package-spec> ...]']
|
|
static params = [
|
|
'all',
|
|
'json',
|
|
'long',
|
|
'parseable',
|
|
'global',
|
|
'workspace',
|
|
]
|
|
|
|
#tree
|
|
#list = []
|
|
#edges = new Set()
|
|
#filterSet
|
|
|
|
async exec (args) {
|
|
const Arborist = require('@npmcli/arborist')
|
|
const arb = new Arborist({
|
|
...this.npm.flatOptions,
|
|
path: this.npm.global ? resolve(this.npm.globalDir, '..') : this.npm.prefix,
|
|
})
|
|
this.#tree = await arb.loadActual()
|
|
|
|
if (this.workspaceNames?.length) {
|
|
this.#filterSet = arb.workspaceDependencySet(
|
|
this.#tree,
|
|
this.workspaceNames,
|
|
this.npm.flatOptions.includeWorkspaceRoot
|
|
)
|
|
} else if (!this.npm.flatOptions.workspacesEnabled) {
|
|
this.#filterSet = arb.excludeWorkspacesDependencySet(this.#tree)
|
|
}
|
|
|
|
if (args.length) {
|
|
for (const arg of args) {
|
|
// specific deps
|
|
this.#getEdges(this.#tree.inventory.query('name', arg), 'edgesIn')
|
|
}
|
|
} else {
|
|
if (this.npm.config.get('all')) {
|
|
// all deps in tree
|
|
this.#getEdges(this.#tree.inventory.values(), 'edgesOut')
|
|
}
|
|
// top-level deps
|
|
this.#getEdges()
|
|
}
|
|
|
|
await Promise.all([...this.#edges].map((e) => this.#getOutdatedInfo(e)))
|
|
|
|
// sorts list alphabetically by name and then dependent
|
|
const outdated = this.#list
|
|
.sort((a, b) => localeCompare(a.name, b.name) || localeCompare(a.dependent, b.dependent))
|
|
|
|
if (outdated.length) {
|
|
process.exitCode = 1
|
|
}
|
|
|
|
if (this.npm.config.get('json')) {
|
|
output.buffer(this.#json(outdated))
|
|
return
|
|
}
|
|
|
|
const res = this.npm.config.get('parseable')
|
|
? this.#parseable(outdated)
|
|
: this.#pretty(outdated)
|
|
|
|
if (res) {
|
|
output.standard(res)
|
|
}
|
|
}
|
|
|
|
#getEdges (nodes, type) {
|
|
// when no nodes are provided then it should only read direct deps
|
|
// from the root node and its workspaces direct dependencies
|
|
if (!nodes) {
|
|
this.#getEdgesOut(this.#tree)
|
|
this.#getWorkspacesEdges()
|
|
return
|
|
}
|
|
|
|
for (const node of nodes) {
|
|
if (type === 'edgesOut') {
|
|
this.#getEdgesOut(node)
|
|
} else {
|
|
this.#getEdgesIn(node)
|
|
}
|
|
}
|
|
}
|
|
|
|
#getEdgesIn (node) {
|
|
for (const edge of node.edgesIn) {
|
|
this.#trackEdge(edge)
|
|
}
|
|
}
|
|
|
|
#getEdgesOut (node) {
|
|
// TODO: normalize usage of edges and avoid looping through nodes here
|
|
const edges = this.npm.global ? node.children.values() : node.edgesOut.values()
|
|
for (const edge of edges) {
|
|
this.#trackEdge(edge)
|
|
}
|
|
}
|
|
|
|
#trackEdge (edge) {
|
|
if (edge.from && this.#filterSet?.size > 0 && !this.#filterSet.has(edge.from.target)) {
|
|
return
|
|
}
|
|
this.#edges.add(edge)
|
|
}
|
|
|
|
#getWorkspacesEdges () {
|
|
if (this.npm.global) {
|
|
return
|
|
}
|
|
|
|
for (const edge of this.#tree.edgesOut.values()) {
|
|
if (edge?.to?.target?.isWorkspace) {
|
|
this.#getEdgesOut(edge.to.target)
|
|
}
|
|
}
|
|
}
|
|
|
|
async #getPackument (spec) {
|
|
return pacote.packument(spec, {
|
|
...this.npm.flatOptions,
|
|
fullMetadata: this.npm.config.get('long'),
|
|
preferOnline: true,
|
|
})
|
|
}
|
|
|
|
async #getOutdatedInfo (edge) {
|
|
const alias = safeNpa(edge.spec)?.subSpec?.name
|
|
const spec = npa(alias ?? edge.name)
|
|
const node = edge.to || edge
|
|
const { path, location, package: { version: current } = {} } = node
|
|
|
|
const type = edge.optional ? 'optionalDependencies'
|
|
: edge.peer ? 'peerDependencies'
|
|
: edge.dev ? 'devDependencies'
|
|
: 'dependencies'
|
|
|
|
for (const omitType of this.npm.flatOptions.omit) {
|
|
if (node[omitType]) {
|
|
return
|
|
}
|
|
}
|
|
|
|
// deps different from prod not currently
|
|
// on disk are not included in the output
|
|
if (edge.error === MISSING && type !== 'dependencies') {
|
|
return
|
|
}
|
|
|
|
// if it's not a range, version, or tag, skip it
|
|
if (!safeNpa(`${edge.name}@${edge.spec}`)?.registry) {
|
|
return null
|
|
}
|
|
|
|
try {
|
|
const packument = await this.#getPackument(spec)
|
|
const expected = alias ? alias.fetchSpec : edge.spec
|
|
const wanted = pickManifest(packument, expected, this.npm.flatOptions)
|
|
const latest = pickManifest(packument, '*', this.npm.flatOptions)
|
|
if (!current || current !== wanted.version || wanted.version !== latest.version) {
|
|
this.#list.push({
|
|
name: alias ? edge.spec.replace('npm', edge.name) : edge.name,
|
|
path,
|
|
type,
|
|
current,
|
|
location,
|
|
wanted: wanted.version,
|
|
latest: latest.version,
|
|
workspaceDependent: edge.from?.isWorkspace ? edge.from.pkgid : null,
|
|
dependent: edge.from?.name ?? 'global',
|
|
homepage: packument.homepage,
|
|
})
|
|
}
|
|
} catch (err) {
|
|
// silently catch and ignore ETARGET, E403 &
|
|
// E404 errors, deps are just skipped
|
|
if (!['ETARGET', 'E404', 'E404'].includes(err.code)) {
|
|
throw err
|
|
}
|
|
}
|
|
}
|
|
|
|
// formatting functions
|
|
|
|
#pretty (list) {
|
|
if (!list.length) {
|
|
return
|
|
}
|
|
|
|
const long = this.npm.config.get('long')
|
|
const { bold, yellow, red, cyan, blue } = this.npm.chalk
|
|
|
|
return table([
|
|
[
|
|
'Package',
|
|
'Current',
|
|
'Wanted',
|
|
'Latest',
|
|
'Location',
|
|
'Depended by',
|
|
...long ? ['Package Type', 'Homepage'] : [],
|
|
].map(h => bold.underline(h)),
|
|
...list.map((d) => [
|
|
d.current === d.wanted ? yellow(d.name) : red(d.name),
|
|
d.current ?? 'MISSING',
|
|
cyan(d.wanted),
|
|
blue(d.latest),
|
|
d.location ?? '-',
|
|
d.workspaceDependent ? blue(d.workspaceDependent) : d.dependent,
|
|
...long ? [d.type, blue(d.homepage ?? '')] : [],
|
|
]),
|
|
], {
|
|
align: ['l', 'r', 'r', 'r', 'l'],
|
|
stringLength: s => stripVTControlCharacters(s).length,
|
|
})
|
|
}
|
|
|
|
// --parseable creates output like this:
|
|
// <fullpath>:<name@wanted>:<name@installed>:<name@latest>:<dependedby>
|
|
#parseable (list) {
|
|
return list.map(d => [
|
|
d.path,
|
|
`${d.name}@${d.wanted}`,
|
|
d.current ? `${d.name}@${d.current}` : 'MISSING',
|
|
`${d.name}@${d.latest}`,
|
|
d.dependent,
|
|
...this.npm.config.get('long') ? [d.type, d.homepage] : [],
|
|
].join(':')).join('\n')
|
|
}
|
|
|
|
#json (list) {
|
|
// TODO(BREAKING_CHANGE): this should just return an array. It's a list and
|
|
// turing 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
|
|
return list.reduce((acc, d) => {
|
|
const dep = {
|
|
current: d.current,
|
|
wanted: d.wanted,
|
|
latest: d.latest,
|
|
dependent: d.dependent,
|
|
location: d.path,
|
|
...this.npm.config.get('long') ? { type: d.type, homepage: d.homepage } : {},
|
|
}
|
|
acc[d.name] = acc[d.name]
|
|
// If this item alread has an outdated dep then we turn it into an array
|
|
? (Array.isArray(acc[d.name]) ? acc[d.name] : [acc[d.name]]).concat(dep)
|
|
: dep
|
|
return acc
|
|
}, {})
|
|
}
|
|
}
|
|
|
|
module.exports = Outdated
|