Skip to content

[BUG] install-strategy=linked re-creates a removed workspace's directory on npm install #9331

@manzoorwanijk

Description

@manzoorwanijk

Is there an existing issue for this?

  • I have searched the existing issues

This issue exists in the latest npm version

  • I am using the latest npm

While trying to fix #5463 via #9330 I found this issue

Current Behavior

When a workspace's directory has been deleted from disk and the entry has been removed from the root package.json's workspaces array, running npm install --install-strategy=linked re-creates the deleted workspace directory as an empty shell containing a self-referential symlink:

packages/b/
└── node_modules/
    └── b -> ..

No source files (no package.json, no index.js) are restored — just the directory and a <workspace>/node_modules/<name> -> .. symlink loop. The symlink target (..) points at the empty packages/b/ directory itself, so resolving it leads back to the same empty shell.

The hoisted (default) strategy does not exhibit this — the deleted directory stays deleted.

Expected Behavior

A workspace removed from package.json's workspaces array and deleted from disk should remain absent after npm install, regardless of install-strategy. The linked strategy should match the hoisted strategy's behavior here.

Steps To Reproduce

PROJ=/Users/me/tmp-cli-test  # any non-/tmp path; /tmp is /private/tmp via symlink and confuses npm
rm -rf "$PROJ" && mkdir -p "$PROJ/packages/a" "$PROJ/packages/b" && cd "$PROJ"

cat > .npmrc << 'EOF'
install-strategy=linked
EOF

cat > package.json << 'EOF'
{ "name": "host", "version": "1.0.0", "workspaces": ["packages/a", "packages/b"] }
EOF
echo '{ "name": "a", "version": "1.0.0" }' > packages/a/package.json
echo '{ "name": "b", "version": "1.0.0" }' > packages/b/package.json

npm install >/dev/null

# Remove workspace b: drop the directory AND the entry in package.json
rm -rf packages/b
node -e "const p=require('./package.json');p.workspaces=['packages/a'];require('fs').writeFileSync('./package.json',JSON.stringify(p,null,2))"

test -d packages/b && echo "before reinstall: present" || echo "before reinstall: gone"
npm install >/dev/null
test -d packages/b && echo "after reinstall:  present" || echo "after reinstall:  gone"

# Inspect what got recreated
find packages/b
readlink packages/b/node_modules/b

Observed

before reinstall: gone
after reinstall:  present
packages/b
packages/b/node_modules
packages/b/node_modules/b
..                          # ← packages/b/node_modules/b -> ..

The recreated directory contains only a self-loop symlink. There is no packages/b/package.json and no source files.

Comparison: hoisted strategy

Repeating the same steps without install-strategy=linked (or with install-strategy=hoisted) yields:

before reinstall: gone
after reinstall:  gone

Environment

  • npm: built from npm/cli branch fix/prune-removed-workspace-from-package-lock at commit ea023dd0f
  • Node.js: v24.15.0
  • OS: macOS (Darwin 25.4.0)
  • npm config:
install-strategy=linked

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions