UNPKG

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