| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488 |
- const { readFile } = require('node:fs/promises')
- const jsonParse = require('json-parse-even-better-errors')
- const { log, output, META } = require('proc-log')
- const npa = require('npm-package-arg')
- const { resolve } = require('node:path')
- const formatBytes = require('../utils/format-bytes.js')
- const relativeDate = require('tiny-relative-date')
- const semver = require('semver')
- const { inspect } = require('node:util')
- const { packument } = require('pacote')
- const Queryable = require('../utils/queryable.js')
- const BaseCommand = require('../base-cmd.js')
- const { getError } = require('../utils/error-message.js')
- const { jsonError, outputError } = require('../utils/output-error.js')
- const readJson = file => readFile(file, 'utf8').then(jsonParse)
- class View extends BaseCommand {
- static description = 'View registry info'
- static name = 'view'
- static params = [
- 'json',
- 'workspace',
- 'workspaces',
- 'include-workspace-root',
- ]
- static workspaces = true
- static ignoreImplicitWorkspace = false
- static usage = ['[<package-spec>] [<field>[.subfield]...]']
- static async completion (opts, npm) {
- if (opts.conf.argv.remain.length <= 2) {
- // There used to be registry completion here, but it stopped making sense somewhere around 50,000 packages on the registry
- return
- }
- // have the package, get the fields
- const config = {
- ...npm.flatOptions,
- fullMetadata: true,
- preferOnline: true,
- _isRoot: true,
- }
- const spec = npa(opts.conf.argv.remain[2])
- const pckmnt = await packument(spec, config)
- const defaultTag = npm.config.get('tag')
- const dv = pckmnt.versions[pckmnt['dist-tags'][defaultTag]]
- pckmnt.versions = Object.keys(pckmnt.versions).sort(semver.compareLoose)
- return getCompletionFields(pckmnt).concat(getCompletionFields(dv))
- }
- async exec (args) {
- let { pkg, local, rest } = parseArgs(args)
- if (local) {
- if (this.npm.global) {
- throw new Error('Cannot use view command in global mode.')
- }
- const dir = this.npm.prefix
- const manifest = await readJson(resolve(dir, 'package.json'))
- if (!manifest.name) {
- throw new Error('Invalid package.json, no "name" field')
- }
- // put the version back if it existed
- pkg = `${manifest.name}${pkg.slice(1)}`
- }
- await this.#viewPackage(pkg, rest)
- }
- async execWorkspaces (args) {
- const { pkg, local, rest } = parseArgs(args)
- if (!local) {
- log.warn('Ignoring workspaces for specified package(s)')
- return this.exec([pkg, ...rest])
- }
- const json = this.npm.config.get('json')
- await this.setWorkspaces()
- for (const name of this.workspaceNames) {
- try {
- await this.#viewPackage(`${name}${pkg.slice(1)}`, rest, { workspace: true })
- } catch (e) {
- const err = getError(e, { npm: this.npm, command: this })
- if (err.code !== 'E404') {
- throw e
- }
- if (json) {
- output.buffer({ [META]: true, jsonError: { [name]: jsonError(err, this.npm) } })
- } else {
- outputError(err)
- }
- process.exitCode = err.exitCode
- }
- }
- }
- async #viewPackage (name, args, { workspace } = {}) {
- const wholePackument = !args.length
- const json = this.npm.config.get('json')
- // If we are viewing many packages and outputting individual fields then output the name before doing any async activity
- if (!json && !wholePackument && workspace) {
- output.standard(`${name}:`)
- }
- const [pckmnt, data] = await this.#getData(name, args, wholePackument)
- if (!json && wholePackument) {
- // pretty view (entire packument)
- for (const v of data) {
- output.standard(this.#prettyView(pckmnt, Object.values(v)[0][Queryable.ALL]))
- }
- return
- }
- const res = this.#packageOutput(cleanData(data, wholePackument), pckmnt._id)
- if (res) {
- if (json) {
- output.buffer(workspace ? { [name]: res } : res)
- } else {
- output.standard(res)
- }
- }
- }
- async #getData (pkg, args) {
- const spec = npa(pkg)
- const pckmnt = await packument(spec, {
- ...this.npm.flatOptions,
- preferOnline: true,
- fullMetadata: true,
- _isRoot: true,
- })
- // get the data about this package
- let version = this.npm.config.get('tag')
- // rawSpec is the git url if this is from git
- if (spec.type !== 'git' && spec.type !== 'directory' && spec.rawSpec !== '*') {
- version = spec.rawSpec
- }
- if (pckmnt['dist-tags']?.[version]) {
- version = pckmnt['dist-tags'][version]
- }
- if (pckmnt.time?.unpublished) {
- const u = pckmnt.time.unpublished
- throw Object.assign(new Error(`Unpublished on ${u.time}`), {
- statusCode: 404,
- code: 'E404',
- pkgid: pckmnt._id,
- })
- }
- const versions = pckmnt.versions || {}
- pckmnt.versions = Object.keys(versions).filter(v => {
- if (semver.valid(v)) {
- return true
- }
- log.info('view', `Ignoring invalid version: ${v}`)
- return false
- }).sort(semver.compareLoose)
- // remove readme unless we asked for it
- if (args.indexOf('readme') === -1) {
- delete pckmnt.readme
- }
- const data = Object.entries(versions)
- .filter(([v]) => semver.satisfies(v, version, true))
- .flatMap(([, v]) => {
- // remove readme unless we asked for it
- if (args.indexOf('readme') !== -1) {
- delete v.readme
- }
- return showFields({
- data: pckmnt,
- version: v,
- fields: args,
- json: this.npm.config.get('json'),
- })
- })
- // No data has been pushed because no data is matching the specified version
- if (!data.length && version !== 'latest') {
- throw Object.assign(new Error(`No match found for version ${version}`), {
- statusCode: 404,
- code: 'E404',
- pkgid: `${pckmnt._id}@${version}`,
- })
- }
- return [pckmnt, data]
- }
- #packageOutput (data, name) {
- const json = this.npm.config.get('json')
- const versions = Object.keys(data)
- const includeVersions = versions.length > 1
- let includeFields
- const res = versions.flatMap((v) => {
- const fields = Object.entries(data[v])
- includeFields ||= (fields.length > 1)
- const msg = json ? {} : []
- for (let [f, d] of fields) {
- d = cleanup(d)
- if (json) {
- msg[f] = d
- continue
- }
- if (includeVersions || includeFields || typeof d !== 'string') {
- d = inspect(d, {
- showHidden: false,
- depth: 5,
- colors: this.npm.color,
- maxArrayLength: null,
- })
- }
- if (f && includeFields) {
- f += ' = '
- }
- msg.push(`${includeVersions ? `${name}@${v} ` : ''}${includeFields ? f : ''}${d}`)
- }
- return msg
- })
- if (json) {
- // TODO(BREAKING_CHANGE): all unwrapping should be removed.
- // Users should know based on their arguments if they can expect an array or an object.
- // And this unwrapping can break that assumption.
- // e.g. `npm view abbrev@^2` should always return an array, but currently since there is only one version matching `^2` this will return a single object instead.
- const first = Object.keys(res[0] || {})
- const jsonRes = first.length === 1 ? res.map(m => m[first[0]]) : res
- if (jsonRes.length === 0) {
- return
- }
- if (jsonRes.length === 1) {
- return jsonRes[0]
- }
- return jsonRes
- }
- return res.join('\n').trim()
- }
- #prettyView (packu, manifest) {
- // More modern, pretty printing of default view
- const unicode = this.npm.config.get('unicode')
- const chalk = this.npm.chalk
- const deps = Object.entries(manifest.dependencies || {}).map(([k, dep]) =>
- `${chalk.blue(k)}: ${dep}`
- )
- // Sort dist-tags by publish time when available, then by tag name, keeping `latest` at the top of the list.
- const distTags = Object.entries(packu['dist-tags'])
- .sort(([aTag, aVer], [bTag, bVer]) => {
- const timeMap = packu.time || {}
- const aTime = aTag === 'latest' ? Infinity : Date.parse(timeMap[aVer] || 0)
- const bTime = bTag === 'latest' ? Infinity : Date.parse(timeMap[bVer] || 0)
- if (aTime === bTime) {
- return aTag > bTag ? -1 : 1
- }
- return aTime > bTime ? -1 : 1
- })
- .map(([k, t]) => `${chalk.blue(k)}: ${t}`)
- const site = manifest.homepage?.url || manifest.homepage
- const bins = Object.keys(manifest.bin || {})
- const licenseField = manifest.license || 'Proprietary'
- const license = typeof licenseField === 'string'
- ? licenseField
- : (licenseField.type || 'Proprietary')
- const res = []
- res.push('')
- res.push([
- chalk.underline.cyan(`${manifest.name}@${manifest.version}`),
- license.toLowerCase().trim() === 'proprietary'
- ? chalk.red(license)
- : chalk.green(license),
- `deps: ${deps.length ? chalk.cyan(deps.length) : chalk.cyan('none')}`,
- `versions: ${chalk.cyan(packu.versions.length + '')}`,
- ].join(' | '))
- manifest.description && res.push(manifest.description)
- if (site) {
- res.push(chalk.blue(site))
- }
- manifest.deprecated && res.push(
- `\n${chalk.redBright('DEPRECATED')}${unicode ? ' ⚠️ ' : '!!'} - ${manifest.deprecated}`
- )
- if (packu.keywords?.length) {
- res.push(`\nkeywords: ${
- packu.keywords.map(k => chalk.cyan(k)).join(', ')
- }`)
- }
- if (bins.length) {
- res.push(`\nbin: ${chalk.cyan(bins.join(', '))}`)
- }
- res.push('\ndist')
- res.push(`.tarball: ${chalk.blue(manifest.dist.tarball)}`)
- res.push(`.shasum: ${chalk.green(manifest.dist.shasum)}`)
- if (manifest.dist.integrity) {
- res.push(`.integrity: ${chalk.green(manifest.dist.integrity)}`)
- }
- if (manifest.dist.unpackedSize) {
- res.push(`.unpackedSize: ${chalk.blue(formatBytes(manifest.dist.unpackedSize, true))}`)
- }
- if (deps.length) {
- const maxDeps = 24
- res.push('\ndependencies:')
- res.push(deps.slice(0, maxDeps).join(', '))
- if (deps.length > maxDeps) {
- res.push(chalk.dim(`(...and ${deps.length - maxDeps} more.)`))
- }
- }
- if (packu.maintainers?.length) {
- res.push('\nmaintainers:')
- packu.maintainers.forEach(u =>
- res.push(`- ${unparsePerson({
- name: chalk.blue(u.name),
- email: chalk.dim(u.email) })}`)
- )
- }
- res.push('\ndist-tags:')
- const maxTags = 5
- res.push(distTags.slice(0, maxTags).join('\n'))
- if (distTags.length > maxTags) {
- res.push(chalk.dim(`(...and ${distTags.length - maxTags} more.)`))
- }
- const publisher = manifest._npmUser && unparsePerson({
- name: chalk.blue(manifest._npmUser.name),
- email: chalk.dim(manifest._npmUser.email),
- })
- if (publisher || packu.time) {
- let publishInfo = 'published'
- if (packu.time?.[manifest.version]) {
- publishInfo += ` ${chalk.cyan(relativeDate(packu.time[manifest.version]))}`
- }
- if (publisher) {
- publishInfo += ` by ${publisher}`
- }
- res.push('')
- res.push(publishInfo)
- }
- return res.join('\n')
- }
- }
- module.exports = View
- function parseArgs (args) {
- if (!args.length) {
- args = ['.']
- }
- const pkg = args.shift()
- return {
- pkg,
- local: /^\.@/.test(pkg) || pkg === '.',
- rest: args,
- }
- }
- function cleanData (obj, wholePackument) {
- // JSON formatted output (JSON or specific attributes from packument)
- const data = obj.reduce((acc, cur) => {
- if (cur) {
- Object.entries(cur).forEach(([k, v]) => {
- acc[k] ||= {}
- Object.keys(v).forEach((t) => {
- acc[k][t] = cur[k][t]
- })
- })
- }
- return acc
- }, {})
- if (wholePackument) {
- const cleaned = Object.entries(data).reduce((acc, [k, v]) => {
- acc[k] = v[Queryable.ALL]
- return acc
- }, {})
- log.silly('view', cleaned)
- return cleaned
- }
- return data
- }
- // return whatever was printed
- function showFields ({ data, version, fields, json }) {
- const o = [data, version].reduce((acc, s) => {
- Object.entries(s).forEach(([k, v]) => {
- acc[k] = v
- })
- return acc
- }, {})
- const queryable = new Queryable(o)
- if (!fields.length) {
- return { [version.version]: queryable.query(Queryable.ALL) }
- }
- return fields.map((field) => {
- const s = queryable.query(field, { unwrapSingleItemArrays: !json })
- if (s) {
- return { [version.version]: s }
- }
- })
- }
- function cleanup (data) {
- if (Array.isArray(data)) {
- return data.map(cleanup)
- }
- if (!data || typeof data !== 'object') {
- return data
- }
- const keys = Object.keys(data)
- if (keys.length <= 3 && data.name && (
- (keys.length === 1) ||
- (keys.length === 3 && data.email && data.url) ||
- (keys.length === 2 && (data.email || data.url)) ||
- data.trustedPublisher
- )) {
- data = unparsePerson(data)
- }
- return data
- }
- const unparsePerson = (d) =>
- `${d.name}${d.email ? ` <${d.email}>` : ''}${d.url ? ` (${d.url})` : ''}`
- function getCompletionFields (d, f = [], pref = []) {
- Object.entries(d).forEach(([k, v]) => {
- if (k.charAt(0) === '_' || k.indexOf('.') !== -1) {
- return
- }
- const p = pref.concat(k).join('.')
- f.push(p)
- if (Array.isArray(v)) {
- v.forEach((val, i) => {
- const pi = p + '[' + i + ']'
- if (val && typeof val === 'object') {
- getCompletionFields(val, f, [p])
- } else {
- f.push(pi)
- }
- })
- return
- }
- if (typeof v === 'object') {
- getCompletionFields(v, f, [p])
- }
- })
- return f
- }
|