fix(reporter): make WARN and error labels readable without color (#11460)

The `WARN` and `ERR_PNPM_*` labels in pnpm's output relied entirely on a colored background to stand out from the surrounding text. In terminals without color — `NO_COLOR` set, output piped, dumb terminals — the badge collapsed into bare `WARN` / `ERR_PNPM_FOO` and became hard to spot inside the message.

This PR wraps each label in brackets (`[WARN]`, `[ERR_PNPM_FOO]`, `[ERROR]`). The bracket characters are painted in the same color as the badge background, so in a color-capable terminal they appear as plain padding inside the colored badge — the rendering matches what we had before. When ANSI is stripped the brackets reappear as ordinary text, giving the label a clear delimiter.
This commit is contained in:
Zoltan Kochan
2026-05-04 22:07:31 +02:00
parent 47b100ecf7
commit cd87c16dd5
9 changed files with 16 additions and 13 deletions

View File

@@ -0,0 +1,6 @@
---
"@pnpm/cli.default-reporter": patch
"pnpm": patch
---
The `WARN` and error code labels in pnpm's output now wrap in brackets (`[WARN]`, `[ERR_PNPM_FOO]`). Previously the labels relied entirely on a colored background to stand out, which meant they blended into the surrounding text in terminals without color (e.g. when `NO_COLOR` is set or output is piped). The brackets are painted in the same color as the badge background, so they appear as ordinary padding in color-capable terminals — only the no-color rendering changes.

View File

@@ -304,7 +304,7 @@ function formatGenericError (errorMessage: string, stack: object): ErrorInfo {
}
function formatErrorSummary (message: string, code?: string): string {
return `${chalk.bgRed.black(`\u2009${code ?? 'ERROR'}\u2009`)} ${chalk.red(message)}`
return `${chalk.bgRed.red('[')}${chalk.bgRed.black(code ?? 'ERROR')}${chalk.bgRed.red(']')} ${chalk.red(message)}`
}
function reportModifiedDependency (msg: { modified: string[] }): ErrorInfo {

View File

@@ -1,8 +1,5 @@
import chalk from 'chalk'
export function formatWarn (message: string): string {
// The \u2009 is the "thin space" unicode character
// It is used instead of ' ' because chalk (as of version 2.1.0)
// trims whitespace at the beginning
return `${chalk.bgYellow.black('\u2009WARN\u2009')} ${message}`
return `${chalk.bgYellow.yellow('[')}${chalk.bgYellow.black('WARN')}${chalk.bgYellow.yellow(']')} ${message}`
}

View File

@@ -27,7 +27,7 @@ import { map, skip, take } from 'rxjs/operators'
import { formatWarn } from '../src/reporterForClient/utils/formatWarn.js'
const formatErrorCode = (code: string) => chalk.bgRed.black(`\u2009${code}\u2009`)
const formatErrorCode = (code: string) => chalk.bgRed.red('[') + chalk.bgRed.black(code) + chalk.bgRed.red(']')
const formatError = (code: string, message: string) => {
return `${formatErrorCode(code)} ${chalk.red(message)}`
}

View File

@@ -19,7 +19,7 @@ interface Exception extends NodeJS.ErrnoException {
stage?: string
}
const formatErrorCode = (code: string) => chalk.bgRed.black(`\u2009${code}\u2009`)
const formatErrorCode = (code: string) => chalk.bgRed.red('[') + chalk.bgRed.black(code) + chalk.bgRed.red(']')
const formatError = (code: string, message: string) => {
return `${formatErrorCode(code)} ${chalk.red(message)}`
}

View File

@@ -1,7 +1,7 @@
import chalk from 'chalk'
export function formatUnknownOptionsError (unknownOptions: Map<string, string[]>): string {
let output = chalk.bgRed.black('\u2009ERROR\u2009')
let output = chalk.bgRed.red('[') + chalk.bgRed.black('ERROR') + chalk.bgRed.red(']')
const unknownOptionsArray = Array.from(unknownOptions.keys())
if (unknownOptionsArray.length > 1) {
return `${output} ${chalk.red(`Unknown options: ${unknownOptionsArray.map(unknownOption => `'${unknownOption}'`).join(', ')}`)}`

View File

@@ -374,7 +374,7 @@ export async function main (inputArgv: string[]): Promise<void> {
}
function printError (message: string, hint?: string): void {
const ERROR = chalk.bgRed.black('\u2009ERROR\u2009')
const ERROR = chalk.bgRed.red('[') + chalk.bgRed.black('ERROR') + chalk.bgRed.red(']')
console.error(`${message.startsWith(ERROR) ? '' : ERROR + ' '}${chalk.red(message)}`)
if (hint) {
console.error(hint)

View File

@@ -92,5 +92,5 @@ test('should print error summary when some packages fail with --no-bail', async
const output = stdout.toString()
expect(output).toContain('ERR_PNPM_RECURSIVE_FAIL')
expect(output).toContain('Summary: 1 fails, 2 passes')
expect(output).toContain('ERROR project-2@1.0.0 build: `exit 1`')
expect(output).toContain('[ERROR] project-2@1.0.0 build: `exit 1`')
})

View File

@@ -8,17 +8,17 @@ test('formatUnknownOptionsError()', async () => {
expect(
stripAnsi(formatUnknownOptionsError(new Map([['foo', []]])))
).toBe(
"\u2009ERROR\u2009 Unknown option: 'foo'"
"[ERROR] Unknown option: 'foo'"
)
expect(
stripAnsi(formatUnknownOptionsError(new Map([['foo', ['foa', 'fob']]])))
).toBe(
`\u2009ERROR\u2009 Unknown option: 'foo'
`[ERROR] Unknown option: 'foo'
Did you mean 'foa', or 'fob'? Use "--config.unknown=value" to force an unknown option.`
)
expect(
stripAnsi(formatUnknownOptionsError(new Map([['foo', []], ['bar', []]])))
).toBe(
"\u2009ERROR\u2009 Unknown options: 'foo', 'bar'"
"[ERROR] Unknown options: 'foo', 'bar'"
)
})