mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-22 21:53:02 -04:00
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.)
This commit is contained in:
5
.changeset/default-reporter-bin.md
Normal file
5
.changeset/default-reporter-bin.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@pnpm/cli.default-reporter": minor
|
||||
---
|
||||
|
||||
Added a `pnpm-render` bin that renders pnpm-shaped NDJSON read from stdin, so the same renderer can be used to format output from external tools that emit `pnpm:*` log records (e.g. `pacquet install --reporter=ndjson 2>&1 >/dev/null | pnpm-render`). An optional first positional argument sets the command name (defaults to `install`).
|
||||
@@ -28,6 +28,24 @@ try {
|
||||
}
|
||||
```
|
||||
|
||||
## `pnpm-render` bin
|
||||
|
||||
Installing this package exposes a `pnpm-render` bin that reads pnpm-shaped NDJSON from stdin and renders it through the default reporter. This lets external tools that emit `pnpm:*` log records reuse pnpm's renderer.
|
||||
|
||||
For example, [pacquet](https://github.com/pnpm/pacquet) emits the same wire format under `--reporter=ndjson` (to stderr), so its output can be piped through `pnpm-render`:
|
||||
|
||||
```sh
|
||||
pacquet install --reporter=ndjson 2>&1 >/dev/null | pnpm-render
|
||||
```
|
||||
|
||||
The redirect (`2>&1 >/dev/null`) is needed because pacquet writes the NDJSON stream to stderr.
|
||||
|
||||
An optional first positional argument sets the command name (defaults to `install`); pass it to match the verb the producer is running so command-specific renderers behave correctly:
|
||||
|
||||
```sh
|
||||
pacquet add lodash --reporter=ndjson 2>&1 >/dev/null | pnpm-render add
|
||||
```
|
||||
|
||||
## Style Guide
|
||||
|
||||
1. Never use blue or grey as font color as they are hard to read in many consoles.
|
||||
|
||||
7
cli/default-reporter/bin/pnpm-render.mjs
Executable file
7
cli/default-reporter/bin/pnpm-render.mjs
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env node
|
||||
import { runCli } from '../lib/cli.js'
|
||||
|
||||
runCli(process.argv.slice(2)).catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -22,8 +22,12 @@
|
||||
},
|
||||
"files": [
|
||||
"lib",
|
||||
"!*.map"
|
||||
"!*.map",
|
||||
"bin"
|
||||
],
|
||||
"bin": {
|
||||
"pnpm-render": "bin/pnpm-render.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "tsgo --watch",
|
||||
"lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||
|
||||
54
cli/default-reporter/src/cli.ts
Normal file
54
cli/default-reporter/src/cli.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { EventEmitter } from 'node:events'
|
||||
import readline from 'node:readline'
|
||||
|
||||
import type { Log } from '@pnpm/core-loggers'
|
||||
import type { Reporter, StreamParser } from '@pnpm/logger'
|
||||
|
||||
import { initDefaultReporter } from './index.js'
|
||||
|
||||
export async function runCli (argv: readonly string[]): Promise<void> {
|
||||
const cmd = argv[0] ?? 'install'
|
||||
|
||||
const emitter = new EventEmitter()
|
||||
const streamParser: StreamParser<Log> = {
|
||||
on: (event: 'data', reporter: Reporter<Log>) => {
|
||||
emitter.on(event, reporter)
|
||||
},
|
||||
removeListener: (event: 'data', reporter: Reporter<Log>) => {
|
||||
emitter.removeListener(event, reporter)
|
||||
},
|
||||
}
|
||||
|
||||
const close = initDefaultReporter({
|
||||
streamParser,
|
||||
context: { argv: [cmd] },
|
||||
reportingOptions: {
|
||||
throttleProgress: 200,
|
||||
},
|
||||
})
|
||||
|
||||
// initDefaultReporter registers its 'data' listener via setTimeout(0); wait
|
||||
// a tick so events emitted from the readline loop below aren't dropped.
|
||||
await new Promise<void>((resolve) => {
|
||||
setTimeout(resolve, 0)
|
||||
})
|
||||
|
||||
try {
|
||||
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity })
|
||||
for await (const line of rl) {
|
||||
if (!line) continue
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = JSON.parse(line)
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
// Guard against valid JSON that isn't a log object (e.g. `null`,
|
||||
// numbers, strings) — the reporter dispatches on `log.name`.
|
||||
if (parsed == null || typeof parsed !== 'object') continue
|
||||
emitter.emit('data', parsed as Log)
|
||||
}
|
||||
} finally {
|
||||
close()
|
||||
}
|
||||
}
|
||||
64
cli/default-reporter/test/cli.ts
Normal file
64
cli/default-reporter/test/cli.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
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
|
||||
}
|
||||
@@ -209,6 +209,7 @@
|
||||
"packlist",
|
||||
"packr",
|
||||
"packument",
|
||||
"pacquet",
|
||||
"paralleljs",
|
||||
"parallelly",
|
||||
"parseable",
|
||||
|
||||
Reference in New Issue
Block a user