diff options
Diffstat (limited to 'deps/npm/lib/outdated.js')
-rw-r--r-- | deps/npm/lib/outdated.js | 628 |
1 files changed, 226 insertions, 402 deletions
diff --git a/deps/npm/lib/outdated.js b/deps/npm/lib/outdated.js index 5b84ae3558..c3b15e4118 100644 --- a/deps/npm/lib/outdated.js +++ b/deps/npm/lib/outdated.js @@ -1,454 +1,278 @@ -/* - -npm outdated [pkg] - -Does the following: - -1. check for a new version of pkg - -If no packages are specified, then run for all installed -packages. - ---parseable creates output like this: -<fullpath>:<name@wanted>:<name@installed>:<name@latest> - -*/ - -module.exports = outdated - -outdated.usage = 'npm outdated [[<@scope>/]<pkg> ...]' - -outdated.completion = require('./utils/completion/installed-deep.js') +'use strict' const os = require('os') -const url = require('url') const path = require('path') -const readPackageTree = require('read-package-tree') -const asyncMap = require('slide').asyncMap +const pacote = require('pacote') +const table = require('text-table') const color = require('ansicolors') const styles = require('ansistyles') -const table = require('text-table') -const semver = require('semver') -const npa = require('libnpm/parse-arg') +const npa = require('npm-package-arg') const pickManifest = require('npm-pick-manifest') -const fetchPackageMetadata = require('./fetch-package-metadata.js') -const mutateIntoLogicalTree = require('./install/mutate-into-logical-tree.js') + +const Arborist = require('@npmcli/arborist') + const npm = require('./npm.js') -const npmConfig = require('./config/figgy-config.js') -const figgyPudding = require('figgy-pudding') -const packument = require('libnpm/packument') -const long = npm.config.get('long') -const isExtraneous = require('./install/is-extraneous.js') -const computeMetadata = require('./install/deps.js').computeMetadata -const computeVersionSpec = require('./install/deps.js').computeVersionSpec -const moduleName = require('./utils/module-name.js') const output = require('./utils/output.js') -const ansiTrim = require('./utils/ansi-trim') - -const OutdatedConfig = figgyPudding({ - also: {}, - color: {}, - depth: {}, - dev: 'development', - development: {}, - global: {}, - json: {}, - only: {}, - parseable: {}, - prod: 'production', - production: {}, - save: {}, - 'save-dev': {}, - 'save-optional': {} -}) - -function uniq (list) { - // we maintain the array because we need an array, not iterator, return - // value. - var uniqed = [] - var seen = new Set() - list.forEach(function (item) { - if (seen.has(item)) return - seen.add(item) - uniqed.push(item) - }) - return uniqed +const usageUtil = require('./utils/usage.js') +const ansiTrim = require('./utils/ansi-trim.js') + +const usage = usageUtil('outdated', + 'npm outdated [[<@scope>/]<pkg> ...]' +) +const completion = require('./utils/completion/none.js') + +function cmd (args, cb) { + outdated(args) + .then(() => cb()) + .catch(cb) } -function andComputeMetadata (next) { - return function (er, tree) { - if (er) return next(er) - next(null, computeMetadata(tree)) - } -} +async function outdated (args) { + const opts = npm.flatOptions + const global = path.resolve(npm.globalDir, '..') + const where = opts.global + ? global + : npm.prefix -function outdated (args, silent, cb) { - if (typeof cb !== 'function') { - cb = silent - silent = false - } - let opts = OutdatedConfig(npmConfig()) - var dir = path.resolve(npm.dir, '..') - - // default depth for `outdated` is 0 (cf. `ls`) - if (opts.depth === Infinity) opts = opts.concat({depth: 0}) - - readPackageTree(dir, andComputeMetadata(function (er, tree) { - if (!tree) return cb(er) - mutateIntoLogicalTree(tree) - outdated_(args, '', tree, {}, 0, opts, function (er, list) { - list = uniq(list || []).sort(function (aa, bb) { - return aa[0].path.localeCompare(bb[0].path) || - aa[1].localeCompare(bb[1]) - }) - if (er || silent || - (list.length === 0 && !opts.json)) { - return cb(er, list) - } - if (opts.json) { - output(makeJSON(list, opts)) - } else if (opts.parseable) { - output(makeParseable(list, opts)) - } else { - var outList = list.map(x => makePretty(x, opts)) - var outHead = [ 'Package', - 'Current', - 'Wanted', - 'Latest', - 'Location' - ] - if (long) outHead.push('Package Type', 'Homepage') - var outTable = [outHead].concat(outList) - - if (opts.color) { - outTable[0] = outTable[0].map(function (heading) { - return styles.underline(heading) - }) - } + const arb = new Arborist({ + ...opts, + path: where + }) - var tableOpts = { - align: ['l', 'r', 'r', 'r', 'l'], - stringLength: function (s) { return ansiTrim(s).length } - } - output(table(outTable, tableOpts)) - } - process.exitCode = list.length ? 1 : 0 - cb(null, list.map(function (item) { return [item[0].parent.path].concat(item.slice(1, 7)) })) - }) - })) -} + const tree = await arb.loadActual() + const list = await outdated_(tree, args, opts) -// [[ dir, dep, has, want, latest, type ]] -function makePretty (p, opts) { - var depname = p[1] - var has = p[2] - var want = p[3] - var latest = p[4] - var type = p[6] - var deppath = p[7] - var homepage = p[0].package.homepage || '' - - var columns = [ depname, - has || 'MISSING', - want, - latest, - deppath || 'global' - ] - if (long) { - columns[5] = type - columns[6] = homepage - } + // sorts list alphabetically + const outdated = list.sort((a, b) => a.name.localeCompare(b.name)) - if (opts.color) { - columns[0] = color[has === want ? 'yellow' : 'red'](columns[0]) // dep - columns[2] = color.green(columns[2]) // want - columns[3] = color.magenta(columns[3]) // latest + // return if no outdated packages + if (outdated.length === 0 && !opts.json) { + return } - return columns -} - -function makeParseable (list) { - return list.map(function (p) { - var dep = p[0] - var depname = p[1] - var dir = dep.path - var has = p[2] - var want = p[3] - var latest = p[4] - var type = p[6] - - var out = [ - dir, - depname + '@' + want, - (has ? (depname + '@' + has) : 'MISSING'), - depname + '@' + latest + // display results + if (opts.json) { + output(makeJSON(outdated, opts)) + } else if (opts.parseable) { + output(makeParseable(outdated, opts)) + } else { + const outList = outdated.map(x => makePretty(x, opts)) + const outHead = ['Package', + 'Current', + 'Wanted', + 'Latest', + 'Location', + 'Depended by' ] - if (long) out.push(type, dep.package.homepage) - return out.join(':') - }).join(os.EOL) -} + if (opts.long) outHead.push('Package Type', 'Homepage') + const outTable = [outHead].concat(outList) -function makeJSON (list, opts) { - var out = {} - list.forEach(function (p) { - var dep = p[0] - var depname = p[1] - var dir = dep.path - var has = p[2] - var want = p[3] - var latest = p[4] - var type = p[6] - if (!opts.global) { - dir = path.relative(process.cwd(), dir) - } - out[depname] = { current: has, - wanted: want, - latest: latest, - location: dir - } - if (long) { - out[depname].type = type - out[depname].homepage = dep.package.homepage + if (opts.color) { + outTable[0] = outTable[0].map(heading => styles.underline(heading)) } - }) - return JSON.stringify(out, null, 2) -} -function outdated_ (args, path, tree, parentHas, depth, opts, cb) { - if (!tree.package) tree.package = {} - if (path && moduleName(tree)) path += ' > ' + tree.package.name - if (!path && moduleName(tree)) path = tree.package.name - if (depth > opts.depth) { - return cb(null, []) + const tableOpts = { + align: ['l', 'r', 'r', 'r', 'l'], + stringLength: s => ansiTrim(s).length + } + output(table(outTable, tableOpts)) } - var types = {} - var pkg = tree.package - - if (!tree.children) tree.children = [] - - var deps = tree.error ? tree.children : tree.children.filter((child) => !isExtraneous(child)) - - deps.forEach(function (dep) { - types[moduleName(dep)] = 'dependencies' - }) +} - Object.keys(tree.missingDeps || {}).forEach(function (name) { - deps.push({ - package: { name: name }, - path: tree.path, - parent: tree, - isMissing: true - }) - types[name] = 'dependencies' - }) +async function outdated_ (tree, deps, opts) { + const list = [] - // If we explicitly asked for dev deps OR we didn't ask for production deps - // AND we asked to save dev-deps OR we didn't ask to save anything that's NOT - // dev deps then… - // (All the save checking here is because this gets called from npm-update currently - // and that requires this logic around dev deps.) - // FIXME: Refactor npm update to not be in terms of outdated. - var dev = opts.dev || /^dev(elopment)?$/.test(opts.also) - var prod = opts.production || /^prod(uction)?$/.test(opts.only) - if ( - (dev || !prod) && - ( - opts['save-dev'] || (!opts.save && !opts['save-optional']) - ) - ) { - Object.keys(tree.missingDevDeps).forEach(function (name) { - deps.push({ - package: { name: name }, - path: tree.path, - parent: tree, - isMissing: true - }) - if (!types[name]) { - types[name] = 'devDependencies' + const edges = new Set() + function getEdges (nodes, type) { + const getEdgesIn = (node) => { + for (const edge of node.edgesIn) { + edges.add(edge) } - }) - } + } - if (opts['save-dev']) { - deps = deps.filter(function (dep) { return pkg.devDependencies[moduleName(dep)] }) - deps.forEach(function (dep) { - types[moduleName(dep)] = 'devDependencies' - }) - } else if (opts.save) { - // remove optional dependencies from dependencies during --save. - deps = deps.filter(function (dep) { return !pkg.optionalDependencies[moduleName(dep)] }) - } else if (opts['save-optional']) { - deps = deps.filter(function (dep) { return pkg.optionalDependencies[moduleName(dep)] }) - deps.forEach(function (dep) { - types[moduleName(dep)] = 'optionalDependencies' - }) - } - var doUpdate = dev || ( - !prod && - !Object.keys(parentHas).length && - !opts.global - ) - if (doUpdate) { - Object.keys(pkg.devDependencies || {}).forEach(function (k) { - if (!(k in parentHas)) { - deps[k] = pkg.devDependencies[k] - types[k] = 'devDependencies' + const getEdgesOut = (node) => { + if (opts.global) { + for (const child of node.children.values()) { + edges.add(child) + } + } else { + for (const edge of node.edgesOut.values()) { + edges.add(edge) + } } - }) - } - - var has = Object.create(parentHas) - tree.children.forEach(function (child) { - if (moduleName(child) && child.package.private) { - deps = deps.filter(function (dep) { return dep !== child }) - } - has[moduleName(child)] = { - version: child.isLink ? 'linked' : child.package.version, - from: child.isLink ? 'file:' + child.path : child.package._from } - }) - // now get what we should have, based on the dep. - // if has[dep] !== shouldHave[dep], then cb with the data - // otherwise dive into the folder - asyncMap(deps, function (dep, cb) { - var name = moduleName(dep) - var required - if (tree.package.dependencies && name in tree.package.dependencies) { - required = tree.package.dependencies[name] - } else if (tree.package.optionalDependencies && name in tree.package.optionalDependencies) { - required = tree.package.optionalDependencies[name] - } else if (tree.package.devDependencies && name in tree.package.devDependencies) { - required = tree.package.devDependencies[name] - } else if (has[name]) { - required = computeVersionSpec(tree, dep) + if (!nodes) return getEdgesOut(tree) + for (const node of nodes) { + type === 'edgesOut' + ? getEdgesOut(node) + : getEdgesIn(node) } + } - if (!long) return shouldUpdate(args, dep, name, has, required, depth, path, opts, cb) - - shouldUpdate(args, dep, name, has, required, depth, path, opts, cb, types[name]) - }, cb) -} - -function shouldUpdate (args, tree, dep, has, req, depth, pkgpath, opts, cb, type) { - // look up the most recent version. - // if that's what we already have, or if it's not on the args list, - // then dive into it. Otherwise, cb() with the data. - - // { version: , from: } - var curr = has[dep] - - function skip (er) { - // show user that no viable version can be found - if (er) return cb(er) - outdated_(args, - pkgpath, - tree, - has, - depth + 1, - opts, - cb) + async function getPackument (spec) { + const packument = await pacote.packument(spec, { + fullMetadata: npm.flatOptions.long, + preferOnline: true + }) + return packument } - if (args.length && args.indexOf(dep) === -1) return skip() + async function getOutdatedInfo (edge) { + const spec = npa(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' - if (tree.isLink && req == null) return skip() + for (const omitType of opts.omit || []) { + if (node[omitType]) { + return + } + } - if (req == null || req === '') req = '*' + // deps different from prod not currently + // on disk are not included in the output + if (edge.error === 'MISSING' && type !== 'dependencies') return - var parsed = npa.resolve(dep, req) - if (parsed.type === 'directory') { - if (tree.isLink) { - return skip() - } else { - return doIt('linked', 'linked') + try { + const packument = await getPackument(spec) + const expected = 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, npm.flatOptions) + const latest = pickManifest(packument, '*', npm.flatOptions) + + if ( + !current || + current !== wanted.version || + wanted.version !== latest.version + ) { + list.push({ + name: edge.name, + path, + type, + current, + location, + wanted: wanted.version, + latest: latest.version, + dependent: edge.from ? edge.from.name : 'global', + 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 + } } - } else if (parsed.type === 'git') { - return doIt('git', 'git') - } else if (parsed.type === 'file') { - return updateLocalDeps() - } else if (parsed.type === 'remote') { - return doIt('remote', 'remote') - } else { - return packument(parsed, opts.concat({ - 'prefer-online': true - })).nodeify(updateDeps) } - function doIt (wanted, latest) { - let c = curr && curr.version - if (parsed.type === 'alias') { - c = `npm:${parsed.subSpec.name}@${c}` + const p = [] + if (deps.length !== 0) { + // specific deps + for (let i = 0; i < deps.length; i++) { + const nodes = tree.inventory.query('name', deps[i]) + getEdges(nodes, 'edgesIn') } - if (!long) { - return cb(null, [[tree, dep, c, wanted, latest, req, null, pkgpath]]) + } else { + if (opts.all) { + // all deps in tree + const nodes = tree.inventory.values() + getEdges(nodes, 'edgesOut') } - cb(null, [[tree, dep, c, wanted, latest, req, type, pkgpath]]) + // top-level deps + getEdges() } - function updateLocalDeps (latestRegistryVersion) { - fetchPackageMetadata('file:' + parsed.fetchSpec, '.', (er, localDependency) => { - if (er) return cb() + for (const edge of edges) { + p.push(getOutdatedInfo(edge)) + } - var wanted = localDependency.version - var latest = localDependency.version + await Promise.all(p) + return list +} - if (latestRegistryVersion) { - latest = latestRegistryVersion - if (semver.lt(wanted, latestRegistryVersion)) { - wanted = latestRegistryVersion - req = dep + '@' + latest - } - } +// formatting functions +function makePretty (dep, opts) { + const { + current = 'MISSING', + location = '-', + homepage = '', + name, + wanted, + latest, + type, + dependent + } = dep - if (!curr || curr.version !== wanted) { - doIt(wanted, latest) - } else { - skip() - } - }) + const columns = [name, current, wanted, latest, location, dependent] + + if (opts.long) { + columns[6] = type + columns[7] = homepage } - function updateDeps (er, d) { - if (er) return cb(er) + if (opts.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 + } - if (parsed.type === 'alias') { - req = parsed.subSpec.rawSpec - } - try { - var l = pickManifest(d, 'latest') - var m = pickManifest(d, req) - } catch (er) { - if (er.code === 'ETARGET' || er.code === 'E403') { - return skip(er) - } else { - return skip() - } - } + return columns +} - // check that the url origin hasn't changed (#1727) and that - // there is no newer version available - var dFromUrl = m._from && url.parse(m._from).protocol - var cFromUrl = curr && curr.from && url.parse(curr.from).protocol - - if ( - !curr || - (dFromUrl && cFromUrl && m._from !== curr.from) || - m.version !== curr.version || - m.version !== l.version - ) { - if (parsed.type === 'alias') { - doIt( - `npm:${parsed.subSpec.name}@${m.version}`, - `npm:${parsed.subSpec.name}@${l.version}` - ) - } else { - doIt(m.version, l.version) - } - } else { - skip() +// --parseable creates output like this: +// <fullpath>:<name@wanted>:<name@installed>:<name@latest>:<dependedby> +function makeParseable (list, opts) { + 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 (opts.long) out.push(type, homepage) + + return out.join(':') + }).join(os.EOL) +} + +function makeJSON (list, opts) { + const out = {} + list.forEach(dep => { + const { name, current, wanted, latest, path, type, dependent, homepage } = dep + out[name] = { + current, + wanted, + latest, + dependent, + location: path } - } + if (opts.long) { + out[name].type = type + out[name].homepage = homepage + } + }) + return JSON.stringify(out, null, 2) } + +module.exports = Object.assign(cmd, { completion, usage }) |