Repository navigation
Description
Verify latest release
- I verified that the issue exists in the latest pnpm release
pnpm version
v11 main d37c6d9, after #17065; checker-level reproduction, not whole-main CLI.
Which area(s) of pnpm are affected? (leave empty if unsure)
No response
Link to the code that reproduces this issue or a replay of the bug
No response
Reproduction steps
This is a checker-level reproduction on main, not a claim that the published pnpm 12 CLI has this regression.
The workspace has packages: [app, a], sharedWorkspaceLockfile: false, injectWorkspacePackages: true, dedupeInjectedDeps: false:
// app/package.json
{"name":"app","dependencies":{"a":"workspace:*"}}
// a/package.json
{"name":"a","version":"1.0.0","dependencies":{"renamed":"file:../b"}}
// b/package.json
{"name":"b","version":"1.0.0"}Official pnpm 11.28.6 successfully installs this entirely local/offline layout and writes these relevant entries to app/pnpm-lock.yaml:
packages:
a@file:../a:
resolution: {directory: ../a, type: directory}
b@file:../b:
resolution: {directory: ../b, type: directory}
snapshots:
a@file:../a:
dependencies:
renamed: b@file:../b
b@file:../b: {}With lockfileDir = /app, merging packages and snapshots entries as v11 does, localDirectoryDepIsUpToDate returns false for a@file:../a although no manifests changed. Replacing only the recorded child ref with file:../b makes it return true. This replacement is a diagnostic control, not a proposed workaround.
Equivalent focused regression test for pnpm11/lockfile/verification/test:
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { localDirectoryDepIsUpToDate } from '../src/localDirectoryDepIsUpToDate.js'
test('accepts a fresh aliased file child of a local directory dependency', async () => {
const root = await mkdtemp(path.join(os.tmpdir(), 'pnpm-aliased-child-'))
await mkdir(path.join(root, 'a'))
await mkdir(path.join(root, 'b'))
await writeFile(path.join(root, 'a/package.json'), JSON.stringify({
name: 'a', version: '1.0.0', dependencies: { renamed: 'file:../b' },
}))
await writeFile(path.join(root, 'b/package.json'), JSON.stringify({ name: 'b', version: '1.0.0' }))
const actual = await localDirectoryDepIsUpToDate({
lockfileDir: root,
workspaceDir: root,
manifestsByDir: {},
resolution: { autoInstallPeers: true, frozenLockfile: true },
}, {
resolution: { directory: 'a', type: 'directory' },
dependencies: { renamed: 'b@file:b' },
})
expect(actual).toBe(true) // current checker returns false
})The checker-level calls above were executed. This Jest snippet expresses the same case; I have not run the repository's full Jest/CLI suite.
Describe the Bug
I executed localDirectoryDepIsUpToDate from main commit d37c6d9 (same checker as merged #17065) against a real lockfile produced by official pnpm 11.28.6.
The source harness strips TypeScript types and bridges shared parsing/I/O dependencies to the official 11.28.6 bundle; the checker, dependency-spec helpers, peer-set and newly extracted resolver rules come from the pinned main sources. This is not a full build/run of the main CLI.
localSpecIsUpToDate removes peer suffixes but does not unwrap the name prefix before getLocalPath:
pnpm/pnpm11/lockfile/verification/src/localDirectoryDepIsUpToDate.ts
Lines 244 to 259 in d37c6d9
| async function localSpecIsUpToDate ( | |
| localDep: LocalDirectoryDep, | |
| { depName, currentSpec, lockfileDep }: DependencyOfLocalDirectoryDep | |
| ): Promise<boolean> { | |
| if (currentSpec.startsWith('link:')) { | |
| return ( | |
| lockfileDep.startsWith('link:') && | |
| resolveSpecPath(localDep.localDepDir, currentSpec.slice(5)) === resolveSpecPath(localDep.lockfileDir, lockfileDep.slice(5)) | |
| ) | |
| } | |
| const cleanLockfileDep = removeSuffix(lockfileDep) | |
| const lockfilePath = getLocalPath(cleanLockfileDep) | |
| if (currentSpec.startsWith('file:')) { | |
| return localPathMatches(localDep, currentSpec.slice(5), lockfilePath) | |
| } | |
| return workspaceDependencyIsUpToDate(localDep, { depName, currentSpec, cleanLockfileDep, lockfilePath }) |
pnpm/pnpm11/lockfile/verification/src/dependencySpec.ts
Lines 16 to 20 in d37c6d9
| export function getLocalPath (lockfileDep: string): string | null { | |
| return lockfileDep.startsWith('link:') || lockfileDep.startsWith('file:') | |
| ? lockfileDep.slice(5) | |
| : null | |
| } |
getLocalPath('b@file:b') returns null because only strings starting with file: or link: are accepted. Path equality therefore fails for a canonical alias ref that pnpm itself writes. The direct importer check already recognizes name@file: refs; this is the child inside an injected/directory package.
Related: #17008, #17065. Main now enables this comparison on frozen installs, so the false verdict can surface as ERR_PNPM_OUTDATED_LOCKFILE. No pnpm 12 regression is claimed.
Environment: macOS 26.7.1 arm64, Node.js 26.2.0. Fixture generated by unmodified official pnpm 11.28.6. Analysis and source-level reproduction prepared and executed with Codex.
Expected Behavior
A freshly generated lockfile should pass this check. Parse/normalize an aliased local ref with package identity retained before comparing its path; peer/patch suffixes should continue to be handled correctly.
Which Node.js version are you using?
26.2.0
Which operating systems have you used?
- macOS
- Windows
- Linux
If your OS is a Linux based, which one it is? (Include the version if relevant)
No response
Contributing a fix
- I am working on a fix
Activity
fix(lockfile): accept an aliased file: child of an injected package (#…