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..9fbda90734921 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,37 @@ 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) => { + if (typeof item === 'string') { + // try to parse it as json in case its a string + try { + item = JSON.parse(item) + } catch { + return acc + } + } + 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..d22ec4dd141a8 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]+ @@ -228,6 +228,50 @@ t.test('console.log output using --json', async (t) => { ) }) +t.test('merges output buffers errors with --json', async (t) => { + const { exitHandler, outputs, npm } = await mockExitHandler(t, { + config: { json: true }, + }) + + npm.outputBuffer({ output_data: 1 }) + npm.outputBuffer(JSON.stringify({ more_data: 2 })) + npm.outputBuffer('not json, will be ignored') + + await exitHandler(err('Error: EBADTHING Something happened')) + + t.equal(process.exitCode, 1) + t.same( + JSON.parse(outputs[0]), + { + output_data: 1, + more_data: 2, + error: { + code: 'EBADTHING', // should default error code to E[A-Z]+ + summary: 'Error: EBADTHING Something happened', + detail: 'Error: EBADTHING Something happened', + }, + }, + 'should output expected json output' + ) +}) + +t.test('output buffer without json', async (t) => { + const { exitHandler, outputs, npm, logs } = await mockExitHandler(t) + + npm.outputBuffer('output_data') + npm.outputBuffer('more_data') + + await exitHandler(err('Error: EBADTHING Something happened')) + + t.equal(process.exitCode, 1) + t.same( + outputs, + [['output_data'], ['more_data']], + 'should output expected output' + ) + t.match(logs.error, [['code', 'EBADTHING']]) +}) + t.test('throw a non-error obj', async (t) => { const { exitHandler, logs } = await mockExitHandler(t)