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