const os = require('os')
const path = require('path')
const pacote = require('pacote')
const table = require('text-table')
const chalk = require('chalk')
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 = ['[<package-spec> ...]']
static params = [
'all',
'json',
'long',
'parseable',
'global',
'workspace',
]
async exec (args) {
const global = path.resolve(this.npm.globalDir, '..')
const where = this.npm.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 => chalk.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.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.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) {
// ignore errors, no alias
}
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.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
}
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
? chalk.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] = chalk[current === wanted ? 'yellow' : 'red'](columns[0]) // current
columns[2] = chalk.green(columns[2]) // wanted
columns[3] = chalk.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
| Name | Type | Size | Permission | Actions |
|---|---|---|---|---|
| access.js | File | 5.45 KB | 0644 |
|
| adduser.js | File | 2.2 KB | 0644 |
|
| audit.js | File | 11.95 KB | 0644 |
|
| bin.js | File | 729 B | 0644 |
|
| birthday.js | File | 508 B | 0644 |
|
| bugs.js | File | 815 B | 0644 |
|
| cache.js | File | 7.08 KB | 0644 |
|
| ci.js | File | 3.63 KB | 0644 |
|
| completion.js | File | 8.91 KB | 0644 |
|
| config.js | File | 8.11 KB | 0644 |
|
| dedupe.js | File | 1.37 KB | 0644 |
|
| deprecate.js | File | 2.06 KB | 0644 |
|
| diff.js | File | 8.1 KB | 0644 |
|
| dist-tag.js | File | 5.47 KB | 0644 |
|
| docs.js | File | 447 B | 0644 |
|
| doctor.js | File | 9.22 KB | 0644 |
|
| edit.js | File | 2 KB | 0644 |
|
| exec.js | File | 2.44 KB | 0644 |
|
| explain.js | File | 3.55 KB | 0644 |
|
| explore.js | File | 2.33 KB | 0644 |
|
| find-dupes.js | File | 602 B | 0644 |
|
| fund.js | File | 6.37 KB | 0644 |
|
| get.js | File | 524 B | 0644 |
|
| help-search.js | File | 5.62 KB | 0644 |
|
| help.js | File | 4.53 KB | 0644 |
|
| hook.js | File | 3.93 KB | 0644 |
|
| init.js | File | 6.81 KB | 0644 |
|
| install-ci-test.js | File | 377 B | 0644 |
|
| install-test.js | File | 374 B | 0644 |
|
| install.js | File | 5.11 KB | 0644 |
|
| link.js | File | 5.02 KB | 0644 |
|
| ll.js | File | 234 B | 0644 |
|
| logout.js | File | 1.34 KB | 0644 |
|
| ls.js | File | 16.94 KB | 0644 |
|
| org.js | File | 4.2 KB | 0644 |
|
| outdated.js | File | 8.84 KB | 0644 |
|
| owner.js | File | 5.88 KB | 0644 |
|
| pack.js | File | 2.36 KB | 0644 |
|
| ping.js | File | 874 B | 0644 |
|
| pkg.js | File | 3.47 KB | 0644 |
|
| prefix.js | File | 343 B | 0644 |
|
| profile.js | File | 11.25 KB | 0644 |
|
| prune.js | File | 779 B | 0644 |
|
| publish.js | File | 6.33 KB | 0644 |
|
| query.js | File | 2.81 KB | 0644 |
|
| rebuild.js | File | 2.16 KB | 0644 |
|
| repo.js | File | 1.24 KB | 0644 |
|
| restart.js | File | 351 B | 0644 |
|
| root.js | File | 298 B | 0644 |
|
| run-script.js | File | 6.9 KB | 0644 |
|
| search.js | File | 2.72 KB | 0644 |
|
| set-script.js | File | 2.63 KB | 0644 |
|
| set.js | File | 572 B | 0644 |
|
| shrinkwrap.js | File | 2.64 KB | 0644 |
|
| star.js | File | 1.87 KB | 0644 |
|
| stars.js | File | 1.03 KB | 0644 |
|
| start.js | File | 341 B | 0644 |
|
| stop.js | File | 336 B | 0644 |
|
| team.js | File | 4.44 KB | 0644 |
|
| test.js | File | 336 B | 0644 |
|
| token.js | File | 6.79 KB | 0644 |
|
| uninstall.js | File | 1.52 KB | 0644 |
|
| unpublish.js | File | 4.51 KB | 0644 |
|
| unstar.js | File | 182 B | 0644 |
|
| update.js | File | 1.7 KB | 0644 |
|
| version.js | File | 3.6 KB | 0644 |
|
| view.js | File | 14.38 KB | 0644 |
|
| whoami.js | File | 514 B | 0644 |
|