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