Last pnpm version that worked
10.33.0
pnpm version
11.1.1
Code to reproduce the issue
# Setup
mkdir -p my-app my-lib
echo '{"name": "my-lib", "version": "1.0.0"}' > my-lib/package.json
echo 'export const value = "original";' > my-lib/index.js
cd my-app
echo '{"name": "my-app", "version": "1.0.0", "dependencies": {"my-lib": "file:../my-lib"}}' > package.json
pnpm install
# Verify initial state
cat node_modules/my-lib/index.js
# => export const value = "original";
# Modify the source directory.
rm ../my-lib/index.js
echo 'export const value = "changed";' > ../my-lib/index.js
# Try to pick up the change
pnpm install
# => "Already up to date" (WRONG - source has changed)
pnpm install --force
# => "Already up to date" (WRONG - even with --force)
cat node_modules/my-lib/index.js
# => export const value = "original"; (STALE)
# Workaround: pnpm update does pick up the change
pnpm update my-lib
cat node_modules/my-lib/index.js
# => export const value = "changed"; (CORRECT)
On pnpm v10, the second pnpm install correctly detects the change and re-imports the updated files from the source directory.
On pnpm v11, the second pnpm install returns "Already up to date" and node_modules remains stale. The only way to pick up the change is pnpm update my-lib or rm -rf node_modules && pnpm install.
Expected behavior
pnpm install should detect that files in a file: directory dependency have changed and re-import them into node_modules, as it did in v10.
The pnpm link documentation explicitly states that with the file: protocol, "you can modify the source code of the linked package, and the changes will be reflected in your project." This is no longer the case in v11.
Either this regression should be fixed, or this change in behaviour should have been called out in the PNPM v11 announcement, and the documentation should be updated, with an official recommendation to use pnpm update for directory-based file: dependencies (while still being able to rely on pnpm install for tarball based file: dependencies).
Actual behavior
pnpm install (and pnpm install --force) reports "Already up to date" and does not re-import changed files from file: directory dependencies. Changes are silently ignored.
Additional information
Root cause
PR #10439 ("refactor: move out skip resolution logic from package requester", commit 0bcbaf999) introduced a short-circuit in the local resolver for type: directory dependencies.
In resolving/local-resolver/src/index.ts, the following block was added:
// Skip resolution if we have a current package and not updating
if (opts.currentPkg?.resolution && spec.type === 'directory' && !opts.update) {
return {
id: opts.currentPkg.id,
resolution: opts.currentPkg.resolution as DirectoryResolution,
resolvedVia: 'local-filesystem',
}
}
When pnpm install is run (where opts.update is falsy) and a resolution already exists in the lockfile, this returns the cached resolution without reading the directory contents at all. The directory fetcher in fetching/directory-fetcher/src/index.ts is never invoked, so no file hashing or comparison occurs.
In v10, the local resolver (resolving/local-resolver/src/index.ts at v10.9.0) had no such short-circuit. It always read the manifest and returned a fresh resolution, allowing the fetcher to detect changes downstream.
Note that this only affects spec.type === 'directory' (the file: protocol pointing to a directory). The spec.type === 'file' path (tarballs) correctly computes a fresh integrity hash via getTarballIntegrity every time and compares it against the cached value.
Workaround
pnpm update <pkg> sets opts.update, bypassing the short-circuit. This can be used in place of pnpm install after modifying a file: directory dependency.
Why link: is not a viable alternative
The obvious suggestion is to use link: instead of file:, since symlinks reflect changes immediately. However, link: dependencies are fundamentally different in how pnpm resolves their transitive dependency tree: overrides entries in pnpm-workspace.yaml are not applied to the dependency graph of link: packages the way they are for file: packages.
This matters in practice when a local package has transitive dependencies that need to be overridden. For example, if my-lib depends on shared-utils@^1.0.0 and you need to override shared-utils to a local version via overrides in pnpm-workspace.yaml:
# pnpm-workspace.yaml
overrides:
"shared-utils": "file:.yalc/shared-utils"
With file:../my-lib, the override is applied and shared-utils resolves to the local copy throughout the entire dependency graph. With link:../my-lib, the override is ignored for my-lib's own dependency tree — shared-utils resolves to the registry version instead.
This makes link: unusable for local development workflows involving tools like yalc or npm pack where the local package has transitive dependencies that also need to be resolved locally. In these scenarios, file: is the only protocol that works, and this regression removes the ability to iterate on those local packages without manual cache-busting.
Additional context
This regression breaks workflows that rely on file: directory dependencies updating on pnpm install, including:
- yalc (
yalc push + pnpm install to pick up changes)
- Any local development workflow where a
file: dependency points to a build output directory that changes frequently
- Monorepo setups using
file: instead of workspace: for cross-package references
Node.js version
22.22.2
Operating System
Linux
Last pnpm version that worked
10.33.0
pnpm version
11.1.1
Code to reproduce the issue
On pnpm v10, the second
pnpm installcorrectly detects the change and re-imports the updated files from the source directory.On pnpm v11, the second
pnpm installreturns "Already up to date" andnode_modulesremains stale. The only way to pick up the change ispnpm update my-liborrm -rf node_modules && pnpm install.Expected behavior
pnpm installshould detect that files in afile:directory dependency have changed and re-import them intonode_modules, as it did in v10.The pnpm link documentation explicitly states that with the
file:protocol, "you can modify the source code of the linked package, and the changes will be reflected in your project." This is no longer the case in v11.Either this regression should be fixed, or this change in behaviour should have been called out in the PNPM v11 announcement, and the documentation should be updated, with an official recommendation to use
pnpm updatefor directory-basedfile:dependencies (while still being able to rely onpnpm installfor tarball basedfile:dependencies).Actual behavior
pnpm install(andpnpm install --force) reports "Already up to date" and does not re-import changed files fromfile:directory dependencies. Changes are silently ignored.Additional information
Root cause
PR #10439 ("refactor: move out skip resolution logic from package requester", commit
0bcbaf999) introduced a short-circuit in the local resolver fortype: directorydependencies.In
resolving/local-resolver/src/index.ts, the following block was added:When
pnpm installis run (whereopts.updateis falsy) and a resolution already exists in the lockfile, this returns the cached resolution without reading the directory contents at all. The directory fetcher infetching/directory-fetcher/src/index.tsis never invoked, so no file hashing or comparison occurs.In v10, the local resolver (
resolving/local-resolver/src/index.tsat v10.9.0) had no such short-circuit. It always read the manifest and returned a fresh resolution, allowing the fetcher to detect changes downstream.Note that this only affects
spec.type === 'directory'(thefile:protocol pointing to a directory). Thespec.type === 'file'path (tarballs) correctly computes a fresh integrity hash viagetTarballIntegrityevery time and compares it against the cached value.Workaround
pnpm update <pkg>setsopts.update, bypassing the short-circuit. This can be used in place ofpnpm installafter modifying afile:directory dependency.Why
link:is not a viable alternativeThe obvious suggestion is to use
link:instead offile:, since symlinks reflect changes immediately. However,link:dependencies are fundamentally different in how pnpm resolves their transitive dependency tree:overridesentries inpnpm-workspace.yamlare not applied to the dependency graph oflink:packages the way they are forfile:packages.This matters in practice when a local package has transitive dependencies that need to be overridden. For example, if
my-libdepends onshared-utils@^1.0.0and you need to overrideshared-utilsto a local version viaoverridesinpnpm-workspace.yaml:With
file:../my-lib, the override is applied andshared-utilsresolves to the local copy throughout the entire dependency graph. Withlink:../my-lib, the override is ignored formy-lib's own dependency tree —shared-utilsresolves to the registry version instead.This makes
link:unusable for local development workflows involving tools like yalc ornpm packwhere the local package has transitive dependencies that also need to be resolved locally. In these scenarios,file:is the only protocol that works, and this regression removes the ability to iterate on those local packages without manual cache-busting.Additional context
This regression breaks workflows that rely on
file:directory dependencies updating onpnpm install, including:yalc push+pnpm installto pick up changes)file:dependency points to a build output directory that changes frequentlyfile:instead ofworkspace:for cross-package referencesNode.js version
22.22.2
Operating System
Linux