From 0db7dcc7a2c795ff5a5b3c0aeeea504d806f110f Mon Sep 17 00:00:00 2001 From: Luke Karrys Date: Tue, 18 Oct 2022 20:37:47 -0700 Subject: [PATCH] feat: output json formatted errors on stdout This also adds a new output method `outputBuffer()` which will buffer all output until it is flushed in the exit handler. This allows the exit handler to catch any errors and append them to the output when in json mode. This was necessary to not introduce a regression in the case of npm/cli#2150. BREAKING CHANGE: `npm` now outputs some json errors on stdout. Previously `npm` would output all json formatted errors on stderr, making it difficult to parse as the stderr stream usually has logs already written to it. In the future, `npm` will differentiate between errors and crashes. Errors, such as `E404` and `ERESOLVE`, will be handled and will continue to be output on stdout. In the case of a crash, `npm` will log the error as usual but will not attempt to display it as json, even in `--json` mode. Moving a case from the category of an error to a crash will not be considered a breaking change. For more information see npm/rfcs#482. Closes #2740 Closes https://github.com/npm/statusboard/issues/589 --- lib/commands/ls.js | 2 +- lib/npm.js | 29 +++++++++++++++++++++++++++++ lib/utils/exit-handler.js | 8 ++++++-- test/fixtures/mock-npm.js | 9 +++++++++ test/lib/utils/exit-handler.js | 4 ++-- 5 files changed, 47 insertions(+), 5 deletions(-) diff --git a/lib/commands/ls.js b/lib/commands/ls.js index 6812c3923787e..7eebdf691683f 100644 --- a/lib/commands/ls.js +++ b/lib/commands/ls.js @@ -177,7 +177,7 @@ class LS extends ArboristWorkspaceCmd { const [rootError] = tree.errors.filter(e => e.code === 'EJSONPARSE' && e.path === resolve(path, 'package.json')) - this.npm.output( + this.npm.outputBuffer( json ? jsonOutput({ path, problems, result, rootError, seenItems }) : parseable diff --git a/lib/npm.js b/lib/npm.js index 763c9b7a384d0..81be514393c6b 100644 --- a/lib/npm.js +++ b/lib/npm.js @@ -40,6 +40,7 @@ class Npm extends EventEmitter { #argvClean = [] #chalk = null + #outputBuffer = [] #logFile = new LogFile() #display = new Display() #timers = new Timers({ @@ -466,6 +467,34 @@ class Npm extends EventEmitter { log.showProgress() } + outputBuffer (item) { + this.#outputBuffer.push(item) + } + + flushOutput (jsonError) { + if (!jsonError && !this.#outputBuffer.length) { + return + } + + if (this.config.get('json')) { + const jsonOutput = this.#outputBuffer.reduce((acc, item) => { + try { + item = JSON.parse(item) + } catch { + // try to parse it as json in case its a string + } + return { ...acc, ...item } + }, {}) + this.output(JSON.stringify({ ...jsonOutput, ...jsonError }, null, 2)) + } else { + for (const item of this.#outputBuffer) { + this.output(item) + } + } + + this.#outputBuffer.length = 0 + } + outputError (...msg) { log.clearProgress() // eslint-disable-next-line no-console diff --git a/lib/utils/exit-handler.js b/lib/utils/exit-handler.js index 2574a83fc559e..a9e061de7a4a5 100644 --- a/lib/utils/exit-handler.js +++ b/lib/utils/exit-handler.js @@ -136,6 +136,7 @@ const exitHandler = err => { let exitCode let noLogMessage + let jsonError if (err) { exitCode = 1 @@ -198,14 +199,13 @@ const exitHandler = err => { } if (hasLoadedNpm && npm.config.get('json')) { - const error = { + jsonError = { error: { code: err.code, summary: messageText(summary), detail: messageText(detail), }, } - npm.outputError(JSON.stringify(error, null, 2)) } if (typeof err.errno === 'number') { @@ -216,6 +216,10 @@ const exitHandler = err => { } } + if (hasLoadedNpm) { + npm.flushOutput(jsonError) + } + log.verbose('exit', exitCode || 0) showLogFileError = (hasLoadedNpm && npm.silent) || noLogMessage diff --git a/test/fixtures/mock-npm.js b/test/fixtures/mock-npm.js index 0318eadebafe6..8a744cd559eaf 100644 --- a/test/fixtures/mock-npm.js +++ b/test/fixtures/mock-npm.js @@ -238,6 +238,15 @@ class MockNpm { } this._mockOutputs.push(msg) } + + // with the older fake mock npm there is no + // difference between output and outputBuffer + // since it just collects the output and never + // calls the exit handler, so we just mock the + // method the same as output. + outputBuffer (...msg) { + this.output(...msg) + } } const FakeMockNpm = (base = {}, t) => { diff --git a/test/lib/utils/exit-handler.js b/test/lib/utils/exit-handler.js index 7e3137013ecab..63f1b5e391301 100644 --- a/test/lib/utils/exit-handler.js +++ b/test/lib/utils/exit-handler.js @@ -208,7 +208,7 @@ t.test('exit handler called - no npm with error without stack', async (t) => { }) t.test('console.log output using --json', async (t) => { - const { exitHandler, outputErrors } = await mockExitHandler(t, { + const { exitHandler, outputs } = await mockExitHandler(t, { config: { json: true }, }) @@ -216,7 +216,7 @@ t.test('console.log output using --json', async (t) => { t.equal(process.exitCode, 1) t.same( - JSON.parse(outputErrors[0]), + JSON.parse(outputs[0]), { error: { code: 'EBADTHING', // should default error code to E[A-Z]+