Skip to content

pnpm install no longer detects changes in file: directory dependencies (regression from v10) #11795

Description

@johnbenjaminmccarthy

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions