From bcc781abf10b46aa5b04da996fc4602524be9b0e Mon Sep 17 00:00:00 2001 From: Luke Karrys Date: Mon, 29 Apr 2024 16:42:35 -0700 Subject: [PATCH] fix: move run-script banners to stderr when in json mode (#7439) Fixes #7354 --- docs/bin/build.js | 2 +- lib/utils/display.js | 24 ++++++++++++------- smoke-tests/test/pack-json-output.js | 12 ++++++++++ .../test/lib/commands/pack.js.test.cjs | 13 +++++++++- test/lib/commands/pack.js | 6 ++++- 5 files changed, 45 insertions(+), 12 deletions(-) create mode 100644 smoke-tests/test/pack-json-output.js diff --git a/docs/bin/build.js b/docs/bin/build.js index b94a85cf6f9bc..c25fd76ed7e7f 100644 --- a/docs/bin/build.js +++ b/docs/bin/build.js @@ -16,7 +16,7 @@ const run = require('../lib/build.js') const { paths } = require('../lib/index') run(paths) - .then((res) => console.log(`Wrote ${res.length} files`)) + .then((res) => console.error(`Wrote ${res.length} files`)) .catch((err) => { process.exitCode = 1 console.error(err) diff --git a/lib/utils/display.js b/lib/utils/display.js index 4939bc4169bda..29a1f7951d506 100644 --- a/lib/utils/display.js +++ b/lib/utils/display.js @@ -267,16 +267,22 @@ class Display { if (this.#outputState.buffering) { this.#outputState.buffer.push([level, meta, ...args]) } else { - // HACK: if it looks like the banner and we are in a state where we hide the - // banner then dont write any output. This hack can be replaced with proc-log.META - const isBanner = args.length === 1 && - typeof args[0] === 'string' && - args[0].startsWith('\n> ') && - args[0].endsWith('\n') - const hideBanner = this.#silent || ['exec', 'explore'].includes(this.#command) - if (!(isBanner && hideBanner)) { - this.#writeOutput(level, meta, ...args) + // HACK: Check if the argument looks like a run-script banner. This can be + // replaced with proc-log.META in @npmcli/run-script + if (typeof args[0] === 'string' && args[0].startsWith('\n> ') && args[0].endsWith('\n')) { + if (this.#silent || ['exec', 'explore'].includes(this.#command)) { + // Silent mode and some specific commands always hide run script banners + break + } else if (this.#json) { + // In json mode, change output to stderr since we dont want to break json + // parsing on stdout if the user is piping to jq or something. + // XXX: in a future (breaking?) change it might make sense for run-script to + // always output these banners with proc-log.output.error if we think they + // align closer with "logging" instead of "output" + level = output.KEYS.error + } } + this.#writeOutput(level, meta, ...args) } break } diff --git a/smoke-tests/test/pack-json-output.js b/smoke-tests/test/pack-json-output.js new file mode 100644 index 0000000000000..419da58324213 --- /dev/null +++ b/smoke-tests/test/pack-json-output.js @@ -0,0 +1,12 @@ + +const t = require('tap') +const setup = require('./fixtures/setup.js') + +t.test('pack --json returns only json on stdout', async t => { + const { npmLocal } = await setup(t) + + const { stderr, stdout } = await npmLocal('pack', '--json') + + t.match(stderr, /> npm@.* prepack\n/, 'stderr has banner') + t.ok(JSON.parse(stdout), 'stdout can be parsed as json') +}) diff --git a/tap-snapshots/test/lib/commands/pack.js.test.cjs b/tap-snapshots/test/lib/commands/pack.js.test.cjs index e251e63d4c279..9ec24b3816261 100644 --- a/tap-snapshots/test/lib/commands/pack.js.test.cjs +++ b/tap-snapshots/test/lib/commands/pack.js.test.cjs @@ -109,13 +109,24 @@ Array [ "name": "@myscope/test-package", "shasum": "{sha}", "size": "{size}", - "unpackedSize": 50, + "unpackedSize": 88, "version": "1.0.0", }, ], ] ` +exports[`test/lib/commands/pack.js TAP should log scoped package output as valid json > stderr has banners 1`] = ` +Array [ + String( + + > @myscope/test-package@1.0.0 prepack + > echo prepack! + + ), +] +` + exports[`test/lib/commands/pack.js TAP should pack current directory with no arguments > logs pack contents 1`] = ` Array [ "package: test-package@1.0.0", diff --git a/test/lib/commands/pack.js b/test/lib/commands/pack.js index 067e84a0980e0..3ea67c78d996a 100644 --- a/test/lib/commands/pack.js +++ b/test/lib/commands/pack.js @@ -72,11 +72,14 @@ t.test('should log output as valid json', async t => { }) t.test('should log scoped package output as valid json', async t => { - const { npm, outputs, logs } = await loadMockNpm(t, { + const { npm, outputs, outputErrors, logs } = await loadMockNpm(t, { prefixDir: { 'package.json': JSON.stringify({ name: '@myscope/test-package', version: '1.0.0', + scripts: { + prepack: 'echo prepack!', + }, }), }, config: { json: true }, @@ -84,6 +87,7 @@ t.test('should log scoped package output as valid json', async t => { await npm.exec('pack', []) const filename = 'myscope-test-package-1.0.0.tgz' t.matchSnapshot(outputs.map(JSON.parse), 'outputs as json') + t.matchSnapshot(outputErrors, 'stderr has banners') t.matchSnapshot(logs.notice, 'logs pack contents') t.ok(fs.statSync(path.resolve(npm.prefix, filename))) })