mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-06 23:44:55 -04:00
* feat: report lockfile verification progress The lockfile resolution verifier introduced in #11705 runs an unbounded registry round-trip on cache miss and was previously silent — on a cold registry cache users saw nothing for several seconds. Emit pnpm:lockfile-verification log events (started/done) around the actual verification pass and render them in the default reporter as a transient progress line that collapses into a final "verified" summary with entry count and elapsed time. The cached short-circuit stays silent. * feat: include lockfile path in verification log and render when non-standard Add `lockfilePath` to the `pnpm:lockfile-verification` event payload so consumers always know which lockfile a `started`/`done` pair refers to. In the default reporter, render the path in the message only when the lockfile lives outside the workspace root (or, for non-workspace installs, outside cwd) — the common case stays uncluttered, while custom `lockfileDir` setups now surface in the verification line. * feat: name what the lockfile verification actually checks in the rendered message "Verifying lockfile" was opaque about *what* was being verified. Reword the rendered messages to explicitly name the check ("supply-chain policies"), so users on a cold-cache pause understand what's happening instead of just seeing the pause. * fix: skip lockfile verification emission for empty candidate set A non-empty lockfile.packages whose snapshots all fail name/version extraction would still emit a "Verifying lockfile (0 entries)" line even though no verifier work runs. Bail before emission when the candidate map is empty so the no-op branch stays silent, matching the contract for the other no-op branches (empty verifiers, no lockfile.packages). * fix(reporter): always close out the verifying-lockfile frame Address two Copilot review points on #11712: 1. The verifier emitted `started` but no terminal event when violations were found or when the registry fan-out threw, leaving "Verifying lockfile…" as the last frame for that block in ansi-diff mode (and an unmatched line in CI logs). Add a `failed` status to the logger, wrap the fan-out in try/finally so a terminal event is emitted on every exit path that emitted `started`, and render a brief failure line so the spinner-style frame is replaced before the PnpmError block prints. 2. The path-suppression heuristic used strict `===` between path.dirname(lockfilePath) and expectedDir, which broke on trailing separators and slash-direction differences. Switch to a path.relative-based check so a workspaceDir like `/repo/` or a Windows path with mixed slashes still correctly suppresses the redundant "at <path>" suffix. * docs: update lockfile verification logging behavior The lockfile verifier now emits log events during the registry round-trip pass, improving user visibility into the process.
151 lines
5.1 KiB
TypeScript
151 lines
5.1 KiB
TypeScript
import path from 'node:path'
|
|
import { stripVTControlCharacters as stripAnsi } from 'node:util'
|
|
|
|
import { expect, test } from '@jest/globals'
|
|
import { toOutput$ } from '@pnpm/cli.default-reporter'
|
|
import type { Config, ConfigContext } from '@pnpm/config.reader'
|
|
import { lockfileVerificationLogger } from '@pnpm/core-loggers'
|
|
import { createStreamParser } from '@pnpm/logger'
|
|
import { firstValueFrom, take, toArray } from 'rxjs'
|
|
|
|
test('prints lockfile verification in-progress and completion messages', async () => {
|
|
const cwd = '/repo'
|
|
const output$ = toOutput$({
|
|
context: {
|
|
argv: ['install'],
|
|
config: { dir: cwd } as Config & ConfigContext,
|
|
},
|
|
streamParser: createStreamParser(),
|
|
})
|
|
|
|
// Subscribe before emitting so we capture both the started and the
|
|
// done frame in ansi-diff mode.
|
|
const frames = firstValueFrom(output$.pipe(take(2), toArray()))
|
|
|
|
const lockfilePath = path.join(cwd, 'pnpm-lock.yaml')
|
|
lockfileVerificationLogger.debug({ status: 'started', entries: 234, lockfilePath })
|
|
lockfileVerificationLogger.debug({
|
|
status: 'done',
|
|
entries: 234,
|
|
elapsedMs: 1234,
|
|
lockfilePath,
|
|
})
|
|
|
|
const [started, done] = await frames
|
|
expect(stripAnsi(started)).toBe('? Verifying lockfile against supply-chain policies (234 entries)...')
|
|
expect(stripAnsi(done)).toBe('✓ Lockfile passes supply-chain policies (234 entries in 1.2s)')
|
|
})
|
|
|
|
test('uses singular noun for one entry', async () => {
|
|
const output$ = toOutput$({
|
|
context: { argv: ['install'] },
|
|
streamParser: createStreamParser(),
|
|
})
|
|
|
|
const frames = firstValueFrom(output$.pipe(take(2), toArray()))
|
|
|
|
lockfileVerificationLogger.debug({ status: 'started', entries: 1 })
|
|
lockfileVerificationLogger.debug({
|
|
status: 'done',
|
|
entries: 1,
|
|
elapsedMs: 42,
|
|
})
|
|
|
|
const [started, done] = await frames
|
|
expect(stripAnsi(started)).toBe('? Verifying lockfile against supply-chain policies (1 entry)...')
|
|
expect(stripAnsi(done)).toBe('✓ Lockfile passes supply-chain policies (1 entry in 42ms)')
|
|
})
|
|
|
|
test('prints relative path when lockfile lives outside the workspace root', async () => {
|
|
const cwd = '/repo/packages/app'
|
|
const workspaceDir = '/repo'
|
|
const output$ = toOutput$({
|
|
context: {
|
|
argv: ['install'],
|
|
config: { dir: cwd, workspaceDir } as Config & ConfigContext,
|
|
},
|
|
streamParser: createStreamParser(),
|
|
})
|
|
|
|
const frames = firstValueFrom(output$.pipe(take(2), toArray()))
|
|
|
|
// Lockfile lives in a sibling dir, not at the workspace root.
|
|
const lockfilePath = '/repo/locks/pnpm-lock.yaml'
|
|
lockfileVerificationLogger.debug({ status: 'started', entries: 5, lockfilePath })
|
|
lockfileVerificationLogger.debug({
|
|
status: 'done',
|
|
entries: 5,
|
|
elapsedMs: 200,
|
|
lockfilePath,
|
|
})
|
|
|
|
const [started, done] = await frames
|
|
expect(stripAnsi(started)).toBe('? Verifying lockfile at ../../locks/pnpm-lock.yaml against supply-chain policies (5 entries)...')
|
|
expect(stripAnsi(done)).toBe('✓ Lockfile at ../../locks/pnpm-lock.yaml passes supply-chain policies (5 entries in 200ms)')
|
|
})
|
|
|
|
test('does not print path when running from workspace subdir and lockfile is at workspace root', async () => {
|
|
const cwd = '/repo/packages/app'
|
|
const workspaceDir = '/repo'
|
|
const output$ = toOutput$({
|
|
context: {
|
|
argv: ['install'],
|
|
config: { dir: cwd, workspaceDir } as Config & ConfigContext,
|
|
},
|
|
streamParser: createStreamParser(),
|
|
})
|
|
|
|
const frames = firstValueFrom(output$.pipe(take(1), toArray()))
|
|
|
|
const lockfilePath = path.join(workspaceDir, 'pnpm-lock.yaml')
|
|
lockfileVerificationLogger.debug({ status: 'started', entries: 10, lockfilePath })
|
|
|
|
const [started] = await frames
|
|
expect(stripAnsi(started)).toBe('? Verifying lockfile against supply-chain policies (10 entries)...')
|
|
})
|
|
|
|
test('suppresses path when workspaceDir has a trailing separator', async () => {
|
|
const cwd = '/repo'
|
|
// Workspace dir with a trailing slash — strict === against
|
|
// path.dirname(lockfilePath) would mismatch; path.relative normalizes.
|
|
const workspaceDir = '/repo/'
|
|
const output$ = toOutput$({
|
|
context: {
|
|
argv: ['install'],
|
|
config: { dir: cwd, workspaceDir } as Config & ConfigContext,
|
|
},
|
|
streamParser: createStreamParser(),
|
|
})
|
|
|
|
const frames = firstValueFrom(output$.pipe(take(1), toArray()))
|
|
|
|
lockfileVerificationLogger.debug({
|
|
status: 'started',
|
|
entries: 3,
|
|
lockfilePath: '/repo/pnpm-lock.yaml',
|
|
})
|
|
|
|
const [started] = await frames
|
|
expect(stripAnsi(started)).toBe('? Verifying lockfile against supply-chain policies (3 entries)...')
|
|
})
|
|
|
|
test('emits a brief failure line on failed status', async () => {
|
|
const output$ = toOutput$({
|
|
context: { argv: ['install'] },
|
|
streamParser: createStreamParser(),
|
|
})
|
|
|
|
const frames = firstValueFrom(output$.pipe(take(2), toArray()))
|
|
|
|
lockfileVerificationLogger.debug({ status: 'started', entries: 12 })
|
|
lockfileVerificationLogger.debug({
|
|
status: 'failed',
|
|
entries: 12,
|
|
elapsedMs: 800,
|
|
})
|
|
|
|
const [started, failed] = await frames
|
|
expect(stripAnsi(started)).toBe('? Verifying lockfile against supply-chain policies (12 entries)...')
|
|
expect(stripAnsi(failed)).toBe('✗ Lockfile failed supply-chain policy check (12 entries in 800ms)')
|
|
})
|