mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-19 12:12:34 -04:00
fix(lockfile.utils): gate file: synthesis on refIsLocalDirectory
Synthesizing `{ type: 'directory' }` resolution for any depPath starting
with `file:` was too wide — `file:./foo.tgz`, `file:./foo.tar.gz`, and
`file:./foo.tar` are local-tarball refs that reach a different code path
in pnpm. Misclassifying them as directories would route them through
local-package install code with a `directory` field they don't have on
disk.
Use the existing `refIsLocalDirectory` helper (which is `startsWith('file:')`
minus the local-tarball extensions) so the synthesis branch only fires for
true workspace-directory refs.
Also expands the JSDoc with the helper's usage contract: every iteration
site that touches `pkgSnapshot.resolution` directly or via a downstream
utility (`pkgSnapshotToResolution`, `convertPackageSnapshot`, etc.) must
route its raw `lockfile.packages[depPath]` entry through this helper first.
Downstream utilities assume their input is already normalized.
Regression test added for the three local-tarball extensions.
This commit is contained in:
@@ -2,6 +2,8 @@ import * as dp from '@pnpm/deps.path'
|
||||
import type { PackageSnapshot, PackageSnapshots } from '@pnpm/lockfile.types'
|
||||
import type { DepPath } from '@pnpm/types'
|
||||
|
||||
import { refIsLocalDirectory } from './refIsLocalTarball.js'
|
||||
|
||||
/**
|
||||
* Normalize a package snapshot so its `resolution` field is always populated.
|
||||
*
|
||||
@@ -23,16 +25,27 @@ import type { DepPath } from '@pnpm/types'
|
||||
* 1. **Inherit from base.** Strip the peer-variant suffix from `depPath` and
|
||||
* look up the base entry. If it has a `resolution`, clone it onto the
|
||||
* variant.
|
||||
* 2. **Synthesize from `file:` depPath.** If the base entry has been pruned
|
||||
* (e.g. `turbo prune --docker` only keeps the variant referenced by the
|
||||
* consumer), synthesize a directory resolution from the `file:` prefix
|
||||
* in the depPath. This matches what pnpm's writer would have produced
|
||||
* for the base entry: workspace deps with peer-resolution variants live
|
||||
* under `packages/<name>` and serialize as `{ directory, type: 'directory' }`.
|
||||
* Do not widen this to other `file:` shapes (`file:./local-tarball.tgz`
|
||||
* etc.) — those reach a different code path.
|
||||
* 2. **Synthesize from a local-directory `file:` depPath.** If the base
|
||||
* entry has been pruned (e.g. `turbo prune --docker` only keeps the
|
||||
* variant referenced by the consumer), synthesize a directory resolution
|
||||
* from the `file:` prefix. This matches what pnpm's writer would have
|
||||
* produced for the base entry of a workspace dep with peer-resolution
|
||||
* variants (`packages/<name>` → `{ directory, type: 'directory' }`).
|
||||
*
|
||||
* Gated on `refIsLocalDirectory` so we don't misclassify local-tarball
|
||||
* `file:` refs (`file:...tgz`, `file:...tar.gz`, `file:...tar`) — those
|
||||
* reach a different code path in pnpm and must not get a `type: 'directory'`
|
||||
* resolution synthesized for them.
|
||||
* 3. **Give up.** Return the input untouched. Callers can decide whether
|
||||
* to throw, skip, or treat as a "broken lockfile" condition.
|
||||
*
|
||||
* **Usage contract:** every site that iterates `lockfile.packages` and
|
||||
* dereferences `pkgSnapshot.resolution` (directly or via `pkgSnapshotToResolution`,
|
||||
* `convertPackageSnapshot`, etc.) must route the raw snapshot through this
|
||||
* helper before doing so. Downstream utilities — including `pkgSnapshotToResolution`
|
||||
* — assume their input is already normalized; calling them with a raw
|
||||
* `lockfile.packages[depPath]` entry re-opens the original peer-variant
|
||||
* crash class.
|
||||
*/
|
||||
export function inheritOrSynthesizeResolution (
|
||||
depPath: string,
|
||||
@@ -46,7 +59,7 @@ export function inheritOrSynthesizeResolution (
|
||||
return { ...pkgSnapshot, resolution: baseSnapshot.resolution }
|
||||
}
|
||||
const nonSemverVersion = dp.parse(depPath).nonSemverVersion
|
||||
if (nonSemverVersion?.startsWith('file:')) {
|
||||
if (nonSemverVersion != null && refIsLocalDirectory(nonSemverVersion)) {
|
||||
return {
|
||||
...pkgSnapshot,
|
||||
resolution: { directory: nonSemverVersion.slice('file:'.length), type: 'directory' },
|
||||
|
||||
@@ -36,3 +36,21 @@ test('returns the input untouched when resolution cannot be inherited or synthes
|
||||
const input = {} as PackageSnapshot
|
||||
expect(inheritOrSynthesizeResolution('foo@1.0.0', input, undefined)).toBe(input)
|
||||
})
|
||||
|
||||
test('does not synthesize a directory resolution for local-tarball file: refs', () => {
|
||||
// `file:foo.tgz`, `file:foo.tar.gz`, `file:foo.tar` are local-tarball refs
|
||||
// (refIsLocalTarball) — they must NOT be turned into `{ type: 'directory' }`.
|
||||
// Without a base to inherit from, the synthesis branch must skip them and
|
||||
// return the input untouched so downstream code can flag the broken lockfile
|
||||
// instead of silently routing through the wrong code path.
|
||||
for (const tarballPath of [
|
||||
'foo@file:foo.tgz' as DepPath,
|
||||
'foo@file:foo.tar.gz' as DepPath,
|
||||
'foo@file:foo.tar' as DepPath,
|
||||
]) {
|
||||
const input = {} as PackageSnapshot
|
||||
const out = inheritOrSynthesizeResolution(tarballPath, input, undefined)
|
||||
expect(out).toBe(input)
|
||||
expect(out.resolution).toBeUndefined()
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user