Files
pnpm/installing/context/src/readLockfiles.ts
Zoltan Kochan a33c4bfcb0 perf: skip resolution when only pnpm-lock.yaml is missing (pnpm + pacquet) (#12004)
* perf: skip resolution when only pnpm-lock.yaml is missing

When pnpm-lock.yaml is absent but node_modules/.pnpm/lock.yaml exists and still
satisfies the manifest, reuse the materialized snapshot to regenerate the
wanted lockfile instead of walking the registry to rebuild it. Closes the
cache+node_modules variation gap in the vlt.sh benchmarks for the pnpm CLI
side; the pacquet port is tracked separately at #11993.

`--frozen-lockfile` still fails when pnpm-lock.yaml is absent: the regenerated
file must be committed, so failing loudly is the correct behavior for CI.

* perf(pacquet): port the cache+node_modules shortcut

When `pnpm-lock.yaml` is absent but `node_modules/.pnpm/lock.yaml` exists
and still satisfies the manifest, synthesize the wanted lockfile from the
materialized snapshot and take the frozen-install path. The install skips
resolution and regenerates `pnpm-lock.yaml` from the synthesized object.

Mirrors the pnpm-side change at 8a2146b7be (#12004). The synthesis path
preserves CI semantics: `--frozen-lockfile` still errors with
`NoLockfile` when `pnpm-lock.yaml` is missing, because the regenerated
file must be committed.

For workspace installs (where `pnpm-workspace.yaml` is present),
`optimistic_repeat_install` pre-empts the install with "Already up to
date" before the synthesis can fire — pnpm's `checkDepsStatus` has the
same gap. That's a separate parity fix; the integration test removes the
workspace-state file to exercise the dispatch path the synthesis lives
in. Real-world single-project installs hit the
`wanted lockfile missing` gate at `optimistic_repeat_install.rs:149`
directly and reach the synthesis without extra setup.

* style(pacquet): apply rustfmt

* refactor: inline lockfile-emptiness check instead of adding a derived flag
2026-05-28 00:47:45 +02:00

152 lines
5.0 KiB
TypeScript

import {
LOCKFILE_VERSION,
WANTED_LOCKFILE,
} from '@pnpm/constants'
import {
createLockfileObject,
existsNonEmptyWantedLockfile,
isEmptyLockfile,
type LockfileObject,
readCurrentLockfile,
readWantedLockfile,
readWantedLockfileAndAutofixConflicts,
} from '@pnpm/lockfile.fs'
import { logger } from '@pnpm/logger'
import type { ProjectId, ProjectRootDir } from '@pnpm/types'
import { clone, equals } from 'ramda'
export interface PnpmContext {
currentLockfile: LockfileObject
existsCurrentLockfile: boolean
existsWantedLockfile: boolean
existsNonEmptyWantedLockfile: boolean
wantedLockfile: LockfileObject
}
export async function readLockfiles (
opts: {
autoInstallPeers: boolean
excludeLinksFromLockfile: boolean
peersSuffixMaxLength: number
ci?: boolean
force: boolean
frozenLockfile: boolean
projects: Array<{
id: ProjectId
rootDir: ProjectRootDir
}>
lockfileDir: string
registry: string
useLockfile: boolean
useGitBranchLockfile?: boolean
mergeGitBranchLockfiles?: boolean
internalPnpmDir: string
}
): Promise<{
currentLockfile: LockfileObject
currentLockfileIsUpToDate: boolean
existsCurrentLockfile: boolean
existsWantedLockfile: boolean
existsNonEmptyWantedLockfile: boolean
wantedLockfile: LockfileObject
wantedLockfileIsModified: boolean
lockfileHadConflicts: boolean
}> {
const wantedLockfileVersion = LOCKFILE_VERSION
// On CI, avoid breaking builds due to incompatible lockfiles by default.
// Ignore incompatible lockfiles only for non-frozen CI installs or when `force` is set;
// in frozen-lockfile mode, incompatible lockfiles should still fail.
const lockfileOpts = {
ignoreIncompatible: opts.force || (opts.ci === true && !opts.frozenLockfile),
wantedVersions: [LOCKFILE_VERSION],
useGitBranchLockfile: opts.useGitBranchLockfile,
mergeGitBranchLockfiles: opts.mergeGitBranchLockfiles,
}
const fileReads = [] as Array<Promise<LockfileObject | undefined | null>>
let lockfileHadConflicts: boolean = false
if (opts.useLockfile) {
if (!opts.frozenLockfile) {
fileReads.push(
(async () => {
try {
const { lockfile, hadConflicts } = await readWantedLockfileAndAutofixConflicts(opts.lockfileDir, lockfileOpts)
lockfileHadConflicts = hadConflicts
return lockfile
} catch (err: any) { // eslint-disable-line
logger.warn({
message: `Ignoring broken lockfile at ${opts.lockfileDir}: ${err.message as string}`,
prefix: opts.lockfileDir,
})
return undefined
}
})()
)
} else {
fileReads.push(readWantedLockfile(opts.lockfileDir, lockfileOpts))
}
} else {
if (await existsNonEmptyWantedLockfile(opts.lockfileDir, lockfileOpts)) {
logger.warn({
message: `A ${WANTED_LOCKFILE} file exists. The current configuration prohibits to read or write a lockfile`,
prefix: opts.lockfileDir,
})
}
fileReads.push(Promise.resolve(undefined))
}
fileReads.push(
(async () => {
try {
return await readCurrentLockfile(opts.internalPnpmDir, lockfileOpts)
} catch (err: any) { // eslint-disable-line
logger.warn({
message: `Ignoring broken lockfile at ${opts.internalPnpmDir}: ${err.message as string}`,
prefix: opts.lockfileDir,
})
return undefined
}
})()
)
const files = await Promise.all<LockfileObject | null | undefined>(fileReads)
const sopts = {
autoInstallPeers: opts.autoInstallPeers,
excludeLinksFromLockfile: opts.excludeLinksFromLockfile,
lockfileVersion: wantedLockfileVersion,
peersSuffixMaxLength: opts.peersSuffixMaxLength,
}
const importerIds = opts.projects.map((importer) => importer.id)
const currentLockfile = files[1] ?? createLockfileObject(importerIds, sopts)
for (const importerId of importerIds) {
if (!currentLockfile.importers[importerId]) {
currentLockfile.importers[importerId] = {
specifiers: {},
}
}
}
const existsWantedLockfile = files[0] != null
const existsCurrentLockfile = files[1] != null
const wantedLockfile = files[0] ??
(currentLockfile && clone(currentLockfile)) ??
createLockfileObject(importerIds, sopts)
// Cloning the current lockfile means the disk copy of the wanted lockfile is
// stale, so flag it for rewriting after the install completes.
let wantedLockfileIsModified = !existsWantedLockfile && existsCurrentLockfile
for (const importerId of importerIds) {
if (!wantedLockfile.importers[importerId]) {
wantedLockfileIsModified = true
wantedLockfile.importers[importerId] = {
specifiers: {},
}
}
}
return {
currentLockfile,
currentLockfileIsUpToDate: equals(currentLockfile, wantedLockfile),
existsCurrentLockfile,
existsWantedLockfile,
existsNonEmptyWantedLockfile: existsWantedLockfile && !isEmptyLockfile(wantedLockfile),
wantedLockfile,
wantedLockfileIsModified,
lockfileHadConflicts,
}
}