Files
Zoltan Kochan 6836547017 feat(default-reporter): add pnpm-render bin to render NDJSON from stdin (#11530)
- Adds a `pnpm-render` bin to `@pnpm/cli.default-reporter` that reads pnpm-shaped NDJSON from stdin and pipes it through the default reporter, so external tools that emit `pnpm:*` log records can reuse pnpm's renderer.
- Optional first positional arg sets the command name (defaults to `install`), e.g. `pnpm-render add` for piping output from `pacquet add`.

## Motivation

[Pacquet](https://github.com/pnpm/pacquet) emits pnpm-shaped `--reporter=ndjson` output for forward-compatibility with pnpm's renderer, but there was no way to actually render it. With this bin:

```sh
pacquet install --reporter=ndjson 2>&1 >/dev/null | pnpm-render
```

(pacquet writes NDJSON to stderr, so the redirect is needed.)
2026-05-08 01:56:54 +02:00

65 lines
2.6 KiB
TypeScript

import { spawn } from 'node:child_process'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { expect, test } from '@jest/globals'
const BIN = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'pnpm-render.mjs')
test('pnpm-render bin renders ndjson piped from stdin', async () => {
const lines = [
{ name: 'pnpm:stage', level: 'debug', prefix: '/tmp/proj', stage: 'resolution_started' },
{ name: 'pnpm:progress', level: 'debug', packageId: 'foo@1.0.0', requester: '/tmp/proj', status: 'resolved' },
{ name: 'pnpm:progress', level: 'debug', packageId: 'bar@2.0.0', requester: '/tmp/proj', status: 'resolved' },
{ name: 'pnpm:summary', level: 'debug', prefix: '/tmp/proj' },
]
const stdout = await runBin(['install'], lines.map((line) => JSON.stringify(line)).join('\n') + '\n')
// ansi-diff intersperses cursor-movement escapes when the rendered string
// changes (e.g. "1" → "2"), so we can't substring-match the final value
// without terminal emulation. Verifying that any progress line rendered is
// enough to prove the bin wired stdin → reporter correctly.
expect(stdout).toContain('Progress: resolved')
})
test('pnpm-render bin ignores malformed and non-object stdin lines', async () => {
const stdout = await runBin(['install'], [
'not-json',
'null',
'42',
'"a string"',
JSON.stringify({ name: 'pnpm:stage', level: 'debug', prefix: '/tmp/proj', stage: 'resolution_started' }),
JSON.stringify({ name: 'pnpm:progress', level: 'debug', packageId: 'foo@1.0.0', requester: '/tmp/proj', status: 'resolved' }),
'',
JSON.stringify({ name: 'pnpm:summary', level: 'debug', prefix: '/tmp/proj' }),
].join('\n') + '\n')
expect(stdout).toContain('Progress: resolved 1')
})
async function runBin (args: readonly string[], stdin: string): Promise<string> {
const child = spawn(process.execPath, [BIN, ...args], { stdio: ['pipe', 'pipe', 'pipe'] })
let stdout = ''
let stderr = ''
child.stdout.on('data', (chunk: Buffer) => {
stdout += chunk.toString()
})
child.stderr.on('data', (chunk: Buffer) => {
stderr += chunk.toString()
})
child.stdin.end(stdin)
// Wait for 'close' (not 'exit'): 'exit' can fire before stdout/stderr
// are fully drained, which leads to truncated captures and flaky asserts.
const exitCode = await new Promise<number | null>((resolve, reject) => {
child.on('error', reject)
child.on('close', (code) => {
resolve(code)
})
})
if (exitCode !== 0) {
throw new Error(`pnpm-render exited with ${exitCode}\nstdout:\n${stdout}\nstderr:\n${stderr}`)
}
return stdout
}