view.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. // npm view [pkg [pkg ...]]
  2. const color = require('ansicolors')
  3. const columns = require('cli-columns')
  4. const fs = require('fs')
  5. const jsonParse = require('json-parse-even-better-errors')
  6. const log = require('npmlog')
  7. const npa = require('npm-package-arg')
  8. const { resolve } = require('path')
  9. const formatBytes = require('./utils/format-bytes.js')
  10. const relativeDate = require('tiny-relative-date')
  11. const semver = require('semver')
  12. const style = require('ansistyles')
  13. const { inspect, promisify } = require('util')
  14. const { packument } = require('pacote')
  15. const readFile = promisify(fs.readFile)
  16. const readJson = async file => jsonParse(await readFile(file, 'utf8'))
  17. const Queryable = require('./utils/queryable.js')
  18. const BaseCommand = require('./base-command.js')
  19. class View extends BaseCommand {
  20. /* istanbul ignore next - see test/lib/load-all-commands.js */
  21. static get description () {
  22. return 'View registry info'
  23. }
  24. /* istanbul ignore next - see test/lib/load-all-commands.js */
  25. static get params () {
  26. return [
  27. 'json',
  28. 'workspace',
  29. 'workspaces',
  30. 'include-workspace-root',
  31. ]
  32. }
  33. /* istanbul ignore next - see test/lib/load-all-commands.js */
  34. static get name () {
  35. return 'view'
  36. }
  37. /* istanbul ignore next - see test/lib/load-all-commands.js */
  38. static get usage () {
  39. return ['[<@scope>/]<pkg>[@<version>] [<field>[.subfield]...]']
  40. }
  41. async completion (opts) {
  42. if (opts.conf.argv.remain.length <= 2) {
  43. // There used to be registry completion here, but it stopped
  44. // making sense somewhere around 50,000 packages on the registry
  45. return
  46. }
  47. // have the package, get the fields
  48. const config = {
  49. ...this.npm.flatOptions,
  50. fullMetadata: true,
  51. preferOnline: true,
  52. }
  53. const spec = npa(opts.conf.argv.remain[2])
  54. const pckmnt = await packument(spec, config)
  55. const defaultTag = this.npm.config.get('tag')
  56. const dv = pckmnt.versions[pckmnt['dist-tags'][defaultTag]]
  57. pckmnt.versions = Object.keys(pckmnt.versions).sort(semver.compareLoose)
  58. return getFields(pckmnt).concat(getFields(dv))
  59. function getFields (d, f, pref) {
  60. f = f || []
  61. if (!d)
  62. return f
  63. pref = pref || []
  64. Object.keys(d).forEach((k) => {
  65. if (k.charAt(0) === '_' || k.indexOf('.') !== -1)
  66. return
  67. const p = pref.concat(k).join('.')
  68. f.push(p)
  69. if (Array.isArray(d[k])) {
  70. d[k].forEach((val, i) => {
  71. const pi = p + '[' + i + ']'
  72. if (val && typeof val === 'object')
  73. getFields(val, f, [p])
  74. else
  75. f.push(pi)
  76. })
  77. return
  78. }
  79. if (typeof d[k] === 'object')
  80. getFields(d[k], f, [p])
  81. })
  82. return f
  83. }
  84. }
  85. exec (args, cb) {
  86. this.view(args).then(() => cb()).catch(cb)
  87. }
  88. execWorkspaces (args, filters, cb) {
  89. this.viewWorkspaces(args, filters).then(() => cb()).catch(cb)
  90. }
  91. async view (args) {
  92. if (!args.length)
  93. args = ['.']
  94. let pkg = args.shift()
  95. const local = /^\.@/.test(pkg) || pkg === '.'
  96. if (local) {
  97. if (this.npm.config.get('global'))
  98. throw new Error('Cannot use view command in global mode.')
  99. const dir = this.npm.prefix
  100. const manifest = await readJson(resolve(dir, 'package.json'))
  101. if (!manifest.name)
  102. throw new Error('Invalid package.json, no "name" field')
  103. // put the version back if it existed
  104. pkg = `${manifest.name}${pkg.slice(1)}`
  105. }
  106. let wholePackument = false
  107. if (!args.length) {
  108. args = ['']
  109. wholePackument = true
  110. }
  111. const [pckmnt, data] = await this.getData(pkg, args)
  112. if (!this.npm.config.get('json') && wholePackument) {
  113. // pretty view (entire packument)
  114. data.map((v) => this.prettyView(pckmnt, v[Object.keys(v)[0]]['']))
  115. } else {
  116. // JSON formatted output (JSON or specific attributes from packument)
  117. let reducedData = data.reduce(reducer, {})
  118. if (wholePackument) {
  119. // No attributes
  120. reducedData = cleanBlanks(reducedData)
  121. log.silly('view', reducedData)
  122. }
  123. // disable the progress bar entirely, as we can't meaningfully update it
  124. // if we may have partial lines printed.
  125. log.disableProgress()
  126. const msg = await this.jsonData(reducedData, pckmnt._id)
  127. if (msg !== '')
  128. console.log(msg)
  129. }
  130. }
  131. async viewWorkspaces (args, filters) {
  132. if (!args.length)
  133. args = ['.']
  134. const pkg = args.shift()
  135. const local = /^\.@/.test(pkg) || pkg === '.'
  136. if (!local) {
  137. this.npm.log.warn('Ignoring workspaces for specified package(s)')
  138. return this.view([pkg, ...args])
  139. }
  140. let wholePackument = false
  141. if (!args.length) {
  142. wholePackument = true
  143. args = [''] // getData relies on this
  144. }
  145. const results = {}
  146. await this.setWorkspaces(filters)
  147. for (const name of this.workspaceNames) {
  148. const wsPkg = `${name}${pkg.slice(1)}`
  149. const [pckmnt, data] = await this.getData(wsPkg, args)
  150. let reducedData = data.reduce(reducer, {})
  151. if (wholePackument) {
  152. // No attributes
  153. reducedData = cleanBlanks(reducedData)
  154. log.silly('view', reducedData)
  155. }
  156. if (!this.npm.config.get('json')) {
  157. if (wholePackument)
  158. data.map((v) => this.prettyView(pckmnt, v[Object.keys(v)[0]]['']))
  159. else {
  160. console.log(`${name}:`)
  161. const msg = await this.jsonData(reducedData, pckmnt._id)
  162. if (msg !== '')
  163. console.log(msg)
  164. }
  165. } else {
  166. const msg = await this.jsonData(reducedData, pckmnt._id)
  167. if (msg !== '')
  168. results[name] = JSON.parse(msg)
  169. }
  170. }
  171. if (Object.keys(results).length > 0)
  172. console.log(JSON.stringify(results, null, 2))
  173. }
  174. async getData (pkg, args) {
  175. const opts = {
  176. ...this.npm.flatOptions,
  177. preferOnline: true,
  178. fullMetadata: true,
  179. }
  180. const spec = npa(pkg)
  181. // get the data about this package
  182. let version = this.npm.config.get('tag')
  183. // rawSpec is the git url if this is from git
  184. if (spec.type !== 'git' && spec.rawSpec)
  185. version = spec.rawSpec
  186. const pckmnt = await packument(spec, opts)
  187. if (pckmnt['dist-tags'] && pckmnt['dist-tags'][version])
  188. version = pckmnt['dist-tags'][version]
  189. if (pckmnt.time && pckmnt.time.unpublished) {
  190. const u = pckmnt.time.unpublished
  191. const er = new Error('Unpublished by ' + u.name + ' on ' + u.time)
  192. er.statusCode = 404
  193. er.code = 'E404'
  194. er.pkgid = pckmnt._id
  195. throw er
  196. }
  197. const data = []
  198. const versions = pckmnt.versions || {}
  199. pckmnt.versions = Object.keys(versions).sort(semver.compareLoose)
  200. // remove readme unless we asked for it
  201. if (args.indexOf('readme') === -1)
  202. delete pckmnt.readme
  203. Object.keys(versions).forEach((v) => {
  204. if (semver.satisfies(v, version, true)) {
  205. args.forEach(arg => {
  206. // remove readme unless we asked for it
  207. if (args.indexOf('readme') !== -1)
  208. delete versions[v].readme
  209. data.push(showFields(pckmnt, versions[v], arg))
  210. })
  211. }
  212. })
  213. if (
  214. !this.npm.config.get('json') &&
  215. args.length === 1 &&
  216. args[0] === ''
  217. )
  218. pckmnt.version = version
  219. return [pckmnt, data]
  220. }
  221. async jsonData (data, name) {
  222. const versions = Object.keys(data)
  223. let msg = ''
  224. let msgJson = []
  225. const includeVersions = versions.length > 1
  226. let includeFields
  227. const json = this.npm.config.get('json')
  228. versions.forEach((v) => {
  229. const fields = Object.keys(data[v])
  230. includeFields = includeFields || (fields.length > 1)
  231. if (json)
  232. msgJson.push({})
  233. fields.forEach((f) => {
  234. let d = cleanup(data[v][f])
  235. if (fields.length === 1 && json)
  236. msgJson[msgJson.length - 1][f] = d
  237. if (includeVersions || includeFields || typeof d !== 'string') {
  238. if (json)
  239. msgJson[msgJson.length - 1][f] = d
  240. else {
  241. d = inspect(d, {
  242. showHidden: false,
  243. depth: 5,
  244. colors: this.npm.color,
  245. maxArrayLength: null,
  246. })
  247. }
  248. } else if (typeof d === 'string' && json)
  249. d = JSON.stringify(d)
  250. if (!json) {
  251. if (f && includeFields)
  252. f += ' = '
  253. msg += (includeVersions ? name + '@' + v + ' ' : '') +
  254. (includeFields ? f : '') + d + '\n'
  255. }
  256. })
  257. })
  258. if (json) {
  259. if (msgJson.length && Object.keys(msgJson[0]).length === 1) {
  260. const k = Object.keys(msgJson[0])[0]
  261. msgJson = msgJson.map(m => m[k])
  262. }
  263. if (msgJson.length === 1)
  264. msg = JSON.stringify(msgJson[0], null, 2) + '\n'
  265. else if (msgJson.length > 1)
  266. msg = JSON.stringify(msgJson, null, 2) + '\n'
  267. }
  268. return msg.trim()
  269. }
  270. prettyView (packument, manifest) {
  271. // More modern, pretty printing of default view
  272. const unicode = this.npm.config.get('unicode')
  273. const tags = []
  274. Object.keys(packument['dist-tags']).forEach((t) => {
  275. const version = packument['dist-tags'][t]
  276. tags.push(`${style.bright(color.green(t))}: ${version}`)
  277. })
  278. const unpackedSize = manifest.dist.unpackedSize &&
  279. formatBytes(manifest.dist.unpackedSize, true)
  280. const licenseField = manifest.license || 'Proprietary'
  281. const info = {
  282. name: color.green(manifest.name),
  283. version: color.green(manifest.version),
  284. bins: Object.keys(manifest.bin || {}).map(color.yellow),
  285. versions: color.yellow(packument.versions.length + ''),
  286. description: manifest.description,
  287. deprecated: manifest.deprecated,
  288. keywords: (packument.keywords || []).map(color.yellow),
  289. license: typeof licenseField === 'string'
  290. ? licenseField
  291. : (licenseField.type || 'Proprietary'),
  292. deps: Object.keys(manifest.dependencies || {}).map((dep) => {
  293. return `${color.yellow(dep)}: ${manifest.dependencies[dep]}`
  294. }),
  295. publisher: manifest._npmUser && unparsePerson({
  296. name: color.yellow(manifest._npmUser.name),
  297. email: color.cyan(manifest._npmUser.email),
  298. }),
  299. modified: !packument.time ? undefined
  300. : color.yellow(relativeDate(packument.time[manifest.version])),
  301. maintainers: (packument.maintainers || []).map((u) => unparsePerson({
  302. name: color.yellow(u.name),
  303. email: color.cyan(u.email),
  304. })),
  305. repo: (
  306. manifest.bugs && (manifest.bugs.url || manifest.bugs)
  307. ) || (
  308. manifest.repository && (manifest.repository.url || manifest.repository)
  309. ),
  310. site: (
  311. manifest.homepage && (manifest.homepage.url || manifest.homepage)
  312. ),
  313. tags,
  314. tarball: color.cyan(manifest.dist.tarball),
  315. shasum: color.yellow(manifest.dist.shasum),
  316. integrity:
  317. manifest.dist.integrity && color.yellow(manifest.dist.integrity),
  318. fileCount:
  319. manifest.dist.fileCount && color.yellow(manifest.dist.fileCount),
  320. unpackedSize: unpackedSize && color.yellow(unpackedSize),
  321. }
  322. if (info.license.toLowerCase().trim() === 'proprietary')
  323. info.license = style.bright(color.red(info.license))
  324. else
  325. info.license = color.green(info.license)
  326. console.log('')
  327. console.log(
  328. style.underline(style.bright(`${info.name}@${info.version}`)) +
  329. ' | ' + info.license +
  330. ' | deps: ' + (info.deps.length ? color.cyan(info.deps.length) : color.green('none')) +
  331. ' | versions: ' + info.versions
  332. )
  333. info.description && console.log(info.description)
  334. if (info.repo || info.site)
  335. info.site && console.log(color.cyan(info.site))
  336. const warningSign = unicode ? ' ⚠️ ' : '!!'
  337. info.deprecated && console.log(
  338. `\n${style.bright(color.red('DEPRECATED'))}${
  339. warningSign
  340. } - ${info.deprecated}`
  341. )
  342. if (info.keywords.length) {
  343. console.log('')
  344. console.log('keywords:', info.keywords.join(', '))
  345. }
  346. if (info.bins.length) {
  347. console.log('')
  348. console.log('bin:', info.bins.join(', '))
  349. }
  350. console.log('')
  351. console.log('dist')
  352. console.log('.tarball:', info.tarball)
  353. console.log('.shasum:', info.shasum)
  354. info.integrity && console.log('.integrity:', info.integrity)
  355. info.unpackedSize && console.log('.unpackedSize:', info.unpackedSize)
  356. const maxDeps = 24
  357. if (info.deps.length) {
  358. console.log('')
  359. console.log('dependencies:')
  360. console.log(columns(info.deps.slice(0, maxDeps), { padding: 1 }))
  361. if (info.deps.length > maxDeps)
  362. console.log(`(...and ${info.deps.length - maxDeps} more.)`)
  363. }
  364. if (info.maintainers && info.maintainers.length) {
  365. console.log('')
  366. console.log('maintainers:')
  367. info.maintainers.forEach((u) => console.log('-', u))
  368. }
  369. console.log('')
  370. console.log('dist-tags:')
  371. console.log(columns(info.tags))
  372. if (info.publisher || info.modified) {
  373. let publishInfo = 'published'
  374. if (info.modified)
  375. publishInfo += ` ${info.modified}`
  376. if (info.publisher)
  377. publishInfo += ` by ${info.publisher}`
  378. console.log('')
  379. console.log(publishInfo)
  380. }
  381. }
  382. }
  383. module.exports = View
  384. function cleanBlanks (obj) {
  385. const clean = {}
  386. Object.keys(obj).forEach((version) => {
  387. clean[version] = obj[version]['']
  388. })
  389. return clean
  390. }
  391. // takes an array of objects and merges them into one object
  392. function reducer (acc, cur) {
  393. if (cur) {
  394. Object.keys(cur).forEach((v) => {
  395. acc[v] = acc[v] || {}
  396. Object.keys(cur[v]).forEach((t) => {
  397. acc[v][t] = cur[v][t]
  398. })
  399. })
  400. }
  401. return acc
  402. }
  403. // return whatever was printed
  404. function showFields (data, version, fields) {
  405. const o = {}
  406. ;[data, version].forEach((s) => {
  407. Object.keys(s).forEach((k) => {
  408. o[k] = s[k]
  409. })
  410. })
  411. const queryable = new Queryable(o)
  412. const s = queryable.query(fields)
  413. const res = { [version.version]: s }
  414. if (s)
  415. return res
  416. }
  417. function cleanup (data) {
  418. if (Array.isArray(data))
  419. return data.map(cleanup)
  420. if (!data || typeof data !== 'object')
  421. return data
  422. const keys = Object.keys(data)
  423. if (keys.length <= 3 &&
  424. data.name &&
  425. (keys.length === 1 ||
  426. (keys.length === 3 && data.email && data.url) ||
  427. (keys.length === 2 && (data.email || data.url))))
  428. data = unparsePerson(data)
  429. return data
  430. }
  431. function unparsePerson (d) {
  432. return d.name +
  433. (d.email ? ' <' + d.email + '>' : '') +
  434. (d.url ? ' (' + d.url + ')' : '')
  435. }