diff --git a/lib/view.js b/lib/view.js index 34a4a65696501..2c89eb9ae1caa 100644 --- a/lib/view.js +++ b/lib/view.js @@ -16,30 +16,38 @@ const readJson = promisify(require('read-package-json')) const relativeDate = require('tiny-relative-date') const semver = require('semver') const style = require('ansistyles') -const usage = require('./utils/usage') +const usageUtil = require('./utils/usage') -const validateName = require('validate-npm-package-name') - -view.usage = usage( +const usage = usageUtil( 'view', 'npm view [<@scope>/][@] [[.subfield]...]' ) -view.completion = function (opts, cb) { +const cmd = (args, silent, cb) => { + if (typeof cb !== 'function') { + cb = silent + silent = false + } + view(args, silent, cb) + .then(() => cb()) + .catch(cb) +} + +const completion = async (opts, cb) => { if (opts.conf.argv.remain.length <= 2) { - // FIXME: there used to be registry completion here, but it stopped making - // sense somewhere around 50,000 packages on the registry + // There used to be registry completion here, but it stopped + // making sense somewhere around 50,000 packages on the registry return cb() } - // have the package, get the fields. + // have the package, get the fields const config = npm.flatOptions const { defaultTag } = config const spec = npa(opts.conf.argv.remain[2]) - return packument(spec, config).then(d => { - const dv = d.versions[d['dist-tags'][defaultTag]] - d.versions = Object.keys(d.versions).sort(semver.compareLoose) - return getFields(d).concat(getFields(dv)) - }).then(() => cb(), cb) + const pckmnt = await packument(spec, config) + const dv = pckmnt.versions[pckmnt['dist-tags'][defaultTag]] + pckmnt.versions = Object.keys(pckmnt.versions).sort(semver.compareLoose) + const fields = getFields(pckmnt).concat(getFields(dv)) + return cb(null, fields) function getFields (d, f, pref) { f = f || [] @@ -63,12 +71,7 @@ view.completion = function (opts, cb) { } } -function view (args, silent, cb) { - if (typeof cb !== 'function') { - cb = silent - silent = false - } - +async function view (args, silent) { if (!args.length) args = ['.'] const opts = npm.flatOptions @@ -83,246 +86,213 @@ function view (args, silent, cb) { const local = (name === '.' || !name) if (opts.global && local) { - return cb(new Error('Cannot use view command in global mode.')) + throw new Error('Cannot use view command in global mode.') } if (local) { const dir = npm.prefix - readJson(path.resolve(dir, 'package.json')).then(d => { - if (!d || !d.name) return cb(new Error('Invalid package.json')) - - const p = d.name - nv = npa(p) - if (pkg && ~pkg.indexOf('@')) { - nv.rawSpec = pkg.split('@')[pkg.indexOf('@')] - } - - fetchAndRead(nv, args, silent, opts, cb) - }).catch(er => cb(er)) + const manifest = await readJson(path.resolve(dir, 'package.json')) + if (!manifest || !manifest.name) throw new Error('Invalid package.json') + const p = manifest.name + nv = npa(p) + if (pkg && ~pkg.indexOf('@')) { + nv.rawSpec = pkg.split('@')[pkg.indexOf('@')] + } + await fetchAndRead(nv, args, silent, opts) } else { - fetchAndRead(nv, args, silent, opts, cb) + await fetchAndRead(nv, args, silent, opts) } } -function fetchAndRead (nv, args, silent, opts, cb) { +async function fetchAndRead (nv, args, silent, opts) { // get the data about this package let version = nv.rawSpec || npm.flatOptions.defaultTag - return packument(nv, { + const pckmnt = await packument(nv, { ...opts, fullMetadata: true, 'prefer-online': true - }).catch(err => { - // TODO - this should probably go into pacote, but the tests expect it. - if (err.code === 'E404') { - err.message = `'${nv.name}' is not in the npm registry.` - const validated = validateName(nv.name) - if (!validated.validForNewPackages) { - err.message += '\n' - err.message += (validated.errors || []).join('\n') - err.message += (validated.warnings || []).join('\n') - } else { - err.message += '\nYou should bug the author to publish it' - err.message += '\n(or use the name yourself!)' - err.message += '\n' - err.message += '\nNote that you can also install from a' - err.message += '\ntarball, folder, http url, or git url.' - } - } - throw err - }).then(data => { - if (data['dist-tags'] && data['dist-tags'][version]) { - version = data['dist-tags'][version] - } + }) - if (data.time && data.time.unpublished) { - const u = data.time.unpublished - const er = new Error('Unpublished by ' + u.name + ' on ' + u.time) - er.statusCode = 404 - er.code = 'E404' - er.pkgid = data._id - throw er - } + if (pckmnt['dist-tags'] && pckmnt['dist-tags'][version]) { + version = pckmnt['dist-tags'][version] + } - const results = [] - const error = null - const versions = data.versions || {} - data.versions = Object.keys(versions).sort(semver.compareLoose) - if (!args.length) args = [''] + if (pckmnt.time && pckmnt.time.unpublished) { + const u = pckmnt.time.unpublished + const er = new Error('Unpublished by ' + u.name + ' on ' + u.time) + er.statusCode = 404 + er.code = 'E404' + er.pkgid = pckmnt._id + throw er + } - // remove readme unless we asked for it - if (args.indexOf('readme') === -1) { - delete data.readme - } + const results = [] + const versions = pckmnt.versions || {} + pckmnt.versions = Object.keys(versions).sort(semver.compareLoose) + if (!args.length) args = [''] - Object.keys(versions).forEach(function (v) { - if (semver.satisfies(v, version, true)) { - args.forEach(function (args) { - // remove readme unless we asked for it - if (args.indexOf('readme') !== -1) { - delete versions[v].readme - } - results.push(showFields(data, versions[v], args)) - }) - } - }) - let retval = results.reduce(reducer, {}) + // remove readme unless we asked for it + if (args.indexOf('readme') === -1) { + delete pckmnt.readme + } - if (args.length === 1 && args[0] === '') { - retval = cleanBlanks(retval) - log.silly('view', retval) + Object.keys(versions).forEach(function (v) { + if (semver.satisfies(v, version, true)) { + args.forEach(args => { + // remove readme unless we asked for it + if (args.indexOf('readme') !== -1) { + delete versions[v].readme + } + results.push(showFields(pckmnt, versions[v], args)) + }) } + }) + let retval = results.reduce(reducer, {}) - if (silent) { - return retval - } else if (error) { - throw error - } else if ( - !opts.json && - args.length === 1 && - args[0] === '' - ) { - data.version = version - return Promise.all( - results.map((v) => prettyView(data, v[Object.keys(v)[0]][''], opts)) - ).then(() => retval) - } else { - return new Promise((resolve, reject) => { - printData(retval, data._id, opts, er => er ? reject(er) : resolve()) - }).then(() => retval) - } - }).then(() => cb(), cb) + if (args.length === 1 && args[0] === '') { + retval = cleanBlanks(retval) + log.silly('view', retval) + } + + if (silent) { + return retval + } else if ( + !opts.json && + args.length === 1 && + args[0] === '' + ) { + // general view + pckmnt.version = version + await Promise.all( + results.map((v) => prettyView(pckmnt, v[Object.keys(v)[0]][''], opts)) + ) + return retval + } else { + // view by field name + await printData(retval, pckmnt._id, opts) + return retval + } } -function prettyView (packument, manifest, opts) { +async function prettyView (packument, manifest, opts) { // More modern, pretty printing of default view const unicode = opts.unicode - return Promise.resolve().then(() => { - if (!manifest) { - log.error( - 'view', - 'No matching versions.\n' + - 'To see a list of versions, run:\n' + - `> npm view ${packument.name} versions` - ) - return - } - const tags = [] - Object.keys(packument['dist-tags']).forEach((t) => { - const version = packument['dist-tags'][t] - tags.push(`${style.bright(color.green(t))}: ${version}`) - }) - const unpackedSize = manifest.dist.unpackedSize && - byteSize(manifest.dist.unpackedSize) - const licenseField = manifest.license || manifest.licence || 'Proprietary' - const info = { - name: color.green(manifest.name), - version: color.green(manifest.version), - bins: Object.keys(manifest.bin || {}).map(color.yellow), - versions: color.yellow(packument.versions.length + ''), - description: manifest.description, - deprecated: manifest.deprecated, - keywords: (packument.keywords || []).map(color.yellow), - license: typeof licenseField === 'string' - ? licenseField - : (licenseField.type || 'Proprietary'), - deps: Object.keys(manifest.dependencies || {}).map((dep) => { - return `${color.yellow(dep)}: ${manifest.dependencies[dep]}` - }), - publisher: manifest._npmUser && unparsePerson({ - name: color.yellow(manifest._npmUser.name), - email: color.cyan(manifest._npmUser.email) - }), - modified: packument.time ? color.yellow(relativeDate(packument.time[packument.version])) : undefined, - maintainers: (packument.maintainers || []).map((u) => unparsePerson({ - name: color.yellow(u.name), - email: color.cyan(u.email) - })), - repo: ( - manifest.bugs && (manifest.bugs.url || manifest.bugs) - ) || ( - manifest.repository && (manifest.repository.url || manifest.repository) - ), - site: ( - manifest.homepage && (manifest.homepage.url || manifest.homepage) - ), - stars: color.yellow('' + packument.users ? Object.keys(packument.users || {}).length : 0), - tags, - tarball: color.cyan(manifest.dist.tarball), - shasum: color.yellow(manifest.dist.shasum), - integrity: manifest.dist.integrity && color.yellow(manifest.dist.integrity), - fileCount: manifest.dist.fileCount && color.yellow(manifest.dist.fileCount), - unpackedSize: unpackedSize && color.yellow(unpackedSize.value) + ' ' + unpackedSize.unit - } - if (info.license.toLowerCase().trim() === 'proprietary') { - info.license = style.bright(color.red(info.license)) - } else { - info.license = color.green(info.license) - } + const tags = [] + + Object.keys(packument['dist-tags']).forEach((t) => { + const version = packument['dist-tags'][t] + tags.push(`${style.bright(color.green(t))}: ${version}`) + }) + const unpackedSize = manifest.dist.unpackedSize && + byteSize(manifest.dist.unpackedSize) + const licenseField = manifest.license || 'Proprietary' + const info = { + name: color.green(manifest.name), + version: color.green(manifest.version), + bins: Object.keys(manifest.bin || {}).map(color.yellow), + versions: color.yellow(packument.versions.length + ''), + description: manifest.description, + deprecated: manifest.deprecated, + keywords: (packument.keywords || []).map(color.yellow), + license: typeof licenseField === 'string' + ? licenseField + : (licenseField.type || 'Proprietary'), + deps: Object.keys(manifest.dependencies || {}).map((dep) => { + return `${color.yellow(dep)}: ${manifest.dependencies[dep]}` + }), + publisher: manifest._npmUser && unparsePerson({ + name: color.yellow(manifest._npmUser.name), + email: color.cyan(manifest._npmUser.email) + }), + modified: packument.time ? color.yellow(relativeDate(packument.time[packument.version])) : undefined, + maintainers: (packument.maintainers || []).map((u) => unparsePerson({ + name: color.yellow(u.name), + email: color.cyan(u.email) + })), + repo: ( + manifest.bugs && (manifest.bugs.url || manifest.bugs) + ) || ( + manifest.repository && (manifest.repository.url || manifest.repository) + ), + site: ( + manifest.homepage && (manifest.homepage.url || manifest.homepage) + ), + tags, + tarball: color.cyan(manifest.dist.tarball), + shasum: color.yellow(manifest.dist.shasum), + integrity: manifest.dist.integrity && color.yellow(manifest.dist.integrity), + fileCount: manifest.dist.fileCount && color.yellow(manifest.dist.fileCount), + unpackedSize: unpackedSize && color.yellow(unpackedSize.value) + ' ' + unpackedSize.unit + } + if (info.license.toLowerCase().trim() === 'proprietary') { + info.license = style.bright(color.red(info.license)) + } else { + info.license = color.green(info.license) + } + console.log('') + console.log( + style.underline(style.bright(`${info.name}@${info.version}`)) + + ' | ' + info.license + + ' | deps: ' + (info.deps.length ? color.cyan(info.deps.length) : color.green('none')) + + ' | versions: ' + info.versions + ) + info.description && console.log(info.description) + if (info.repo || info.site) { + info.site && console.log(color.cyan(info.site)) + } + + const warningSign = unicode ? ' ⚠️ ' : '!!' + info.deprecated && console.log( + `\n${style.bright(color.red('DEPRECATED'))}${ + warningSign + } - ${info.deprecated}` + ) + + if (info.keywords.length) { console.log('') - console.log( - style.underline(style.bright(`${info.name}@${info.version}`)) + - ' | ' + info.license + - ' | deps: ' + (info.deps.length ? color.cyan(info.deps.length) : color.green('none')) + - ' | versions: ' + info.versions - ) - info.description && console.log(info.description) - if (info.repo || info.site) { - info.site && console.log(color.cyan(info.site)) - } + console.log('keywords:', info.keywords.join(', ')) + } - const warningSign = unicode ? ' ⚠️ ' : '!!' - info.deprecated && console.log( - `\n${style.bright(color.red('DEPRECATED'))}${ - warningSign - } - ${info.deprecated}` - ) + if (info.bins.length) { + console.log('') + console.log('bin:', info.bins.join(', ')) + } - if (info.keywords.length) { - console.log('') - console.log('keywords:', info.keywords.join(', ')) - } + console.log('') + console.log('dist') + console.log('.tarball:', info.tarball) + console.log('.shasum:', info.shasum) + info.integrity && console.log('.integrity:', info.integrity) + info.unpackedSize && console.log('.unpackedSize:', info.unpackedSize) - if (info.bins.length) { - console.log('') - console.log('bin:', info.bins.join(', ')) + const maxDeps = 24 + if (info.deps.length) { + console.log('') + console.log('dependencies:') + console.log(columns(info.deps.slice(0, maxDeps), { padding: 1 })) + if (info.deps.length > maxDeps) { + console.log(`(...and ${info.deps.length - maxDeps} more.)`) } + } + if (info.maintainers && info.maintainers.length) { console.log('') - console.log('dist') - console.log('.tarball:', info.tarball) - console.log('.shasum:', info.shasum) - info.integrity && console.log('.integrity:', info.integrity) - info.unpackedSize && console.log('.unpackedSize:', info.unpackedSize) - - const maxDeps = 24 - if (info.deps.length) { - console.log('') - console.log('dependencies:') - console.log(columns(info.deps.slice(0, maxDeps), { padding: 1 })) - if (info.deps.length > maxDeps) { - console.log(`(...and ${info.deps.length - maxDeps} more.)`) - } - } + console.log('maintainers:') + info.maintainers.forEach((u) => console.log('-', u)) + } - if (info.maintainers && info.maintainers.length) { - console.log('') - console.log('maintainers:') - info.maintainers.forEach((u) => console.log('-', u)) - } + console.log('') + console.log('dist-tags:') + console.log(columns(info.tags)) + if (info.publisher || info.modified) { + let publishInfo = 'published' + if (info.modified) { publishInfo += ` ${info.modified}` } + if (info.publisher) { publishInfo += ` by ${info.publisher}` } console.log('') - console.log('dist-tags:') - console.log(columns(info.tags)) - - if (info.publisher || info.modified) { - let publishInfo = 'published' - if (info.modified) { publishInfo += ` ${info.modified}` } - if (info.publisher) { publishInfo += ` by ${info.publisher}` } - console.log('') - console.log(publishInfo) - } - }) + console.log(publishInfo) + } } function cleanBlanks (obj) { @@ -357,8 +327,6 @@ function showFields (data, version, fields) { return search(o, fields.split('.'), version.version, fields) } -const hasOwnProperty = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key) - function search (data, fields, version, title) { let field const tail = fields @@ -375,7 +343,7 @@ function search (data, fields, version, title) { if (index) { field = index[1] index = index[2] - if (data.field && hasOwnProperty(data.field, index)) { + if (data[field] && data[field][index]) { return search(data[field][index], tail, version, title) } else { field = field + '[' + index + ']' @@ -395,15 +363,11 @@ function search (data, fields, version, title) { results = results.reduce(reducer, {}) return results } - if (!hasOwnProperty(data, field)) return undefined + if (!data[field]) return undefined data = data[field] if (tail.length) { - if (typeof data === 'object') { - // there are more fields to deal with. - return search(data, tail, version, title) - } else { - return new Error('Not an object: ' + data) - } + // there are more fields to deal with. + return search(data, tail, version, title) } o = {} o[version] = {} @@ -411,7 +375,7 @@ function search (data, fields, version, title) { return o } -function printData (data, name, opts, cb) { +async function printData (data, name, opts) { const versions = Object.keys(data) let msg = '' let msgJson = [] @@ -438,7 +402,6 @@ function printData (data, name, opts, cb) { } if (!opts.json) { if (f && includeFields) f += ' = ' - if (d.indexOf('\n') !== -1) d = ' \n' + d msg += (includeVersions ? name + '@' + v + ' ' : '') + (includeFields ? f : '') + d + '\n' } @@ -450,43 +413,24 @@ function printData (data, name, opts, cb) { const k = Object.keys(msgJson[0])[0] msgJson = msgJson.map(function (m) { return m[k] }) } - - if (msgJson.length === 1) { - msg = JSON.stringify(msgJson[0], null, 2) + '\n' - } else if (msgJson.length > 1) { - msg = JSON.stringify(msgJson, null, 2) + '\n' - } + msg = JSON.stringify(msgJson[0], null, 2) + '\n' } - // preserve output symmetry by adding a whitespace-only line at the end if - // there's one at the beginning - if (/^\s*\n/.test(msg)) msg += '\n' - // disable the progress bar entirely, as we can't meaningfully update it if // we may have partial lines printed. log.disableProgress() // print directly to stdout to not unnecessarily add blank lines - process.stdout.write(msg, () => cb(null, data)) + console.log(msg.trim()) } + function cleanup (data) { if (Array.isArray(data)) { return data.map(cleanup) } if (!data || typeof data !== 'object') return data - if (typeof data.versions === 'object' && - data.versions && - !Array.isArray(data.versions)) { - data.versions = Object.keys(data.versions || {}) - } - - let keys = Object.keys(data) - keys.forEach(function (d) { - if (d.charAt(0) === '_') delete data[d] - else if (typeof data[d] === 'object') data[d] = cleanup(data[d]) - }) - keys = Object.keys(data) + const keys = Object.keys(data) if (keys.length <= 3 && data.name && (keys.length === 1 || @@ -497,8 +441,9 @@ function cleanup (data) { return data } function unparsePerson (d) { - if (typeof d === 'string') return d return d.name + (d.email ? ' <' + d.email + '>' : '') + (d.url ? ' (' + d.url + ')' : '') } + +module.exports = Object.assign(cmd, { completion, usage }) diff --git a/tap-snapshots/test-lib-view.js-TAP.test.js b/tap-snapshots/test-lib-view.js-TAP.test.js new file mode 100644 index 0000000000000..473ad227d1c5a --- /dev/null +++ b/tap-snapshots/test-lib-view.js-TAP.test.js @@ -0,0 +1,290 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/lib/view.js TAP should log info by field name array field - 1 element > must match snapshot 1`] = ` + +claudia +` + +exports[`test/lib/view.js TAP should log info by field name array field - 2 elements > must match snapshot 1`] = ` + +maintainers[0].name = 'claudia' +maintainers[1].name = 'isaacs' +` + +exports[`test/lib/view.js TAP should log info by field name maintainers with email > must match snapshot 1`] = ` + +{ + "maintainers": [ + { + "name": "claudia", + "email": "c@yellow.com", + "twitter": "cyellow" + }, + { + "name": "isaacs", + "email": "i@yellow.com", + "twitter": "iyellow" + } + ], + "name": "yellow" +} +` + +exports[`test/lib/view.js TAP should log info by field name maintainers with url > must match snapshot 1`] = ` + +[ + "claudia (http://c.yellow.com)", + "isaacs (http://i.yellow.com)" +] +` + +exports[`test/lib/view.js TAP should log info by field name nested field with brackets > must match snapshot 1`] = ` + +"Jeremy Ashkenas" +` + +exports[`test/lib/view.js TAP should log info by field name readme > must match snapshot 1`] = ` + + +` + +exports[`test/lib/view.js TAP should log info by field name several fields > must match snapshot 1`] = ` + +{ + "name": "underscore", + "version": "1.3.1" +} +` + +exports[`test/lib/view.js TAP should log info by field name several fields with several versions > must match snapshot 1`] = ` + +underscore@1.0.3 'Jeremy Ashkenas ' +underscore@1.0.4 'Jeremy Ashkenas ' +underscore@1.1.0 'Jeremy Ashkenas ' +underscore@1.1.1 'Jeremy Ashkenas ' +underscore@1.1.2 'Jeremy Ashkenas ' +underscore@1.1.3 'Jeremy Ashkenas ' +underscore@1.1.4 'Jeremy Ashkenas ' +underscore@1.1.5 'Jeremy Ashkenas ' +underscore@1.1.6 'Jeremy Ashkenas ' +underscore@1.1.7 'Jeremy Ashkenas ' +underscore@1.2.0 'Jeremy Ashkenas ' +underscore@1.2.1 'Jeremy Ashkenas ' +underscore@1.2.2 'Jeremy Ashkenas ' +underscore@1.2.3 'Jeremy Ashkenas ' +underscore@1.2.4 'Jeremy Ashkenas ' +underscore@1.3.0 'Jeremy Ashkenas ' +underscore@1.3.1 'Jeremy Ashkenas ' +underscore@1.3.2 'Jeremy Ashkenas ' +underscore@1.3.3 'Jeremy Ashkenas ' +underscore@1.4.0 'Jeremy Ashkenas ' +underscore@1.4.1 'Jeremy Ashkenas ' +underscore@1.4.2 'Jeremy Ashkenas ' +underscore@1.4.3 'Jeremy Ashkenas ' +underscore@1.4.4 'Jeremy Ashkenas ' +underscore@1.5.0 'Jeremy Ashkenas ' +underscore@1.5.1 'Jeremy Ashkenas ' +` + +exports[`test/lib/view.js TAP should log info by field name unknown nested field > must match snapshot 1`] = ` + + +` + +exports[`test/lib/view.js TAP should log info of package in current working dir non-specific version > must match snapshot 1`] = ` + + +underscore@1.3.1 | Proprietary | deps: none | versions: 26 +JavaScript's functional programming helper library. +http://documentcloud.github.com/underscore/ + +dist +.tarball:http://localhost:1331/underscore/-/underscore-1.3.1.tgz +.shasum:6cb8aad0e77eb5dbbfb54b22bcd8697309cf9641 + +maintainers: +-jashkenas <jashkenas@gmail.com> + +dist-tags: +latest: 1.5.1 +stable: 1.5.1 + +published over a year ago by jashkenas <jashkenas@gmail.com> +` + +exports[`test/lib/view.js TAP should log info of package in current working dir specific version > must match snapshot 1`] = ` + + +underscore@1.3.1 | Proprietary | deps: none | versions: 26 +JavaScript's functional programming helper library. +http://documentcloud.github.com/underscore/ + +dist +.tarball:http://localhost:1331/underscore/-/underscore-1.3.1.tgz +.shasum:6cb8aad0e77eb5dbbfb54b22bcd8697309cf9641 + +maintainers: +-jashkenas <jashkenas@gmail.com> + +dist-tags: +latest: 1.5.1 +stable: 1.5.1 + +published over a year ago by jashkenas <jashkenas@gmail.com> +` + +exports[`test/lib/view.js TAP should log package info mkdirp@0.3.5 > must match snapshot 1`] = ` + + +mkdirp@0.3.5 | MIT | deps: none | versions: 17 +Recursively mkdir, like \`mkdir -p\` + +dist +.tarball:http://localhost:1331/mkdirp/-/mkdirp-0.3.5.tgz +.shasum:de3e5f8961c88c787ee1368df849ac4413eca8d7 + +maintainers: +-substack <mail@substack.net> + +dist-tags: +latest: 0.3.5 + +published over a year ago by substack <mail@substack.net> +` + +exports[`test/lib/view.js TAP should log package info package with homepage > must match snapshot 1`] = ` + + +orange@undefined | Proprietary | deps: none | versions: 2 +http://hm.orange.com + +dist +.tarball:undefined +.shasum:undefined +.integrity:--- +.unpackedSize:1 B + +dist-tags: +1.0.1: [object Object] +` + +exports[`test/lib/view.js TAP should log package info package with license, bugs, repository and other fields > must match snapshot 1`] = ` + + +green@undefined | ACME | deps: 2 | versions: 2 + +DEPRECATED ⚠️ - true + +keywords:colors, green, crayola + +bin:green + +dist +.tarball:undefined +.shasum:undefined +.integrity:--- +.unpackedSize:1 B + +dependencies: +red: [object Object] +yellow: [object Object] + +dist-tags: +1.0.1: [object Object] +` + +exports[`test/lib/view.js TAP should log package info package with maintainers info as object > must match snapshot 1`] = ` + + +pink@undefined | Proprietary | deps: none | versions: 2 + +dist +.tarball:undefined +.shasum:undefined +.integrity:--- +.unpackedSize:1 B + +dist-tags: +1.0.1: [object Object] +` + +exports[`test/lib/view.js TAP should log package info package with more than 25 deps > must match snapshot 1`] = ` + + +black@undefined | Proprietary | deps: 25 | versions: 2 + +dist +.tarball:undefined +.shasum:undefined +.integrity:--- +.unpackedSize:1 B + +dependencies: +0: [object Object] +10: [object Object] +11: [object Object] +12: [object Object] +13: [object Object] +14: [object Object] +15: [object Object] +16: [object Object] +17: [object Object] +18: [object Object] +19: [object Object] +1: [object Object] +20: [object Object] +21: [object Object] +22: [object Object] +23: [object Object] +2: [object Object] +3: [object Object] +4: [object Object] +5: [object Object] +6: [object Object] +7: [object Object] +8: [object Object] +9: [object Object] +(...and 1 more.) + +dist-tags: +1.0.1: [object Object] +` + +exports[`test/lib/view.js TAP should log package info package with no modified time > must match snapshot 1`] = ` + + +cyan@undefined | Proprietary | deps: none | versions: 2 + +dist +.tarball:undefined +.shasum:undefined + +dist-tags: + + +published by claudia <claudia@cyan.com> +` + +exports[`test/lib/view.js TAP should log package info package with no repo or homepage > must match snapshot 1`] = ` + + +blue@undefined | Proprietary | deps: none | versions: 2 + +dist +.tarball:undefined +.shasum:undefined + +dist-tags: + + +published 12 months ago +` + +exports[`test/lib/view.js TAP should log package info package with no versions > must match snapshot 1`] = ` + +` diff --git a/test/lib/view.js b/test/lib/view.js new file mode 100644 index 0000000000000..bd0bc4d37ae98 --- /dev/null +++ b/test/lib/view.js @@ -0,0 +1,492 @@ +const t = require('tap') +const mr = require('npm-registry-mock') +const requireInject = require('require-inject') + +const REG = 'http://localhost:1331' + +let logs +let server +const cleanLogs = (done) => { + logs = '' + const fn = (...args) => { + logs += '\n' + args.map(el => logs += el) + } + console.log = fn + done() +} + +t.beforeEach(cleanLogs) +t.test('setup', (t) => { + var mocks = { + 'get': { + '/red': [200, { + 'name' : 'red', + 'dist-tags': { + '1.0.1': {} + }, + 'time': { + 'unpublished': new Date() + } + }], + '/blue': [200, { + 'name': 'blue', + 'dist-tags': {}, + 'time': { + '1.0.0': '2019-08-06T16:21:09.842Z' + }, + 'versions': { + '1.0.0': { + 'name': 'blue', + 'dist': {} + }, + '1.0.1': {} + } + }], + '/cyan': [200, { + '_npmUser': { + 'name': 'claudia', + 'email': 'claudia@cyan.com' + } , + 'name': 'cyan', + 'dist-tags': {}, + 'versions': { + '1.0.0': { + 'name': 'cyan', + 'dist': {} + }, + '1.0.1': {} + } + }], + '/brown': [200, { + 'name': 'brown' + }], + '/yellow': [200, { + 'name': 'yellow', + 'author': { + 'name': 'foo', + 'email': 'foo@yellow.com', + 'twitter': 'foo' + }, + 'versions': { + '1.0.0': { + 'maintainers': [ + { 'name': 'claudia', 'email': 'c@yellow.com', 'twitter': 'cyellow' }, + { 'name': 'isaacs', 'email': 'i@yellow.com', 'twitter': 'iyellow' } + ] + }, + '1.0.1': {} + } + }], + '/purple': [200, { + 'name': 'purple', + 'versions': { + '1.0.0': { + 'foo': 1, + 'maintainers': [ + { 'name': 'claudia' } + ] + }, + '1.0.1': {} + } + }], + '/green': [200, { + 'name': 'green', + 'dist-tags': { + '1.0.1': {} + }, + 'keywords': ['colors', 'green', 'crayola'], + 'versions': { + '1.0.0': { + 'bugs': { + 'url': 'http://bugs.green.com' + }, + 'deprecated': true, + 'repository': { + 'url': 'http://repository.green.com' + }, + 'license': { type: 'ACME' }, + 'bin': { + 'green': 'bin/green.js' + }, + 'dependencies': { + 'red': {}, + 'yellow': {} + }, + 'dist': { + 'integrity': '---', + 'fileCount': 1, + 'unpackedSize': 1 + } + }, + '1.0.1': {} + } + }], + '/black': [200, { + 'name': 'black', + 'dist-tags': { + '1.0.1': {} + }, + 'versions': { + '1.0.0': { + 'bugs': 'http://bugs.black.com', + 'license': {}, + 'dependencies': (() => { + const deps = {} + for (i = 0; i < 25; i++) { + deps[i] = {} + } + return deps + })(), + 'dist': { + 'integrity': '---', + 'fileCount': 1, + 'unpackedSize': 1 + } + }, + '1.0.1': {} + } + }], + '/pink': [200, { + 'name': 'pink', + 'dist-tags': { + '1.0.1': {} + }, + 'versions': { + '1.0.0': { + 'maintainers': [ + { 'name': 'claudia', 'url': 'http://c.yellow.com' }, + { 'name': 'isaacs', 'url': 'http://i.yellow.com' } + ], + 'repository': 'http://repository.pink.com', + 'license': {}, + 'dist': { + 'integrity': '---', + 'fileCount': 1, + 'unpackedSize': 1 + } + }, + '1.0.1': {} + } + }], + '/orange': [200, { + 'name': 'orange', + 'dist-tags': { + '1.0.1': {} + }, + 'versions': { + '1.0.0': { + 'homepage': 'http://hm.orange.com', + 'license': {}, + 'dist': { + 'integrity': '---', + 'fileCount': 1, + 'unpackedSize': 1 + } + }, + '1.0.1': {} + } + }] + } + } + mr({ port: 1331, mocks }, (err, s) => { + server = s + }) + t.done() +}) + +t.test('should log package info', t => { + const view = requireInject('../../lib/view.js', { + '../../lib/npm.js': { + flatOptions: { + registry: REG, + global: false, + unicode: true + } + } + }) + + t.test('mkdirp@0.3.5', t => { + view(['mkdirp@0.3.5'], () => { + t.matchSnapshot(logs) + t.end() + }) + }) + + t.test('package with license, bugs, repository and other fields', t => { + view(['green@1.0.0'], () => { + t.matchSnapshot(logs) + t.end() + }) + }) + + t.test('package with more than 25 deps', t => { + view(['black@1.0.0'], () => { + t.matchSnapshot(logs) + t.end() + }) + }) + + t.test('package with maintainers info as object', t => { + view(['pink@1.0.0'], () => { + t.matchSnapshot(logs) + t.end() + }) + }) + + t.test('package with homepage', t => { + view(['orange@1.0.0'], () => { + t.matchSnapshot(logs) + t.end() + }) + }) + + t.test('package with no versions', t => { + view(['brown'], () => { + t.matchSnapshot(logs) + t.end() + }) + }) + + t.test('package with no repo or homepage', t => { + view(['blue@1.0.0'], () => { + t.matchSnapshot(logs) + t.end() + }) + }) + + t.test('package with no modified time', t => { + view(['cyan@1.0.0'], () => { + t.matchSnapshot(logs) + t.end() + }) + }) + + t.end() +}) + +t.test('should log info of package in current working dir', t => { + const testDir = t.testdir({ + 'package.json': JSON.stringify({ + name: 'underscore', + version: '1.3.1' + }, null, 2) + }) + + const view = requireInject('../../lib/view.js', { + '../../lib/npm.js': { + prefix: testDir, + flatOptions: { + defaultTag: '1.3.1', + registry: REG, + global: false + } + } + }) + + t.test('specific version', t => { + view(['.@1.3.1'], () => { + t.matchSnapshot(logs) + t.end() + }) + }) + + t.test('non-specific version', t => { + view(['.'], () => { + t.matchSnapshot(logs) + t.end() + }) + }) + + t.end() +}) + +t.test('should log info by field name', t => { + const viewJson = requireInject('../../lib/view.js', { + '../../lib/npm.js': { + flatOptions: { + registry: REG, + json: true, + global: false + } + } + }) + + const view = requireInject('../../lib/view.js', { + '../../lib/npm.js': { + flatOptions: { + registry: REG, + global: false + } + } + }) + + t.test('readme', t => { + view(['underscore@1.x.x', 'readme'], () => { + t.matchSnapshot(logs) + t.end() + }) + }) + + t.test('several fields', t => { + viewJson(['underscore@1.3.1', 'name', 'version', 'foo[bar]'], () => { + t.matchSnapshot(logs) + t.end() + }) + }) + + t.test('several fields with several versions', t => { + view(['underscore@1.x.x', 'author'], () => { + t.matchSnapshot(logs) + t.end() + }) + }) + + t.test('nested field with brackets', t => { + viewJson(['underscore@1.3.0', 'author[name]'], () => { + t.matchSnapshot(logs) + t.end() + }) + }) + + t.test('maintainers with email', t => { + viewJson(['yellow@1.0.0', 'maintainers', 'name'], () => { + t.matchSnapshot(logs) + t.end() + }) + }) + + t.test('maintainers with url', t => { + viewJson(['pink@1.0.0', 'maintainers'], () => { + t.matchSnapshot(logs) + t.end() + }) + }) + + t.test('unknown nested field ', t => { + view(['underscore@1.3.1', 'dist.foobar'], () => { + t.matchSnapshot(logs) + t.end() + }) + }) + + t.test('array field - 1 element', t => { + view(['purple@1.0.0', 'maintainers.name'], () => { + t.matchSnapshot(logs) + t.end() + }) + }) + + t.test('array field - 2 elements', t => { + view(['yellow@1.x.x', 'maintainers.name'], () => { + t.matchSnapshot(logs) + t.end() + }) + }) + + t.end() +}) + +t.test('throw error if global mode', (t) => { + const view = requireInject('../../lib/view.js', { + '../../lib/npm.js': { + flatOptions: { + global: true + } + } + }) + view([], (err) => { + t.equals(err.message, 'Cannot use view command in global mode.') + t.end() + }) +}) + +t.test('throw error if invalid package.json', (t) => { + const testDir = t.testdir({ + 'package.json': '{}' + }) + + const view = requireInject('../../lib/view.js', { + '../../lib/npm.js': { + prefix: testDir, + flatOptions: { + global: false + } + } + }) + view([], (err) => { + t.equals(err.message, 'Invalid package.json') + t.end() + }) +}) + +t.test('throws when unpublished', (t) => { + const view = requireInject('../../lib/view.js', { + '../../lib/npm.js': { + flatOptions: { + defaultTag: '1.0.1', + registry: REG, + global: false + } + } + }) + view(['red'], (err) => { + t.equals(err.code, 'E404') + t.end() + }) +}) + +t.test('should be silent', (t) => { + const view = requireInject('../../lib/view.js', { + '../../lib/npm.js': { + flatOptions: { + defaultTag: '1.0.1', + registry: REG, + global: false + } + } + }) + view(['blue'], true, (err) => { + t.notOk(logs.length, 'no logs') + t.end() + }) +}) + +t.test('completion', (t) => { + const view = requireInject('../../lib/view.js', { + '../../lib/npm.js': { + flatOptions: { + defaultTag: '1.0.1', + registry: REG, + global: false + } + } + }) + view.completion({ + conf: { argv: { remain: ['npm', 'view', 'mkdirp@0.3.5'] } } + }, (err, res) => { + t.ok(res, 'returns back fields') + t.end() + }) +}) + +t.test('no registry completion', (t) => { + const view = requireInject('../../lib/view.js', { + '../../lib/npm.js': { + flatOptions: { + defaultTag: '1.0.1', + } + } + }) + view.completion({ + conf: { argv: { remain: ['npm', 'view'] } } + }, (err) => { + t.notOk(err, 'there is no package completion') + t.end() + }) +}) + +t.test('cleanup', function (t) { + server.close() + t.done() +}) \ No newline at end of file diff --git a/test/tap/view.js b/test/tap/view.js deleted file mode 100644 index 71d21487ae99c..0000000000000 --- a/test/tap/view.js +++ /dev/null @@ -1,381 +0,0 @@ -var common = require('../common-tap.js') -const t = require('tap') -var test = t.test -var osenv = require('osenv') -var path = require('path') -var fs = require('fs') -var rimraf = require('rimraf') -var mkdirp = require('mkdirp') - -// this test has to use a tmpdir so that it's outside of -// the current package context of npm. -var tmp = osenv.tmpdir() -var t1dir = path.resolve(tmp, 'view-local-no-pkg') -var t2dir = path.resolve(tmp, 'view-local-notmine') -var t3dir = path.resolve(tmp, 'view-local-mine') -var mr = require('npm-registry-mock') - -var server - -t.teardown(() => { - rimraf.sync(t1dir) - rimraf.sync(t2dir) - rimraf.sync(t3dir) - if (server) { - server.close() - } -}) - -test('setup', function (t) { - mkdirp.sync(t1dir) - mkdirp.sync(t2dir) - mkdirp.sync(t3dir) - - fs.writeFileSync(t2dir + '/package.json', JSON.stringify({ - author: 'Evan Lucas', - name: 'test-repo-url-https', - version: '0.0.1' - }), 'utf8') - - fs.writeFileSync(t3dir + '/package.json', JSON.stringify({ - author: 'Evan Lucas', - name: 'biscuits', - version: '0.0.1' - }), 'utf8') - - t.pass('created fixtures') - - mr({ port: common.port, plugin: plugin }, function (er, s) { - server = s - t.end() - }) -}) - -function plugin (server) { - server - .get('/biscuits') - .many() - .reply(404, {'error': 'version not found'}) -} - -test('npm view . in global mode', function (t) { - common.npm([ - 'view', - '.', - '--registry=' + common.registry, - '--global' - ], { cwd: t1dir }, function (err, code, stdout, stderr) { - t.ifError(err, 'view command finished successfully') - t.equal(code, 1, 'exit not ok') - t.similar(stderr, /Cannot use view command in global mode./m) - t.end() - }) -}) - -test('npm view --global', function (t) { - common.npm([ - 'view', - '--registry=' + common.registry, - '--global' - ], { cwd: t1dir }, function (err, code, stdout, stderr) { - t.ifError(err, 'view command finished successfully') - t.equal(code, 1, 'exit not ok') - t.similar(stderr, /Cannot use view command in global mode./m) - t.end() - }) -}) - -test('npm view . with no package.json', function (t) { - common.npm([ - 'view', - '.', - '--registry=' + common.registry - ], { cwd: t1dir }, function (err, code, stdout, stderr) { - t.ifError(err, 'view command finished successfully') - t.equal(code, 1, 'exit not ok') - t.similar(stderr, /Invalid package.json/m) - t.end() - }) -}) - -test('npm view . with no published package', function (t) { - common.npm([ - 'view', - '.', - '--registry=' + common.registry - ], { cwd: t3dir }, function (err, code, stdout, stderr) { - t.ifError(err, 'view command finished successfully') - t.equal(code, 1, 'exit not ok') - t.similar(stderr, /not in the npm registry/m) - t.end() - }) -}) - -test('npm view .', function (t) { - common.npm([ - 'view', - '.', - '--registry=' + common.registry - ], { cwd: t2dir }, function (err, code, stdout) { - t.ifError(err, 'view command finished successfully') - t.equal(code, 0, 'exit ok') - t.matches(stdout, /test-repo-url-https/, 'has the right package') - t.end() - }) -}) - -test('npm view . select fields', function (t) { - common.npm([ - 'view', - '.', - 'main', - '--registry=' + common.registry - ], { cwd: t2dir }, function (err, code, stdout) { - t.ifError(err, 'view command finished successfully') - t.equal(code, 0, 'exit ok') - t.equal(stdout.trim(), 'index.js', 'should print `index.js`') - t.end() - }) -}) - -test('npm view .@', function (t) { - common.npm([ - 'view', - '.@0.0.0', - 'version', - '--registry=' + common.registry - ], { cwd: t2dir }, function (err, code, stdout) { - t.ifError(err, 'view command finished successfully') - t.equal(code, 0, 'exit ok') - t.equal(stdout.trim(), '0.0.0', 'should print `0.0.0`') - t.end() - }) -}) - -test('npm view .@ version --json', function (t) { - common.npm([ - 'view', - '.@0.0.0', - 'version', - '--json', - '--registry=' + common.registry - ], { cwd: t2dir }, function (err, code, stdout) { - t.ifError(err, 'view command finished successfully') - t.equal(code, 0, 'exit ok') - t.equal(stdout.trim(), '"0.0.0"', 'should print `"0.0.0"`') - t.end() - }) -}) - -test('npm view . --json author name version', function (t) { - common.npm([ - 'view', - '.', - 'author', - 'name', - 'version', - '--json', - '--registry=' + common.registry - ], { cwd: t2dir }, function (err, code, stdout) { - var expected = JSON.stringify({ - author: 'Evan Lucas ', - name: 'test-repo-url-https', - version: '0.0.1' - }, null, 2) - t.ifError(err, 'view command finished successfully') - t.equal(code, 0, 'exit ok') - t.equal(stdout.trim(), expected, 'should print ' + expected) - t.end() - }) -}) - -test('npm view .@ --json author name version', function (t) { - common.npm([ - 'view', - '.@0.0.0', - 'author', - 'name', - 'version', - '--json', - '--registry=' + common.registry - ], { cwd: t2dir }, function (err, code, stdout) { - var expected = JSON.stringify({ - author: 'Evan Lucas ', - name: 'test-repo-url-https', - version: '0.0.0' - }, null, 2) - t.ifError(err, 'view command finished successfully') - t.equal(code, 0, 'exit ok') - t.equal(stdout.trim(), expected, 'should print ' + expected) - t.end() - }) -}) - -test('npm view ', function (t) { - common.npm([ - 'view', - 'underscore', - '--registry=' + common.registry - ], { cwd: t2dir }, function (err, code, stdout) { - t.ifError(err, 'view command finished successfully') - t.equal(code, 0, 'exit ok') - t.matches(stdout, /underscore/, 'should have name `underscore`') - t.end() - }) -}) - -test('npm view --global', function (t) { - common.npm([ - 'view', - 'underscore', - '--global', - '--registry=' + common.registry - ], { cwd: t2dir }, function (err, code, stdout) { - t.ifError(err, 'view command finished successfully') - t.equal(code, 0, 'exit ok') - t.matches(stdout, /underscore/, 'should have name `underscore`') - t.end() - }) -}) - -test('npm view @ versions', function (t) { - common.npm([ - 'view', - 'underscore@^1.5.0', - 'versions', - '--registry=' + common.registry - ], { cwd: t2dir }, function (err, code, stdout) { - t.ifError(err, 'view command finished successfully') - t.equal(code, 0, 'exit ok') - var re = new RegExp('1.5.0') - t.similar(stdout, re, 'should have version `1.5.0`') - t.end() - }) -}) - -test('npm view @ version --json', function (t) { - common.npm([ - 'view', - 'underscore@~1.5.0', - 'version', - '--json', - '--registry=' + common.registry - ], { cwd: t2dir }, function (err, code, stdout) { - t.ifError(err, 'view command finished successfully') - t.equal(code, 0, 'exit ok') - t.equal(stdout.trim(), JSON.stringify([ - '1.5.0', - '1.5.1' - ], null, 2), 'should have three versions') - t.end() - }) -}) - -test('npm view --json', function (t) { - t.plan(3) - common.npm([ - 'view', - 'underscore', - '--json', - '--registry=' + common.registry - ], { cwd: t2dir }, function (err, code, stdout) { - t.ifError(err, 'view command finished successfully') - t.equal(code, 0, 'exit ok') - try { - var out = JSON.parse(stdout.trim()) - t.similar(out, { - maintainers: ['jashkenas '] - }, 'should have the same maintainer') - } catch (er) { - t.fail('Unable to parse JSON') - } - }) -}) - -test('npm view @', function (t) { - common.npm([ - 'view', - 'underscore@12345', - '--registry=' + common.registry - ], { cwd: t2dir }, function (err, code, stdout) { - t.ifError(err, 'view command finished successfully') - t.equal(code, 0, 'exit ok') - t.equal(stdout.trim(), '', 'should return empty') - t.end() - }) -}) - -test('npm view @ --json', function (t) { - common.npm([ - 'view', - 'underscore@12345', - '--json', - '--registry=' + common.registry - ], { cwd: t2dir }, function (err, code, stdout) { - t.ifError(err, 'view command finished successfully') - t.equal(code, 0, 'exit ok') - t.equal(stdout.trim(), '', 'should return empty') - t.end() - }) -}) - -test('npm view ', function (t) { - common.npm([ - 'view', - 'underscore', - 'homepage', - '--registry=' + common.registry - ], { cwd: t2dir }, function (err, code, stdout) { - t.ifError(err, 'view command finished successfully') - t.equal(code, 0, 'exit ok') - t.equal(stdout.trim(), 'http://underscorejs.org', - 'homepage should equal `http://underscorejs.org`') - t.end() - }) -}) - -test('npm view with invalid package name', function (t) { - var invalidName = 'InvalidPackage' - - server.get('/' + invalidName).reply('404', {'error': 'not found'}) - common.npm([ - 'view', - invalidName, - '--registry=' + common.registry - ], {}, function (err, code, stdout, stderr) { - t.ifError(err, 'view command finished successfully') - t.equal(code, 1, 'exit not ok') - - t.similar(stderr, new RegExp('is not in the npm registry'), - 'Package should NOT be found') - - t.dissimilar(stderr, new RegExp('use the name yourself!'), - 'Suggestion should not be there') - - t.similar(stderr, new RegExp('name can no longer contain capital letters'), - 'Suggestion about Capital letter should be there') - - t.end() - }) -}) - -test('npm view with valid but non existent package name', function (t) { - server.get('/valid-but-non-existent-package').reply(404, {'error': 'not found'}) - common.npm([ - 'view', - 'valid-but-non-existent-package', - '--registry=' + common.registry - ], {}, function (err, code, stdout, stderr) { - t.ifError(err, 'view command finished successfully') - t.equal(code, 1, 'exit not ok') - - t.similar(stderr, - new RegExp("'valid-but-non-existent-package' is not in the npm registry\\."), - 'Package should NOT be found') - - t.similar(stderr, new RegExp('use the name yourself!'), - 'Suggestion should be there') - - t.end() - }) -})