Repository navigation
Description
Verify latest release
- I verified that the issue exists in the latest pnpm release
pnpm version
12.11.2
Which area(s) of pnpm are affected?
CLI
Link to the code that reproduces this issue or a replay of the bug
The complete generated fixture is inline below; no external repository is needed.
Reproduction steps
Use the tested runtime below. In a new empty disposable directory, run the following. The local package has no dependencies or lifecycle scripts; the app has no package-manager pin, hooks or other dependencies.
set -eu
mkdir -p app local-plugin
printf '%s\n' '{"name":"fixture-app","version":"1.0.0","private":true}' > app/package.json
printf '%s\n' 'nodeLinker: hoisted' > app/pnpm-workspace.yaml
printf '%s\n' '{"name":"fixture-plugin","version":"1.0.0","type":"module","exports":"./index.js"}' > local-plugin/package.json
printf '%s\n' 'export default "fixture marker";' > local-plugin/index.js
cat app/package.json app/pnpm-workspace.yaml local-plugin/package.json local-plugin/index.js
cd app
pnpm --version
pnpm add ../local-plugin --store-dir ../store
node --input-type=module -e "console.log((await import('fixture-plugin')).default)"
pnpm remove fixture-plugin --store-dir ../store
cat package.json pnpm-lock.yaml
node -e "console.log(require('node:fs').lstatSync('node_modules/fixture-plugin').isSymbolicLink())"
node --input-type=module -e "console.log((await import('fixture-plugin')).default)"Describe the Bug
With nodeLinker: hoisted, removing a direct local link: dependency updates the manifest and lockfile but leaves its node_modules symlink. Node can still import the removed package from the existing tree, whereas a clean install of the resulting manifest and lockfile cannot import it.
Both add and remove exit with status 0. Remove reports the following, then the last two commands print true and fixture marker:
Already up to date
dependencies:
- fixture-plugin link:../local-plugin
The resulting manifest has no dependency declaration and the lockfile importer is .: {}, but node_modules/fixture-plugin remains a symlink to ../../local-plugin. I reproduced this twice in independent generated hoisted-link fixtures. A subsequent pnpm install --frozen-lockfile also exits 0 and leaves the stale symlink and successful import.
In otherwise equivalent controls, nodeLinker: isolated with the same local link, and nodeLinker: hoisted with pnpm add file:../local-plugin, both remove the installed entry; importing the package then fails with ERR_MODULE_NOT_FOUND. A fresh install of the post-removal manifest and lockfile also has no package entry and cannot import it.
A verified workaround is to unlink only the stale node_modules/fixture-plugin symlink. That makes the import fail as expected and leaves the sibling package files intact.
Expected Behavior
pnpm remove fixture-plugin should remove the direct dependency from package.json, pnpm-lock.yaml and node_modules, as described by pnpm help remove. The package has no transitive consumers, so a subsequent direct Node import should fail with ERR_MODULE_NOT_FOUND. This command does not use --lockfile-only.
Which Node.js version are you using?
v26.10.0
Which operating systems have you used?
- macOS
- Windows
- Linux
Native arm64 pnpm executable. The reproduction used a clean environment with disposable HOME, cache, configuration, data and store directories, without credentials. Other platforms and the current main runtime were not tested.
If your OS is a Linux based, which one it is? (Include the version if relevant)
Not applicable.
Contributing a fix
- I am working on a fix
Evidence
- Expected source: The tested CLI's
pnpm help removefirst line says "Removes packages fromnode_modulesand from the project'spackage.json"; the separate--lockfile-onlyoption explicitly skips installed-tree removal. - Failure source: The self-contained reproduction above prints
truefor the remaining symlink andfixture markerfor the post-removal import after a successful remove. - Evidence provenance: observed
- Local verification: reproduced
- Reproduction completeness: complete
Restoration check
Run the local-link reproduction with the hoisted linker. Add must still install a usable link and the pre-removal import must succeed. After successful removal, the manifest and lockfile must omit the dependency, the symlink must be absent, and direct Node import must fail with ERR_MODULE_NOT_FOUND. The isolated-link and hoisted-file controls should continue to remove their entries correctly. Removing the symlink must preserve the sibling package.
Triage assessment
- Impact level: P2
- Assessment status: supported
- Impact basis: A removed direct dependency remains importable in the existing installed tree but is absent from a clean installation, which can hide an undeclared import during development. The observed scope is the hoisted local-link case; this is runtime behavior, not cosmetic cleanup.
- Workaround status: available
- Workaround basis: Unlinking only the known stale package symlink was tested. It restores the failed import result while preserving the sibling package source and manifest.
Written by an agent (Codex, gpt-6.1-sol).
Activity
fix(remove): unlink a removed link: dependency under the hoisted link…