mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-28 09:55:39 -04:00
External processes like SSH passphrase prompts can write to the terminal between progress updates. The previous renderer used `ansi-diff`, which only overwrites the characters it knows changed, so leftover characters from the external output stayed visible on the progress line — e.g. `added 0sa':`, where `sa':` is a fragment of `Enter passphrase for key '.../.ssh/id_rsa':`. Closes https://github.com/pnpm/pnpm/issues/12350 ## Summary The interactive (non-append-only) reporter now redraws the whole frame in place on each update instead of incrementally diffing it: - return the cursor to the top-left of the previous frame (`ESC[<rows>A` followed by a carriage return, so the redraw starts at column 0 even if an external process left the cursor mid-line), - erase from there to the end of the display (`ESC[0J`), - reprint the frame — all in a single atomic write, so there is no flicker. Because the whole region is erased on every frame, any characters an external process wrote in between are cleared. This matches pacquet's `Output::Frame` rendering (the column-reset hardening was applied to both stacks). The now-unused `ansi-diff` dependency has been removed. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Zoltan Kochan <z@kochan.io>
65 lines
2.6 KiB
TypeScript
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')
|
|
|
|
// The reporter intersperses cursor-movement escapes when redrawing each
|
|
// frame, 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
|
|
}
|