view.js 15 KB

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