Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: move run-script banners to stderr when in json mode #7439

Merged
merged 3 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/bin/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change was made so that our prepack script doesn't log anything to stdout, so that a smoke-test could be added that running node . pack --json only ends up with json parseable output on stdout.

This highlights an important note about this change, as it only redirects the banner from @npmcli/run-script to stderr when --json is set. The user's run-script is spawned with stdio: inherit so if that process sends output to stdout it could still break in json mode. This is not something npm should change since we want to allow spawned process to write to whatever stream they want.

If a user really wants to ensure that the script cannot output anything, they should also use --foreground-scripts=false or --silent.

.catch((err) => {
process.exitCode = 1
console.error(err)
Expand Down
24 changes: 15 additions & 9 deletions lib/utils/display.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
12 changes: 12 additions & 0 deletions smoke-tests/test/pack-json-output.js
Original file line number Diff line number Diff line change
@@ -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')
})
13 changes: 12 additions & 1 deletion tap-snapshots/test/lib/commands/pack.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 5 additions & 1 deletion test/lib/commands/pack.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,18 +72,22 @@ 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 },
})
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)))
})
Expand Down