diff --git a/lib/commands/view.js b/lib/commands/view.js index 29cfd1da24fda..11d54b681fa6d 100644 --- a/lib/commands/view.js +++ b/lib/commands/view.js @@ -1,7 +1,7 @@ const columns = require('cli-columns') const { readFile } = require('fs/promises') const jsonParse = require('json-parse-even-better-errors') -const { log, output } = require('proc-log') +const { log, output, META } = require('proc-log') const npa = require('npm-package-arg') const { resolve } = require('path') const formatBytes = require('../utils/format-bytes.js') @@ -11,6 +11,8 @@ const { inspect } = require('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 = async file => jsonParse(await readFile(file, 'utf8')) @@ -103,10 +105,24 @@ class View extends BaseCommand { return this.exec([pkg, ...rest]) } + const json = this.npm.config.get('json') await this.setWorkspaces() for (const name of this.workspaceNames) { - await this.#viewPackage(`${name}${pkg.slice(1)}`, rest, { workspace: true }) + 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 = 1 + } } } diff --git a/lib/utils/display.js b/lib/utils/display.js index 3fa8d23e9dc30..33247eab3fe4b 100644 --- a/lib/utils/display.js +++ b/lib/utils/display.js @@ -99,6 +99,7 @@ const mergeJson = (meta, buffer) => { acc[0].push(tryJsonParse(i[2])) // index 1 is the meta object acc[1].push(i[1][JSON_ERROR_KEY]) + return acc }, [ [], // meta also contains the meta object passed to flush diff --git a/tap-snapshots/test/lib/commands/view.js.test.cjs b/tap-snapshots/test/lib/commands/view.js.test.cjs index 54ceb893ae58a..90da5840f4f40 100644 --- a/tap-snapshots/test/lib/commands/view.js.test.cjs +++ b/tap-snapshots/test/lib/commands/view.js.test.cjs @@ -363,6 +363,140 @@ yellow@1.0.1 'claudia' yellow@1.0.2 'claudia' ` +exports[`test/lib/commands/view.js TAP workspaces 404 workspaces basic > must match snapshot 1`] = ` + +green@1.0.0 | ACME | deps: 2 | versions: 2 +green is a very important color + +DEPRECATED!! - true + +keywords: colors, green, crayola + +bin: green + +dist +.tarball: http://hm.green.com/1.0.0.tgz +.shasum: 123 +.integrity: --- +.unpackedSize: 1.0 GB + +dependencies: +red: 1.0.0 +yellow: 1.0.0 + +maintainers: +- claudia <c@yellow.com> +- isaacs <i@yellow.com> + +dist-tags: +latest: 1.0.0 +error code E404 +error 404 404 +` + +exports[`test/lib/commands/view.js TAP workspaces 404 workspaces json > must match snapshot 1`] = ` +{ + "green": { + "_id": "green", + "name": "green", + "dist-tags": { + "latest": "1.0.0" + }, + "maintainers": [ + { + "name": "claudia", + "email": "c@yellow.com", + "twitter": "cyellow" + }, + { + "name": "isaacs", + "email": "i@yellow.com", + "twitter": "iyellow" + } + ], + "keywords": [ + "colors", + "green", + "crayola" + ], + "versions": [ + "1.0.0", + "1.0.1" + ], + "version": "1.0.0", + "description": "green is a very important color", + "bugs": { + "url": "http://bugs.green.com" + }, + "deprecated": true, + "repository": { + "url": "http://repository.green.com" + }, + "license": { + "type": "ACME" + }, + "bin": { + "green": "bin/green.js" + }, + "dependencies": { + "red": "1.0.0", + "yellow": "1.0.0" + }, + "dist": { + "shasum": "123", + "tarball": "http://hm.green.com/1.0.0.tgz", + "integrity": "---", + "fileCount": 1, + "unpackedSize": 1000000000 + } + }, + "error": { + "missing-package": { + "code": "E404", + "summary": "404", + "detail": "" + } + } +} +` + +exports[`test/lib/commands/view.js TAP workspaces 404 workspaces non-404 error rejects > must match snapshot 1`] = ` + +green@1.0.0 | ACME | deps: 2 | versions: 2 +green is a very important color + +DEPRECATED!! - true + +keywords: colors, green, crayola + +bin: green + +dist +.tarball: http://hm.green.com/1.0.0.tgz +.shasum: 123 +.integrity: --- +.unpackedSize: 1.0 GB + +dependencies: +red: 1.0.0 +yellow: 1.0.0 + +maintainers: +- claudia <c@yellow.com> +- isaacs <i@yellow.com> + +dist-tags: +latest: 1.0.0 +error Unknown error +` + +exports[`test/lib/commands/view.js TAP workspaces 404 workspaces non-404 error rejects with single arg > must match snapshot 1`] = ` +green: +1.0.0 +unknown-error: +error Unknown error +` + exports[`test/lib/commands/view.js TAP workspaces all workspaces --json > must match snapshot 1`] = ` { "green": { diff --git a/test/fixtures/mock-logs.js b/test/fixtures/mock-logs.js index ce4c189219467..a9277e4ce999c 100644 --- a/test/fixtures/mock-logs.js +++ b/test/fixtures/mock-logs.js @@ -24,6 +24,7 @@ const logsByTitle = (logs) => ({ module.exports = () => { const outputs = [] const outputErrors = [] + const fullOutput = [] const levelLogs = [] const logs = Object.defineProperties([], { @@ -53,6 +54,7 @@ module.exports = () => { // in the future if/when we refactor what logs look like. if (!isLog(str)) { outputErrors.push(str) + fullOutput.push(str) return } @@ -70,12 +72,14 @@ module.exports = () => { const level = stripAnsi(rawLevel) logs.push(str.replaceAll(prefix, `${level} `)) + fullOutput.push(str.replaceAll(prefix, `${level} `)) levelLogs.push({ level, message: str.replaceAll(prefix, '') }) }, }, stdout: { write: (str) => { outputs.push(trimTrailingNewline(str)) + fullOutput.push(trimTrailingNewline(str)) }, }, } @@ -88,9 +92,12 @@ module.exports = () => { clearOutput: () => { outputs.length = 0 outputErrors.length = 0 + fullOutput.length = 0 }, outputErrors, joinedOutputError: () => joinAndTrimTrailingNewlines(outputs), + fullOutput, + joinedFullOutput: () => joinAndTrimTrailingNewlines(fullOutput), logs, clearLogs: () => { levelLogs.length = 0 diff --git a/test/lib/commands/view.js b/test/lib/commands/view.js index d15d62f8acdcb..1aa54e206e2cd 100644 --- a/test/lib/commands/view.js +++ b/test/lib/commands/view.js @@ -271,13 +271,24 @@ const packument = (nv, opts) => { }, }, } + if (nv.type === 'git') { return mocks[nv.hosted.project] } + if (nv.raw === './blue') { return mocks.blue } - return mocks[nv.name] + + if (mocks[nv.name]) { + return mocks[nv.name] + } + + if (nv.name === 'unknown-error') { + throw new Error('Unknown error') + } + + throw Object.assign(new Error('404'), { code: 'E404' }) } const loadMockNpm = async function (t, opts = {}) { @@ -543,6 +554,24 @@ t.test('workspaces', async t => { }, } + const prefixDir404 = { + 'test-workspace-b': { + 'package.json': JSON.stringify({ + name: 'missing-package', + version: '1.2.3', + }), + }, + } + + const prefixDirError = { + 'test-workspace-b': { + 'package.json': JSON.stringify({ + name: 'unknown-error', + version: '1.2.3', + }), + }, + } + t.test('all workspaces', async t => { const { view, joinedOutput } = await loadMockNpm(t, { prefixDir, @@ -624,6 +653,46 @@ t.test('workspaces', async t => { t.matchSnapshot(joinedOutput()) t.matchSnapshot(logs.warn, 'should have warning of ignoring workspaces') }) + + t.test('404 workspaces', async t => { + t.test('basic', async t => { + const { view, joinedFullOutput } = await loadMockNpm(t, { + prefixDir: { ...prefixDir, ...prefixDir404 }, + config: { workspaces: true, loglevel: 'error' }, + }) + await view.exec([]) + t.matchSnapshot(joinedFullOutput()) + t.equal(process.exitCode, 1) + }) + + t.test('json', async t => { + const { view, joinedFullOutput } = await loadMockNpm(t, { + prefixDir: { ...prefixDir, ...prefixDir404 }, + config: { workspaces: true, json: true, loglevel: 'error' }, + }) + await view.exec([]) + t.matchSnapshot(joinedFullOutput()) + t.equal(process.exitCode, 1) + }) + + t.test('non-404 error rejects', async t => { + const { view, joinedFullOutput } = await loadMockNpm(t, { + prefixDir: { ...prefixDir, ...prefixDirError }, + config: { workspaces: true, loglevel: 'error' }, + }) + await t.rejects(view.exec([])) + t.matchSnapshot(joinedFullOutput()) + }) + + t.test('non-404 error rejects with single arg', async t => { + const { view, joinedFullOutput } = await loadMockNpm(t, { + prefixDir: { ...prefixDir, ...prefixDirError }, + config: { workspaces: true, loglevel: 'error' }, + }) + await t.rejects(view.exec(['.', 'version'])) + t.matchSnapshot(joinedFullOutput()) + }) + }) }) t.test('completion', async t => {