1// show the installed versions of packages 2// 3// --parseable creates output like this: 4// <fullpath>:<name@ver>:<realpath>:<flags> 5// Flags are a :-separated list of zero or more indicators 6 7module.exports = exports = ls 8 9var path = require('path') 10var url = require('url') 11var readPackageTree = require('read-package-tree') 12var archy = require('archy') 13var semver = require('semver') 14var color = require('ansicolors') 15var moduleName = require('./utils/module-name.js') 16var npa = require('npm-package-arg') 17var sortedObject = require('sorted-object') 18var npm = require('./npm.js') 19var mutateIntoLogicalTree = require('./install/mutate-into-logical-tree.js') 20var computeMetadata = require('./install/deps.js').computeMetadata 21var readShrinkwrap = require('./install/read-shrinkwrap.js') 22var packageId = require('./utils/package-id.js') 23var usage = require('./utils/usage') 24var output = require('./utils/output.js') 25 26ls.usage = usage( 27 'ls', 28 'npm ls [[<@scope>/]<pkg> ...]' 29) 30 31ls.completion = require('./utils/completion/installed-deep.js') 32 33function ls (args, silent, cb) { 34 if (typeof cb !== 'function') { 35 cb = silent 36 silent = false 37 } 38 var dir = path.resolve(npm.dir, '..') 39 readPackageTree(dir, function (_, physicalTree) { 40 if (!physicalTree) physicalTree = {package: {}, path: dir} 41 physicalTree.isTop = true 42 readShrinkwrap.andInflate(physicalTree, function () { 43 lsFromTree(dir, computeMetadata(physicalTree), args, silent, cb) 44 }) 45 }) 46} 47 48function inList (list, value) { 49 return list.indexOf(value) !== -1 50} 51 52var lsFromTree = ls.fromTree = function (dir, physicalTree, args, silent, cb) { 53 if (typeof cb !== 'function') { 54 cb = silent 55 silent = false 56 } 57 58 // npm ls 'foo@~1.3' bar 'baz@<2' 59 if (!args) { 60 args = [] 61 } else { 62 args = args.map(function (a) { 63 if (typeof a === 'object' && a.package._requested.type === 'alias') { 64 return [moduleName(a), `npm:${a.package.name}@${a.package.version}`, a] 65 } else if (typeof a === 'object') { 66 return [a.package.name, a.package.version, a] 67 } else { 68 var p = npa(a) 69 var name = p.name 70 // When version spec is missing, we'll skip using it when filtering. 71 // Otherwise, `semver.validRange` would return '*', which won't 72 // match prerelease versions. 73 var ver = (p.rawSpec && 74 (semver.validRange(p.rawSpec) || '')) 75 return [ name, ver, a ] 76 } 77 }) 78 } 79 80 var data = mutateIntoLogicalTree.asReadInstalled(physicalTree) 81 82 pruneNestedExtraneous(data) 83 filterByEnv(data) 84 filterByLink(data) 85 86 var unlooped = filterFound(unloop(data), args) 87 var lite = getLite(unlooped) 88 89 if (silent) return cb(null, data, lite) 90 91 var long = npm.config.get('long') 92 var json = npm.config.get('json') 93 var out 94 if (json) { 95 var seen = new Set() 96 var d = long ? unlooped : lite 97 // the raw data can be circular 98 out = JSON.stringify(d, function (k, o) { 99 if (typeof o === 'object') { 100 if (seen.has(o)) return '[Circular]' 101 seen.add(o) 102 } 103 return o 104 }, 2) 105 } else if (npm.config.get('parseable')) { 106 out = makeParseable(unlooped, long, dir) 107 } else if (data) { 108 out = makeArchy(unlooped, long, dir) 109 } 110 output(out) 111 112 if (args.length && !data._found) process.exitCode = 1 113 114 var er 115 // if any errors were found, then complain and exit status 1 116 if (lite.problems && lite.problems.length) { 117 er = lite.problems.join('\n') 118 } 119 cb(er, data, lite) 120} 121 122function pruneNestedExtraneous (data, visited) { 123 visited = visited || [] 124 visited.push(data) 125 for (var i in data.dependencies) { 126 if (data.dependencies[i].extraneous) { 127 data.dependencies[i].dependencies = {} 128 } else if (visited.indexOf(data.dependencies[i]) === -1) { 129 pruneNestedExtraneous(data.dependencies[i], visited) 130 } 131 } 132} 133 134function filterByEnv (data) { 135 var dev = npm.config.get('dev') || /^dev(elopment)?$/.test(npm.config.get('only')) 136 var production = npm.config.get('production') || /^prod(uction)?$/.test(npm.config.get('only')) 137 var dependencies = {} 138 var devKeys = Object.keys(data.devDependencies || []) 139 var prodKeys = Object.keys(data._dependencies || []) 140 Object.keys(data.dependencies).forEach(function (name) { 141 if (!dev && inList(devKeys, name) && !inList(prodKeys, name) && data.dependencies[name].missing) { 142 return 143 } 144 145 if ((dev && inList(devKeys, name)) || // only --dev 146 (production && inList(prodKeys, name)) || // only --production 147 (!dev && !production)) { // no --production|--dev|--only=xxx 148 dependencies[name] = data.dependencies[name] 149 } 150 }) 151 data.dependencies = dependencies 152} 153 154function filterByLink (data) { 155 if (npm.config.get('link')) { 156 var dependencies = {} 157 Object.keys(data.dependencies).forEach(function (name) { 158 var dependency = data.dependencies[name] 159 if (dependency.link) { 160 dependencies[name] = dependency 161 } 162 }) 163 data.dependencies = dependencies 164 } 165} 166 167function alphasort (a, b) { 168 a = a.toLowerCase() 169 b = b.toLowerCase() 170 return a > b ? 1 171 : a < b ? -1 : 0 172} 173 174function isCruft (data) { 175 return data.extraneous && data.error && data.error.code === 'ENOTDIR' 176} 177 178function getLite (data, noname, depth) { 179 var lite = {} 180 181 if (isCruft(data)) return lite 182 183 var maxDepth = npm.config.get('depth') 184 185 if (typeof depth === 'undefined') depth = 0 186 if (!noname && data.name) lite.name = data.name 187 if (data.version) lite.version = data.version 188 if (data.extraneous) { 189 lite.extraneous = true 190 lite.problems = lite.problems || [] 191 lite.problems.push('extraneous: ' + packageId(data) + ' ' + (data.path || '')) 192 } 193 194 if (data.error && data.path !== path.resolve(npm.globalDir, '..') && 195 (data.error.code !== 'ENOENT' || noname)) { 196 lite.invalid = true 197 lite.problems = lite.problems || [] 198 var message = data.error.message 199 lite.problems.push('error in ' + data.path + ': ' + message) 200 } 201 202 if (data._from) { 203 lite.from = data._from 204 } 205 206 if (data._resolved) { 207 lite.resolved = data._resolved 208 } 209 210 if (data.invalid) { 211 lite.invalid = true 212 lite.problems = lite.problems || [] 213 lite.problems.push('invalid: ' + 214 packageId(data) + 215 ' ' + (data.path || '')) 216 } 217 218 if (data.peerInvalid) { 219 lite.peerInvalid = true 220 lite.problems = lite.problems || [] 221 lite.problems.push('peer dep not met: ' + 222 packageId(data) + 223 ' ' + (data.path || '')) 224 } 225 226 var deps = (data.dependencies && Object.keys(data.dependencies)) || [] 227 if (deps.length) { 228 lite.dependencies = deps.map(function (d) { 229 var dep = data.dependencies[d] 230 if (dep.missing && !dep.optional) { 231 lite.problems = lite.problems || [] 232 var p 233 if (data.depth > maxDepth) { 234 p = 'max depth reached: ' 235 } else { 236 p = 'missing: ' 237 } 238 p += d + '@' + dep.requiredBy + 239 ', required by ' + 240 packageId(data) 241 lite.problems.push(p) 242 if (dep.dependencies) { 243 return [d, getLite(dep, true)] 244 } else { 245 return [d, { required: dep.requiredBy, missing: true }] 246 } 247 } else if (dep.peerMissing) { 248 lite.problems = lite.problems || [] 249 dep.peerMissing.forEach(function (missing) { 250 var pdm = 'peer dep missing: ' + 251 missing.requires + 252 ', required by ' + 253 missing.requiredBy 254 lite.problems.push(pdm) 255 }) 256 return [d, { required: dep, peerMissing: true }] 257 } else if (npm.config.get('json')) { 258 if (depth === maxDepth) delete dep.dependencies 259 return [d, getLite(dep, true, depth + 1)] 260 } 261 return [d, getLite(dep, true)] 262 }).reduce(function (deps, d) { 263 if (d[1].problems) { 264 lite.problems = lite.problems || [] 265 lite.problems.push.apply(lite.problems, d[1].problems) 266 } 267 deps[d[0]] = d[1] 268 return deps 269 }, {}) 270 } 271 return lite 272} 273 274function unloop (root) { 275 var queue = [root] 276 var seen = new Set() 277 seen.add(root) 278 279 while (queue.length) { 280 var current = queue.shift() 281 var deps = current.dependencies = current.dependencies || {} 282 Object.keys(deps).forEach(function (d) { 283 var dep = deps[d] 284 if (dep.missing && !dep.dependencies) return 285 if (dep.path && seen.has(dep)) { 286 dep = deps[d] = Object.assign({}, dep) 287 dep.dependencies = {} 288 dep._deduped = path.relative(root.path, dep.path).replace(/node_modules\//g, '') 289 return 290 } 291 seen.add(dep) 292 queue.push(dep) 293 }) 294 } 295 296 return root 297} 298 299function filterFound (root, args) { 300 if (!args.length) return root 301 if (!root.dependencies) return root 302 303 // Mark all deps 304 var toMark = [root] 305 while (toMark.length) { 306 var markPkg = toMark.shift() 307 var markDeps = markPkg.dependencies 308 if (!markDeps) continue 309 Object.keys(markDeps).forEach(function (depName) { 310 var dep = markDeps[depName] 311 if (dep.peerMissing && !dep._from) return 312 dep._parent = markPkg 313 for (var ii = 0; ii < args.length; ii++) { 314 var argName = args[ii][0] 315 var argVersion = args[ii][1] 316 var argRaw = args[ii][2] 317 var found 318 if (typeof argRaw === 'object') { 319 if (dep.path === argRaw.path) { 320 found = true 321 } 322 } else if (depName === argName && argVersion) { 323 found = semver.satisfies(dep.version, argVersion, true) 324 } else if (depName === argName) { 325 // If version is missing from arg, just do a name match. 326 found = true 327 } 328 if (found) { 329 dep._found = 'explicit' 330 var parent = dep._parent 331 while (parent && !parent._found && !parent._deduped) { 332 parent._found = 'implicit' 333 parent = parent._parent 334 } 335 break 336 } 337 } 338 toMark.push(dep) 339 }) 340 } 341 var toTrim = [root] 342 while (toTrim.length) { 343 var trimPkg = toTrim.shift() 344 var trimDeps = trimPkg.dependencies 345 if (!trimDeps) continue 346 trimPkg.dependencies = {} 347 Object.keys(trimDeps).forEach(function (name) { 348 var dep = trimDeps[name] 349 if (!dep._found) return 350 if (dep._found === 'implicit' && dep._deduped) return 351 trimPkg.dependencies[name] = dep 352 toTrim.push(dep) 353 }) 354 } 355 return root 356} 357 358function makeArchy (data, long, dir) { 359 var out = makeArchy_(data, long, dir, 0) 360 return archy(out, '', { unicode: npm.config.get('unicode') }) 361} 362 363function makeArchy_ (data, long, dir, depth, parent, d) { 364 if (data.missing) { 365 if (depth - 1 <= npm.config.get('depth')) { 366 // just missing 367 var unmet = 'UNMET ' + (data.optional ? 'OPTIONAL ' : '') + 'DEPENDENCY' 368 if (npm.color) { 369 if (data.optional) { 370 unmet = color.bgBlack(color.yellow(unmet)) 371 } else { 372 unmet = color.bgBlack(color.red(unmet)) 373 } 374 } 375 var label = data._id || (d + '@' + data.requiredBy) 376 if (data._found === 'explicit' && data._id) { 377 if (npm.color) { 378 label = color.bgBlack(color.yellow(label.trim())) + ' ' 379 } else { 380 label = label.trim() + ' ' 381 } 382 } 383 return { 384 label: unmet + ' ' + label, 385 nodes: Object.keys(data.dependencies || {}) 386 .sort(alphasort).filter(function (d) { 387 return !isCruft(data.dependencies[d]) 388 }).map(function (d) { 389 return makeArchy_(sortedObject(data.dependencies[d]), long, dir, depth + 1, data, d) 390 }) 391 } 392 } else { 393 return {label: d + '@' + data.requiredBy} 394 } 395 } 396 397 var out = {} 398 if (data._requested && data._requested.type === 'alias') { 399 out.label = `${d}@npm:${data._id}` 400 } else { 401 out.label = data._id || '' 402 } 403 if (data._found === 'explicit' && data._id) { 404 if (npm.color) { 405 out.label = color.bgBlack(color.yellow(out.label.trim())) + ' ' 406 } else { 407 out.label = out.label.trim() + ' ' 408 } 409 } 410 if (data.link) out.label += ' -> ' + data.link 411 412 if (data._deduped) { 413 if (npm.color) { 414 out.label += ' ' + color.brightBlack('deduped') 415 } else { 416 out.label += ' deduped' 417 } 418 } 419 420 if (data.invalid) { 421 if (data.realName !== data.name) out.label += ' (' + data.realName + ')' 422 var invalid = 'invalid' 423 if (npm.color) invalid = color.bgBlack(color.red(invalid)) 424 out.label += ' ' + invalid 425 } 426 427 if (data.peerInvalid) { 428 var peerInvalid = 'peer invalid' 429 if (npm.color) peerInvalid = color.bgBlack(color.red(peerInvalid)) 430 out.label += ' ' + peerInvalid 431 } 432 433 if (data.peerMissing) { 434 var peerMissing = 'UNMET PEER DEPENDENCY' 435 436 if (npm.color) peerMissing = color.bgBlack(color.red(peerMissing)) 437 out.label = peerMissing + ' ' + out.label 438 } 439 440 if (data.extraneous && data.path !== dir) { 441 var extraneous = 'extraneous' 442 if (npm.color) extraneous = color.bgBlack(color.green(extraneous)) 443 out.label += ' ' + extraneous 444 } 445 446 if (data.error && depth) { 447 var message = data.error.message 448 if (message.indexOf('\n')) message = message.slice(0, message.indexOf('\n')) 449 var error = 'error: ' + message 450 if (npm.color) error = color.bgRed(color.brightWhite(error)) 451 out.label += ' ' + error 452 } 453 454 // add giturl to name@version 455 if (data._resolved) { 456 try { 457 var type = npa(data._resolved).type 458 var isGit = type === 'git' || type === 'hosted' 459 if (isGit) { 460 out.label += ' (' + data._resolved + ')' 461 } 462 } catch (ex) { 463 // npa threw an exception then it ain't git so whatev 464 } 465 } 466 467 if (long) { 468 if (dir === data.path) out.label += '\n' + dir 469 out.label += '\n' + getExtras(data) 470 } else if (dir === data.path) { 471 if (out.label) out.label += ' ' 472 out.label += dir 473 } 474 475 // now all the children. 476 out.nodes = [] 477 if (depth <= npm.config.get('depth')) { 478 out.nodes = Object.keys(data.dependencies || {}) 479 .sort(alphasort).filter(function (d) { 480 return !isCruft(data.dependencies[d]) 481 }).map(function (d) { 482 return makeArchy_(sortedObject(data.dependencies[d]), long, dir, depth + 1, data, d) 483 }) 484 } 485 486 if (out.nodes.length === 0 && data.path === dir) { 487 out.nodes = ['(empty)'] 488 } 489 490 return out 491} 492 493function getExtras (data) { 494 var extras = [] 495 496 if (data.description) extras.push(data.description) 497 if (data.repository) extras.push(data.repository.url) 498 if (data.homepage) extras.push(data.homepage) 499 if (data._from) { 500 var from = data._from 501 if (from.indexOf(data.name + '@') === 0) { 502 from = from.substr(data.name.length + 1) 503 } 504 var u = url.parse(from) 505 if (u.protocol) extras.push(from) 506 } 507 return extras.join('\n') 508} 509 510function makeParseable (data, long, dir, depth, parent, d) { 511 if (data._deduped) return [] 512 depth = depth || 0 513 if (depth > npm.config.get('depth')) return [ makeParseable_(data, long, dir, depth, parent, d) ] 514 return [ makeParseable_(data, long, dir, depth, parent, d) ] 515 .concat(Object.keys(data.dependencies || {}) 516 .sort(alphasort).map(function (d) { 517 return makeParseable(data.dependencies[d], long, dir, depth + 1, data, d) 518 })) 519 .filter(function (x) { return x && x.length }) 520 .join('\n') 521} 522 523function makeParseable_ (data, long, dir, depth, parent, d) { 524 if (data.hasOwnProperty('_found') && data._found !== 'explicit') return '' 525 526 if (data.missing) { 527 if (depth < npm.config.get('depth')) { 528 data = npm.config.get('long') 529 ? path.resolve(parent.path, 'node_modules', d) + 530 ':' + d + '@' + JSON.stringify(data.requiredBy) + ':INVALID:MISSING' 531 : '' 532 } else { 533 data = path.resolve(dir || '', 'node_modules', d || '') + 534 (npm.config.get('long') 535 ? ':' + d + '@' + JSON.stringify(data.requiredBy) + 536 ':' + // no realpath resolved 537 ':MAXDEPTH' 538 : '') 539 } 540 541 return data 542 } 543 544 if (!npm.config.get('long')) return data.path 545 546 return data.path + 547 ':' + (data._id || '') + 548 (data.link && data.link !== data.path ? ':' + data.link : '') + 549 (data.extraneous ? ':EXTRANEOUS' : '') + 550 (data.error && data.path !== path.resolve(npm.globalDir, '..') ? ':ERROR' : '') + 551 (data.invalid ? ':INVALID' : '') + 552 (data.peerInvalid ? ':PEERINVALID' : '') + 553 (data.peerMissing ? ':PEERINVALID:MISSING' : '') 554} 555