UNPKG

16.5 kBJavaScriptView Raw
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.realPath !== data.path ? data.realPath : '') +
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}