mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-28 09:55:39 -04:00
* 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
152 lines
5.0 KiB
TypeScript
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,
|
|
}
|
|
}
|