• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2
3npm outdated [pkg]
4
5Does the following:
6
71. check for a new version of pkg
8
9If no packages are specified, then run for all installed
10packages.
11
12--parseable creates output like this:
13<fullpath>:<name@wanted>:<name@installed>:<name@latest>
14
15*/
16
17module.exports = outdated
18
19outdated.usage = 'npm outdated [[<@scope>/]<pkg> ...]'
20
21outdated.completion = require('./utils/completion/installed-deep.js')
22
23const os = require('os')
24const url = require('url')
25const path = require('path')
26const readPackageTree = require('read-package-tree')
27const asyncMap = require('slide').asyncMap
28const color = require('ansicolors')
29const styles = require('ansistyles')
30const table = require('text-table')
31const semver = require('semver')
32const npa = require('libnpm/parse-arg')
33const pickManifest = require('npm-pick-manifest')
34const fetchPackageMetadata = require('./fetch-package-metadata.js')
35const mutateIntoLogicalTree = require('./install/mutate-into-logical-tree.js')
36const npm = require('./npm.js')
37const npmConfig = require('./config/figgy-config.js')
38const figgyPudding = require('figgy-pudding')
39const packument = require('libnpm/packument')
40const long = npm.config.get('long')
41const isExtraneous = require('./install/is-extraneous.js')
42const computeMetadata = require('./install/deps.js').computeMetadata
43const computeVersionSpec = require('./install/deps.js').computeVersionSpec
44const moduleName = require('./utils/module-name.js')
45const output = require('./utils/output.js')
46const ansiTrim = require('./utils/ansi-trim')
47
48const OutdatedConfig = figgyPudding({
49  also: {},
50  color: {},
51  depth: {},
52  dev: 'development',
53  development: {},
54  global: {},
55  json: {},
56  only: {},
57  parseable: {},
58  prod: 'production',
59  production: {},
60  save: {},
61  'save-dev': {},
62  'save-optional': {}
63})
64
65function uniq (list) {
66  // we maintain the array because we need an array, not iterator, return
67  // value.
68  var uniqed = []
69  var seen = new Set()
70  list.forEach(function (item) {
71    if (seen.has(item)) return
72    seen.add(item)
73    uniqed.push(item)
74  })
75  return uniqed
76}
77
78function andComputeMetadata (next) {
79  return function (er, tree) {
80    if (er) return next(er)
81    next(null, computeMetadata(tree))
82  }
83}
84
85function outdated (args, silent, cb) {
86  if (typeof cb !== 'function') {
87    cb = silent
88    silent = false
89  }
90  let opts = OutdatedConfig(npmConfig())
91  var dir = path.resolve(npm.dir, '..')
92
93  // default depth for `outdated` is 0 (cf. `ls`)
94  if (opts.depth === Infinity) opts = opts.concat({depth: 0})
95
96  readPackageTree(dir, andComputeMetadata(function (er, tree) {
97    if (!tree) return cb(er)
98    mutateIntoLogicalTree(tree)
99    outdated_(args, '', tree, {}, 0, opts, function (er, list) {
100      list = uniq(list || []).sort(function (aa, bb) {
101        return aa[0].path.localeCompare(bb[0].path) ||
102          aa[1].localeCompare(bb[1])
103      })
104      if (er || silent ||
105          (list.length === 0 && !opts.json)) {
106        return cb(er, list)
107      }
108      if (opts.json) {
109        output(makeJSON(list, opts))
110      } else if (opts.parseable) {
111        output(makeParseable(list, opts))
112      } else {
113        var outList = list.map(x => makePretty(x, opts))
114        var outHead = [ 'Package',
115          'Current',
116          'Wanted',
117          'Latest',
118          'Location'
119        ]
120        if (long) outHead.push('Package Type', 'Homepage')
121        var outTable = [outHead].concat(outList)
122
123        if (opts.color) {
124          outTable[0] = outTable[0].map(function (heading) {
125            return styles.underline(heading)
126          })
127        }
128
129        var tableOpts = {
130          align: ['l', 'r', 'r', 'r', 'l'],
131          stringLength: function (s) { return ansiTrim(s).length }
132        }
133        output(table(outTable, tableOpts))
134      }
135      process.exitCode = list.length ? 1 : 0
136      cb(null, list.map(function (item) { return [item[0].parent.path].concat(item.slice(1, 7)) }))
137    })
138  }))
139}
140
141// [[ dir, dep, has, want, latest, type ]]
142function makePretty (p, opts) {
143  var depname = p[1]
144  var has = p[2]
145  var want = p[3]
146  var latest = p[4]
147  var type = p[6]
148  var deppath = p[7]
149  var homepage = p[0].package.homepage || ''
150
151  var columns = [ depname,
152    has || 'MISSING',
153    want,
154    latest,
155    deppath || 'global'
156  ]
157  if (long) {
158    columns[5] = type
159    columns[6] = homepage
160  }
161
162  if (opts.color) {
163    columns[0] = color[has === want ? 'yellow' : 'red'](columns[0]) // dep
164    columns[2] = color.green(columns[2]) // want
165    columns[3] = color.magenta(columns[3]) // latest
166  }
167
168  return columns
169}
170
171function makeParseable (list) {
172  return list.map(function (p) {
173    var dep = p[0]
174    var depname = p[1]
175    var dir = dep.path
176    var has = p[2]
177    var want = p[3]
178    var latest = p[4]
179    var type = p[6]
180
181    var out = [
182      dir,
183      depname + '@' + want,
184      (has ? (depname + '@' + has) : 'MISSING'),
185      depname + '@' + latest
186    ]
187    if (long) out.push(type, dep.package.homepage)
188
189    return out.join(':')
190  }).join(os.EOL)
191}
192
193function makeJSON (list, opts) {
194  var out = {}
195  list.forEach(function (p) {
196    var dep = p[0]
197    var depname = p[1]
198    var dir = dep.path
199    var has = p[2]
200    var want = p[3]
201    var latest = p[4]
202    var type = p[6]
203    if (!opts.global) {
204      dir = path.relative(process.cwd(), dir)
205    }
206    out[depname] = { current: has,
207      wanted: want,
208      latest: latest,
209      location: dir
210    }
211    if (long) {
212      out[depname].type = type
213      out[depname].homepage = dep.package.homepage
214    }
215  })
216  return JSON.stringify(out, null, 2)
217}
218
219function outdated_ (args, path, tree, parentHas, depth, opts, cb) {
220  if (!tree.package) tree.package = {}
221  if (path && moduleName(tree)) path += ' > ' + tree.package.name
222  if (!path && moduleName(tree)) path = tree.package.name
223  if (depth > opts.depth) {
224    return cb(null, [])
225  }
226  var types = {}
227  var pkg = tree.package
228
229  if (!tree.children) tree.children = []
230
231  var deps = tree.error ? tree.children : tree.children.filter((child) => !isExtraneous(child))
232
233  deps.forEach(function (dep) {
234    types[moduleName(dep)] = 'dependencies'
235  })
236
237  Object.keys(tree.missingDeps || {}).forEach(function (name) {
238    deps.push({
239      package: { name: name },
240      path: tree.path,
241      parent: tree,
242      isMissing: true
243    })
244    types[name] = 'dependencies'
245  })
246
247  // If we explicitly asked for dev deps OR we didn't ask for production deps
248  // AND we asked to save dev-deps OR we didn't ask to save anything that's NOT
249  // dev deps then…
250  // (All the save checking here is because this gets called from npm-update currently
251  // and that requires this logic around dev deps.)
252  // FIXME: Refactor npm update to not be in terms of outdated.
253  var dev = opts.dev || /^dev(elopment)?$/.test(opts.also)
254  var prod = opts.production || /^prod(uction)?$/.test(opts.only)
255  if (
256    (dev || !prod) &&
257    (
258      opts['save-dev'] || (!opts.save && !opts['save-optional'])
259    )
260  ) {
261    Object.keys(tree.missingDevDeps).forEach(function (name) {
262      deps.push({
263        package: { name: name },
264        path: tree.path,
265        parent: tree,
266        isMissing: true
267      })
268      if (!types[name]) {
269        types[name] = 'devDependencies'
270      }
271    })
272  }
273
274  if (opts['save-dev']) {
275    deps = deps.filter(function (dep) { return pkg.devDependencies[moduleName(dep)] })
276    deps.forEach(function (dep) {
277      types[moduleName(dep)] = 'devDependencies'
278    })
279  } else if (opts.save) {
280    // remove optional dependencies from dependencies during --save.
281    deps = deps.filter(function (dep) { return !pkg.optionalDependencies[moduleName(dep)] })
282  } else if (opts['save-optional']) {
283    deps = deps.filter(function (dep) { return pkg.optionalDependencies[moduleName(dep)] })
284    deps.forEach(function (dep) {
285      types[moduleName(dep)] = 'optionalDependencies'
286    })
287  }
288  var doUpdate = dev || (
289    !prod &&
290    !Object.keys(parentHas).length &&
291    !opts.global
292  )
293  if (doUpdate) {
294    Object.keys(pkg.devDependencies || {}).forEach(function (k) {
295      if (!(k in parentHas)) {
296        deps[k] = pkg.devDependencies[k]
297        types[k] = 'devDependencies'
298      }
299    })
300  }
301
302  var has = Object.create(parentHas)
303  tree.children.forEach(function (child) {
304    if (moduleName(child) && child.package.private) {
305      deps = deps.filter(function (dep) { return dep !== child })
306    }
307    has[moduleName(child)] = {
308      version: child.isLink ? 'linked' : child.package.version,
309      from: child.isLink ? 'file:' + child.path : child.package._from
310    }
311  })
312
313  // now get what we should have, based on the dep.
314  // if has[dep] !== shouldHave[dep], then cb with the data
315  // otherwise dive into the folder
316  asyncMap(deps, function (dep, cb) {
317    var name = moduleName(dep)
318    var required
319    if (tree.package.dependencies && name in tree.package.dependencies) {
320      required = tree.package.dependencies[name]
321    } else if (tree.package.optionalDependencies && name in tree.package.optionalDependencies) {
322      required = tree.package.optionalDependencies[name]
323    } else if (tree.package.devDependencies && name in tree.package.devDependencies) {
324      required = tree.package.devDependencies[name]
325    } else if (has[name]) {
326      required = computeVersionSpec(tree, dep)
327    }
328
329    if (!long) return shouldUpdate(args, dep, name, has, required, depth, path, opts, cb)
330
331    shouldUpdate(args, dep, name, has, required, depth, path, opts, cb, types[name])
332  }, cb)
333}
334
335function shouldUpdate (args, tree, dep, has, req, depth, pkgpath, opts, cb, type) {
336  // look up the most recent version.
337  // if that's what we already have, or if it's not on the args list,
338  // then dive into it.  Otherwise, cb() with the data.
339
340  // { version: , from: }
341  var curr = has[dep]
342
343  function skip (er) {
344    // show user that no viable version can be found
345    if (er) return cb(er)
346    outdated_(args,
347      pkgpath,
348      tree,
349      has,
350      depth + 1,
351      opts,
352      cb)
353  }
354
355  if (args.length && args.indexOf(dep) === -1) return skip()
356
357  if (tree.isLink && req == null) return skip()
358
359  if (req == null || req === '') req = '*'
360
361  var parsed = npa.resolve(dep, req)
362  if (parsed.type === 'directory') {
363    if (tree.isLink) {
364      return skip()
365    } else {
366      return doIt('linked', 'linked')
367    }
368  } else if (parsed.type === 'git') {
369    return doIt('git', 'git')
370  } else if (parsed.type === 'file') {
371    return updateLocalDeps()
372  } else if (parsed.type === 'remote') {
373    return doIt('remote', 'remote')
374  } else {
375    return packument(parsed, opts.concat({
376      'prefer-online': true
377    })).nodeify(updateDeps)
378  }
379
380  function doIt (wanted, latest) {
381    let c = curr && curr.version
382    if (parsed.type === 'alias') {
383      c = `npm:${parsed.subSpec.name}@${c}`
384    }
385    if (!long) {
386      return cb(null, [[tree, dep, c, wanted, latest, req, null, pkgpath]])
387    }
388    cb(null, [[tree, dep, c, wanted, latest, req, type, pkgpath]])
389  }
390
391  function updateLocalDeps (latestRegistryVersion) {
392    fetchPackageMetadata('file:' + parsed.fetchSpec, '.', (er, localDependency) => {
393      if (er) return cb()
394
395      var wanted = localDependency.version
396      var latest = localDependency.version
397
398      if (latestRegistryVersion) {
399        latest = latestRegistryVersion
400        if (semver.lt(wanted, latestRegistryVersion)) {
401          wanted = latestRegistryVersion
402          req = dep + '@' + latest
403        }
404      }
405
406      if (!curr || curr.version !== wanted) {
407        doIt(wanted, latest)
408      } else {
409        skip()
410      }
411    })
412  }
413
414  function updateDeps (er, d) {
415    if (er) return cb(er)
416
417    if (parsed.type === 'alias') {
418      req = parsed.subSpec.rawSpec
419    }
420    try {
421      var l = pickManifest(d, 'latest')
422      var m = pickManifest(d, req)
423    } catch (er) {
424      if (er.code === 'ETARGET' || er.code === 'E403') {
425        return skip(er)
426      } else {
427        return skip()
428      }
429    }
430
431    // check that the url origin hasn't changed (#1727) and that
432    // there is no newer version available
433    var dFromUrl = m._from && url.parse(m._from).protocol
434    var cFromUrl = curr && curr.from && url.parse(curr.from).protocol
435
436    if (
437      !curr ||
438      (dFromUrl && cFromUrl && m._from !== curr.from) ||
439      m.version !== curr.version ||
440      m.version !== l.version
441    ) {
442      if (parsed.type === 'alias') {
443        doIt(
444          `npm:${parsed.subSpec.name}@${m.version}`,
445          `npm:${parsed.subSpec.name}@${l.version}`
446        )
447      } else {
448        doIt(m.version, l.version)
449      }
450    } else {
451      skip()
452    }
453  }
454}
455