UNPKG

14.7 kBJavaScriptView Raw
1'use strict'
2
3// npm view [pkg [pkg ...]]
4module.exports = view
5
6const BB = require('bluebird')
7
8const byteSize = require('byte-size')
9const color = require('ansicolors')
10const columns = require('cli-columns')
11const relativeDate = require('tiny-relative-date')
12const style = require('ansistyles')
13var npm = require('./npm.js')
14var readJson = require('read-package-json')
15var log = require('npmlog')
16var util = require('util')
17var semver = require('semver')
18var mapToRegistry = require('./utils/map-to-registry.js')
19var npa = require('npm-package-arg')
20var path = require('path')
21var usage = require('./utils/usage')
22
23view.usage = usage(
24 'view',
25 'npm view [<@scope>/]<pkg>[@<version>] [<field>[.subfield]...]'
26)
27
28view.completion = function (opts, cb) {
29 if (opts.conf.argv.remain.length <= 2) {
30 // FIXME: there used to be registry completion here, but it stopped making
31 // sense somewhere around 50,000 packages on the registry
32 return cb()
33 }
34 // have the package, get the fields.
35 var tag = npm.config.get('tag')
36 mapToRegistry(opts.conf.argv.remain[2], npm.config, function (er, uri, auth) {
37 if (er) return cb(er)
38
39 npm.registry.get(uri, { auth: auth }, function (er, d) {
40 if (er) return cb(er)
41 var dv = d.versions[d['dist-tags'][tag]]
42 var fields = []
43 d.versions = Object.keys(d.versions).sort(semver.compareLoose)
44 fields = getFields(d).concat(getFields(dv))
45 cb(null, fields)
46 })
47 })
48
49 function getFields (d, f, pref) {
50 f = f || []
51 if (!d) return f
52 pref = pref || []
53 Object.keys(d).forEach(function (k) {
54 if (k.charAt(0) === '_' || k.indexOf('.') !== -1) return
55 var p = pref.concat(k).join('.')
56 f.push(p)
57 if (Array.isArray(d[k])) {
58 d[k].forEach(function (val, i) {
59 var pi = p + '[' + i + ']'
60 if (val && typeof val === 'object') getFields(val, f, [p])
61 else f.push(pi)
62 })
63 return
64 }
65 if (typeof d[k] === 'object') getFields(d[k], f, [p])
66 })
67 return f
68 }
69}
70
71function view (args, silent, cb) {
72 if (typeof cb !== 'function') {
73 cb = silent
74 silent = false
75 }
76
77 if (!args.length) args = ['.']
78
79 var pkg = args.shift()
80 var nv
81 if (/^[.]@/.test(pkg)) {
82 nv = npa.resolve(null, pkg.slice(2))
83 } else {
84 nv = npa(pkg)
85 }
86 var name = nv.name
87 var local = (name === '.' || !name)
88
89 if (npm.config.get('global') && local) {
90 return cb(new Error('Cannot use view command in global mode.'))
91 }
92
93 if (local) {
94 var dir = npm.prefix
95 readJson(path.resolve(dir, 'package.json'), function (er, d) {
96 d = d || {}
97 if (er && er.code !== 'ENOENT' && er.code !== 'ENOTDIR') return cb(er)
98 if (!d.name) return cb(new Error('Invalid package.json'))
99
100 var p = d.name
101 nv = npa(p)
102 if (pkg && ~pkg.indexOf('@')) {
103 nv.rawSpec = pkg.split('@')[pkg.indexOf('@')]
104 }
105
106 fetchAndRead(nv, args, silent, cb)
107 })
108 } else {
109 fetchAndRead(nv, args, silent, cb)
110 }
111}
112
113function fetchAndRead (nv, args, silent, cb) {
114 // get the data about this package
115 var name = nv.name
116 var version = nv.rawSpec || npm.config.get('tag')
117
118 mapToRegistry(name, npm.config, function (er, uri, auth) {
119 if (er) return cb(er)
120
121 npm.registry.get(uri, { auth: auth }, function (er, data) {
122 if (er) return cb(er)
123 if (data['dist-tags'] && data['dist-tags'][version]) {
124 version = data['dist-tags'][version]
125 }
126
127 if (data.time && data.time.unpublished) {
128 var u = data.time.unpublished
129 er = new Error('Unpublished by ' + u.name + ' on ' + u.time)
130 er.statusCode = 404
131 er.code = 'E404'
132 er.pkgid = data._id
133 return cb(er, data)
134 }
135
136 var results = []
137 var error = null
138 var versions = data.versions || {}
139 data.versions = Object.keys(versions).sort(semver.compareLoose)
140 if (!args.length) args = ['']
141
142 // remove readme unless we asked for it
143 if (args.indexOf('readme') === -1) {
144 delete data.readme
145 }
146
147 Object.keys(versions).forEach(function (v) {
148 if (semver.satisfies(v, version, true)) {
149 args.forEach(function (args) {
150 // remove readme unless we asked for it
151 if (args.indexOf('readme') !== -1) {
152 delete versions[v].readme
153 }
154 results.push(showFields(data, versions[v], args))
155 })
156 }
157 })
158 var retval = results.reduce(reducer, {})
159
160 if (args.length === 1 && args[0] === '') {
161 retval = cleanBlanks(retval)
162 log.silly('cleanup', retval)
163 }
164
165 if (error || silent) {
166 cb(error, retval)
167 } else if (
168 !npm.config.get('json') &&
169 args.length === 1 &&
170 args[0] === ''
171 ) {
172 data.version = version
173 BB.all(results.map((v) => prettyView(data, v[Object.keys(v)[0]][''])))
174 .nodeify(cb)
175 .then(() => retval)
176 } else {
177 printData(retval, data._id, cb.bind(null, error, retval))
178 }
179 })
180 })
181}
182
183function prettyView (packument, manifest) {
184 // More modern, pretty printing of default view
185 const unicode = npm.config.get('unicode')
186 return BB.try(() => {
187 if (!manifest) {
188 log.error(
189 'view',
190 'No matching versions.\n' +
191 'To see a list of versions, run:\n' +
192 `> npm view ${packument.name} versions`
193 )
194 return
195 }
196 const tags = []
197 Object.keys(packument['dist-tags']).forEach((t) => {
198 const version = packument['dist-tags'][t]
199 tags.push(`${style.bright(color.green(t))}: ${version}`)
200 })
201 const unpackedSize = manifest.dist.unpackedSize &&
202 byteSize(manifest.dist.unpackedSize)
203 const licenseField = manifest.license || manifest.licence || 'Proprietary'
204 const info = {
205 name: color.green(manifest.name),
206 version: color.green(manifest.version),
207 bins: Object.keys(manifest.bin || {}).map(color.yellow),
208 versions: color.yellow(packument.versions.length + ''),
209 description: manifest.description,
210 deprecated: manifest.deprecated,
211 keywords: (packument.keywords || []).map(color.yellow),
212 license: typeof licenseField === 'string'
213 ? licenseField
214 : (licenseField.type || 'Proprietary'),
215 deps: Object.keys(manifest.dependencies || {}).map((dep) => {
216 return `${color.yellow(dep)}: ${manifest.dependencies[dep]}`
217 }),
218 publisher: manifest._npmUser && unparsePerson({
219 name: color.yellow(manifest._npmUser.name),
220 email: color.cyan(manifest._npmUser.email)
221 }),
222 modified: color.yellow(relativeDate(packument.time[packument.version])),
223 maintainers: (packument.maintainers || []).map((u) => unparsePerson({
224 name: color.yellow(u.name),
225 email: color.cyan(u.email)
226 })),
227 repo: (
228 manifest.bugs && (manifest.bugs.url || manifest.bugs)
229 ) || (
230 manifest.repository && (manifest.repository.url || manifest.repository)
231 ),
232 site: (
233 manifest.homepage && (manifest.homepage.url || manifest.homepage)
234 ),
235 stars: color.yellow('' + packument.users ? Object.keys(packument.users || {}).length : 0),
236 tags,
237 tarball: color.cyan(manifest.dist.tarball),
238 shasum: color.yellow(manifest.dist.shasum),
239 integrity: manifest.dist.integrity && color.yellow(manifest.dist.integrity),
240 fileCount: manifest.dist.fileCount && color.yellow(manifest.dist.fileCount),
241 unpackedSize: unpackedSize && color.yellow(unpackedSize.value) + ' ' + unpackedSize.unit
242 }
243 if (info.license.toLowerCase().trim() === 'proprietary') {
244 info.license = style.bright(color.red(info.license))
245 } else {
246 info.license = color.green(info.license)
247 }
248 console.log('')
249 console.log(
250 style.underline(style.bright(`${info.name}@${info.version}`)) +
251 ' | ' + info.license +
252 ' | deps: ' + (info.deps.length ? color.cyan(info.deps.length) : color.green('none')) +
253 ' | versions: ' + info.versions
254 )
255 info.description && console.log(info.description)
256 if (info.repo || info.site) {
257 info.site && console.log(color.cyan(info.site))
258 }
259
260 const warningSign = unicode ? ' ⚠️ ' : '!!'
261 info.deprecated && console.log(
262 `\n${style.bright(color.red('DEPRECATED'))}${
263 warningSign
264 } - ${info.deprecated}`
265 )
266
267 if (info.keywords.length) {
268 console.log('')
269 console.log('keywords:', info.keywords.join(', '))
270 }
271
272 if (info.bins.length) {
273 console.log('')
274 console.log('bin:', info.bins.join(', '))
275 }
276
277 console.log('')
278 console.log('dist')
279 console.log('.tarball:', info.tarball)
280 console.log('.shasum:', info.shasum)
281 info.integrity && console.log('.integrity:', info.integrity)
282 info.unpackedSize && console.log('.unpackedSize:', info.unpackedSize)
283
284 const maxDeps = 24
285 if (info.deps.length) {
286 console.log('')
287 console.log('dependencies:')
288 console.log(columns(info.deps.slice(0, maxDeps), {padding: 1}))
289 if (info.deps.length > maxDeps) {
290 console.log(`(...and ${info.deps.length - maxDeps} more.)`)
291 }
292 }
293
294 if (info.maintainers && info.maintainers.length) {
295 console.log('')
296 console.log('maintainers:')
297 info.maintainers.forEach((u) => console.log('-', u))
298 }
299
300 console.log('')
301 console.log('dist-tags:')
302 console.log(columns(info.tags))
303
304 if (info.publisher || info.modified) {
305 let publishInfo = 'published'
306 if (info.modified) { publishInfo += ` ${info.modified}` }
307 if (info.publisher) { publishInfo += ` by ${info.publisher}` }
308 console.log('')
309 console.log(publishInfo)
310 }
311 })
312}
313
314function cleanBlanks (obj) {
315 var clean = {}
316 Object.keys(obj).forEach(function (version) {
317 clean[version] = obj[version]['']
318 })
319 return clean
320}
321
322function reducer (l, r) {
323 if (r) {
324 Object.keys(r).forEach(function (v) {
325 l[v] = l[v] || {}
326 Object.keys(r[v]).forEach(function (t) {
327 l[v][t] = r[v][t]
328 })
329 })
330 }
331
332 return l
333}
334
335// return whatever was printed
336function showFields (data, version, fields) {
337 var o = {}
338 ;[data, version].forEach(function (s) {
339 Object.keys(s).forEach(function (k) {
340 o[k] = s[k]
341 })
342 })
343 return search(o, fields.split('.'), version.version, fields)
344}
345
346function search (data, fields, version, title) {
347 var field
348 var tail = fields
349 while (!field && fields.length) field = tail.shift()
350 fields = [field].concat(tail)
351 var o
352 if (!field && !tail.length) {
353 o = {}
354 o[version] = {}
355 o[version][title] = data
356 return o
357 }
358 var index = field.match(/(.+)\[([^\]]+)\]$/)
359 if (index) {
360 field = index[1]
361 index = index[2]
362 if (data.field && data.field.hasOwnProperty(index)) {
363 return search(data[field][index], tail, version, title)
364 } else {
365 field = field + '[' + index + ']'
366 }
367 }
368 if (Array.isArray(data)) {
369 if (data.length === 1) {
370 return search(data[0], fields, version, title)
371 }
372 var results = []
373 data.forEach(function (data, i) {
374 var tl = title.length
375 var newt = title.substr(0, tl - fields.join('.').length - 1) +
376 '[' + i + ']' + [''].concat(fields).join('.')
377 results.push(search(data, fields.slice(), version, newt))
378 })
379 results = results.reduce(reducer, {})
380 return results
381 }
382 if (!data.hasOwnProperty(field)) return undefined
383 data = data[field]
384 if (tail.length) {
385 if (typeof data === 'object') {
386 // there are more fields to deal with.
387 return search(data, tail, version, title)
388 } else {
389 return new Error('Not an object: ' + data)
390 }
391 }
392 o = {}
393 o[version] = {}
394 o[version][title] = data
395 return o
396}
397
398function printData (data, name, cb) {
399 var versions = Object.keys(data)
400 var msg = ''
401 var msgJson = []
402 var includeVersions = versions.length > 1
403 var includeFields
404
405 versions.forEach(function (v) {
406 var fields = Object.keys(data[v])
407 includeFields = includeFields || (fields.length > 1)
408 if (npm.config.get('json')) msgJson.push({})
409 fields.forEach(function (f) {
410 var d = cleanup(data[v][f])
411 if (fields.length === 1 && npm.config.get('json')) {
412 msgJson[msgJson.length - 1][f] = d
413 }
414 if (includeVersions || includeFields || typeof d !== 'string') {
415 if (npm.config.get('json')) {
416 msgJson[msgJson.length - 1][f] = d
417 } else {
418 d = util.inspect(d, { showHidden: false, depth: 5, colors: npm.color, maxArrayLength: null })
419 }
420 } else if (typeof d === 'string' && npm.config.get('json')) {
421 d = JSON.stringify(d)
422 }
423 if (!npm.config.get('json')) {
424 if (f && includeFields) f += ' = '
425 if (d.indexOf('\n') !== -1) d = ' \n' + d
426 msg += (includeVersions ? name + '@' + v + ' ' : '') +
427 (includeFields ? f : '') + d + '\n'
428 }
429 })
430 })
431
432 if (npm.config.get('json')) {
433 if (msgJson.length && Object.keys(msgJson[0]).length === 1) {
434 var k = Object.keys(msgJson[0])[0]
435 msgJson = msgJson.map(function (m) { return m[k] })
436 }
437
438 if (msgJson.length === 1) {
439 msg = JSON.stringify(msgJson[0], null, 2) + '\n'
440 } else if (msgJson.length > 1) {
441 msg = JSON.stringify(msgJson, null, 2) + '\n'
442 }
443 }
444
445 // preserve output symmetry by adding a whitespace-only line at the end if
446 // there's one at the beginning
447 if (/^\s*\n/.test(msg)) msg += '\n'
448
449 // disable the progress bar entirely, as we can't meaningfully update it if
450 // we may have partial lines printed.
451 log.disableProgress()
452
453 // print directly to stdout to not unnecessarily add blank lines
454 process.stdout.write(msg, () => cb(null, data))
455}
456function cleanup (data) {
457 if (Array.isArray(data)) {
458 return data.map(cleanup)
459 }
460 if (!data || typeof data !== 'object') return data
461
462 if (typeof data.versions === 'object' &&
463 data.versions &&
464 !Array.isArray(data.versions)) {
465 data.versions = Object.keys(data.versions || {})
466 }
467
468 var keys = Object.keys(data)
469 keys.forEach(function (d) {
470 if (d.charAt(0) === '_') delete data[d]
471 else if (typeof data[d] === 'object') data[d] = cleanup(data[d])
472 })
473 keys = Object.keys(data)
474 if (keys.length <= 3 &&
475 data.name &&
476 (keys.length === 1 ||
477 (keys.length === 3 && data.email && data.url) ||
478 (keys.length === 2 && (data.email || data.url)))) {
479 data = unparsePerson(data)
480 }
481 return data
482}
483function unparsePerson (d) {
484 if (typeof d === 'string') return d
485 return d.name +
486 (d.email ? ' <' + d.email + '>' : '') +
487 (d.url ? ' (' + d.url + ')' : '')
488}