376 lines
8.9 KiB
JavaScript
376 lines
8.9 KiB
JavaScript
|
const os = require('os')
|
||
|
const path = require('path')
|
||
|
const pacote = require('pacote')
|
||
|
const table = require('text-table')
|
||
|
const color = require('chalk')
|
||
|
const styles = require('ansistyles')
|
||
|
const npa = require('npm-package-arg')
|
||
|
const pickManifest = require('npm-pick-manifest')
|
||
|
const localeCompare = require('@isaacs/string-locale-compare')('en')
|
||
|
|
||
|
const Arborist = require('@npmcli/arborist')
|
||
|
|
||
|
const ansiTrim = require('../utils/ansi-trim.js')
|
||
|
const ArboristWorkspaceCmd = require('../arborist-cmd.js')
|
||
|
|
||
|
class Outdated extends ArboristWorkspaceCmd {
|
||
|
static description = 'Check for outdated packages'
|
||
|
static name = 'outdated'
|
||
|
static usage = ['[[<@scope>/]<pkg> ...]']
|
||
|
static params = [
|
||
|
'all',
|
||
|
'json',
|
||
|
'long',
|
||
|
'parseable',
|
||
|
'global',
|
||
|
'workspace',
|
||
|
]
|
||
|
|
||
|
async exec (args) {
|
||
|
const global = path.resolve(this.npm.globalDir, '..')
|
||
|
const where = this.npm.config.get('global')
|
||
|
? global
|
||
|
: this.npm.prefix
|
||
|
|
||
|
const arb = new Arborist({
|
||
|
...this.npm.flatOptions,
|
||
|
path: where,
|
||
|
})
|
||
|
|
||
|
this.edges = new Set()
|
||
|
this.list = []
|
||
|
this.tree = await arb.loadActual()
|
||
|
|
||
|
if (this.workspaceNames && 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 !== 0) {
|
||
|
// specific deps
|
||
|
for (let i = 0; i < args.length; i++) {
|
||
|
const nodes = this.tree.inventory.query('name', args[i])
|
||
|
this.getEdges(nodes, 'edgesIn')
|
||
|
}
|
||
|
} else {
|
||
|
if (this.npm.config.get('all')) {
|
||
|
// all deps in tree
|
||
|
const nodes = this.tree.inventory.values()
|
||
|
this.getEdges(nodes, 'edgesOut')
|
||
|
}
|
||
|
// top-level deps
|
||
|
this.getEdges()
|
||
|
}
|
||
|
|
||
|
await Promise.all(Array.from(this.edges).map((edge) => {
|
||
|
return this.getOutdatedInfo(edge)
|
||
|
}))
|
||
|
|
||
|
// sorts list alphabetically
|
||
|
const outdated = this.list.sort((a, b) => localeCompare(a.name, b.name))
|
||
|
|
||
|
if (outdated.length > 0) {
|
||
|
process.exitCode = 1
|
||
|
}
|
||
|
|
||
|
// return if no outdated packages
|
||
|
if (outdated.length === 0 && !this.npm.config.get('json')) {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// display results
|
||
|
if (this.npm.config.get('json')) {
|
||
|
this.npm.output(this.makeJSON(outdated))
|
||
|
} else if (this.npm.config.get('parseable')) {
|
||
|
this.npm.output(this.makeParseable(outdated))
|
||
|
} else {
|
||
|
const outList = outdated.map(x => this.makePretty(x))
|
||
|
const outHead = ['Package',
|
||
|
'Current',
|
||
|
'Wanted',
|
||
|
'Latest',
|
||
|
'Location',
|
||
|
'Depended by',
|
||
|
]
|
||
|
|
||
|
if (this.npm.config.get('long')) {
|
||
|
outHead.push('Package Type', 'Homepage')
|
||
|
}
|
||
|
const outTable = [outHead].concat(outList)
|
||
|
|
||
|
if (this.npm.color) {
|
||
|
outTable[0] = outTable[0].map(heading => styles.underline(heading))
|
||
|
}
|
||
|
|
||
|
const tableOpts = {
|
||
|
align: ['l', 'r', 'r', 'r', 'l'],
|
||
|
stringLength: s => ansiTrim(s).length,
|
||
|
}
|
||
|
this.npm.output(table(outTable, tableOpts))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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) {
|
||
|
type === 'edgesOut'
|
||
|
? this.getEdgesOut(node)
|
||
|
: 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
|
||
|
if (this.npm.config.get('global')) {
|
||
|
for (const child of node.children.values()) {
|
||
|
this.trackEdge(child)
|
||
|
}
|
||
|
} else {
|
||
|
for (const edge of node.edgesOut.values()) {
|
||
|
this.trackEdge(edge)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
trackEdge (edge) {
|
||
|
const filteredOut =
|
||
|
edge.from
|
||
|
&& this.filterSet
|
||
|
&& this.filterSet.size > 0
|
||
|
&& !this.filterSet.has(edge.from.target)
|
||
|
|
||
|
if (filteredOut) {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
this.edges.add(edge)
|
||
|
}
|
||
|
|
||
|
getWorkspacesEdges (node) {
|
||
|
if (this.npm.config.get('global')) {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
for (const edge of this.tree.edgesOut.values()) {
|
||
|
const workspace = edge
|
||
|
&& edge.to
|
||
|
&& edge.to.target
|
||
|
&& edge.to.target.isWorkspace
|
||
|
|
||
|
if (workspace) {
|
||
|
this.getEdgesOut(edge.to.target)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
async getPackument (spec) {
|
||
|
const packument = await pacote.packument(spec, {
|
||
|
...this.npm.flatOptions,
|
||
|
fullMetadata: this.npm.config.get('long'),
|
||
|
preferOnline: true,
|
||
|
})
|
||
|
return packument
|
||
|
}
|
||
|
|
||
|
async getOutdatedInfo (edge) {
|
||
|
let alias = false
|
||
|
try {
|
||
|
alias = npa(edge.spec).subSpec
|
||
|
} catch (err) {
|
||
|
}
|
||
|
const spec = npa(alias ? alias.name : edge.name)
|
||
|
const node = edge.to || edge
|
||
|
const { path, location } = node
|
||
|
const { version: current } = node.package || {}
|
||
|
|
||
|
const type = edge.optional ? 'optionalDependencies'
|
||
|
: edge.peer ? 'peerDependencies'
|
||
|
: edge.dev ? 'devDependencies'
|
||
|
: 'dependencies'
|
||
|
|
||
|
for (const omitType of this.npm.config.get('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
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
const packument = await this.getPackument(spec)
|
||
|
const expected = alias ? alias.fetchSpec : edge.spec
|
||
|
// if it's not a range, version, or tag, skip it
|
||
|
try {
|
||
|
if (!npa(`${edge.name}@${edge.spec}`).registry) {
|
||
|
return null
|
||
|
}
|
||
|
} catch (err) {
|
||
|
return null
|
||
|
}
|
||
|
const wanted = pickManifest(packument, expected, this.npm.flatOptions)
|
||
|
const latest = pickManifest(packument, '*', this.npm.flatOptions)
|
||
|
|
||
|
if (
|
||
|
!current ||
|
||
|
current !== wanted.version ||
|
||
|
wanted.version !== latest.version
|
||
|
) {
|
||
|
const dependent = edge.from ?
|
||
|
this.maybeWorkspaceName(edge.from)
|
||
|
: 'global'
|
||
|
|
||
|
this.list.push({
|
||
|
name: alias ? edge.spec.replace('npm', edge.name) : edge.name,
|
||
|
path,
|
||
|
type,
|
||
|
current,
|
||
|
location,
|
||
|
wanted: wanted.version,
|
||
|
latest: latest.version,
|
||
|
dependent,
|
||
|
homepage: packument.homepage,
|
||
|
})
|
||
|
}
|
||
|
} catch (err) {
|
||
|
// silently catch and ignore ETARGET, E403 &
|
||
|
// E404 errors, deps are just skipped
|
||
|
if (!(
|
||
|
err.code === 'ETARGET' ||
|
||
|
err.code === 'E403' ||
|
||
|
err.code === 'E404')
|
||
|
) {
|
||
|
throw err
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
maybeWorkspaceName (node) {
|
||
|
if (!node.isWorkspace) {
|
||
|
return node.name
|
||
|
}
|
||
|
|
||
|
const humanOutput =
|
||
|
!this.npm.config.get('json') && !this.npm.config.get('parseable')
|
||
|
|
||
|
const workspaceName =
|
||
|
humanOutput
|
||
|
? node.pkgid
|
||
|
: node.name
|
||
|
|
||
|
return this.npm.color && humanOutput
|
||
|
? color.green(workspaceName)
|
||
|
: workspaceName
|
||
|
}
|
||
|
|
||
|
// formatting functions
|
||
|
makePretty (dep) {
|
||
|
const {
|
||
|
current = 'MISSING',
|
||
|
location = '-',
|
||
|
homepage = '',
|
||
|
name,
|
||
|
wanted,
|
||
|
latest,
|
||
|
type,
|
||
|
dependent,
|
||
|
} = dep
|
||
|
|
||
|
const columns = [name, current, wanted, latest, location, dependent]
|
||
|
|
||
|
if (this.npm.config.get('long')) {
|
||
|
columns[6] = type
|
||
|
columns[7] = homepage
|
||
|
}
|
||
|
|
||
|
if (this.npm.color) {
|
||
|
columns[0] = color[current === wanted ? 'yellow' : 'red'](columns[0]) // current
|
||
|
columns[2] = color.green(columns[2]) // wanted
|
||
|
columns[3] = color.magenta(columns[3]) // latest
|
||
|
}
|
||
|
|
||
|
return columns
|
||
|
}
|
||
|
|
||
|
// --parseable creates output like this:
|
||
|
// <fullpath>:<name@wanted>:<name@installed>:<name@latest>:<dependedby>
|
||
|
makeParseable (list) {
|
||
|
return list.map(dep => {
|
||
|
const {
|
||
|
name,
|
||
|
current,
|
||
|
wanted,
|
||
|
latest,
|
||
|
path,
|
||
|
dependent,
|
||
|
type,
|
||
|
homepage,
|
||
|
} = dep
|
||
|
const out = [
|
||
|
path,
|
||
|
name + '@' + wanted,
|
||
|
current ? (name + '@' + current) : 'MISSING',
|
||
|
name + '@' + latest,
|
||
|
dependent,
|
||
|
]
|
||
|
if (this.npm.config.get('long')) {
|
||
|
out.push(type, homepage)
|
||
|
}
|
||
|
|
||
|
return out.join(':')
|
||
|
}).join(os.EOL)
|
||
|
}
|
||
|
|
||
|
makeJSON (list) {
|
||
|
const out = {}
|
||
|
list.forEach(dep => {
|
||
|
const {
|
||
|
name,
|
||
|
current,
|
||
|
wanted,
|
||
|
latest,
|
||
|
path,
|
||
|
type,
|
||
|
dependent,
|
||
|
homepage,
|
||
|
} = dep
|
||
|
out[name] = {
|
||
|
current,
|
||
|
wanted,
|
||
|
latest,
|
||
|
dependent,
|
||
|
location: path,
|
||
|
}
|
||
|
if (this.npm.config.get('long')) {
|
||
|
out[name].type = type
|
||
|
out[name].homepage = homepage
|
||
|
}
|
||
|
})
|
||
|
return JSON.stringify(out, null, 2)
|
||
|
}
|
||
|
}
|
||
|
module.exports = Outdated
|