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