Skip to content

fix(arborist): prune removed-workspace entries from package-lock.json#9330

Open
manzoorwanijk wants to merge 1 commit intonpm:latestfrom
manzoorwanijk:fix/prune-removed-workspace-from-package-lock
Open

fix(arborist): prune removed-workspace entries from package-lock.json#9330
manzoorwanijk wants to merge 1 commit intonpm:latestfrom
manzoorwanijk:fix/prune-removed-workspace-from-package-lock

Conversation

@manzoorwanijk
Copy link
Copy Markdown
Contributor

When a workspace is removed from a project, package-lock.json keeps a stale entry for the removed location with extraneous: true (and a leftover node_modules/<name> link in lockfile v2's dependencies block).
This happens regardless of the install strategy and regardless of whether the user also removed the workspace from package.json's workspaces array.

The root cause is in Shrinkwrap#commit() in workspaces/arborist/lib/shrinkwrap.js.
On every save, commit() rebuilds data.packages by iterating the ideal tree's inventory and writing one entry per node.
A workspace whose directory has been deleted (or whose declaration has been removed) survives in the inventory because it was loaded from the previous lockfile, but it has no incoming workspace edge from the root, so calcDepFlags marks it extraneous.
The current loop writes that node back to data.packages unconditionally, so the lockfile carries forward the entry indefinitely.

Fixed by skipping the write for nodes that are extraneous and whose lockfile location is not under node_modules/.
That precisely targets the "ghost workspace" shape: workspace-style locations (packages/b, app, e, etc.) that no longer have any reference in the project, while leaving real extraneous registry packages (which always live under node_modules/) untouched.
The legacy dependencies field in lockfile v2 is rebuilt by walking the tree from the root via #buildLegacyLockfile, so the orphan never gets visited and naturally drops out alongside the packages entry.

The lockfile's root.workspaces array is intentionally left as a verbatim mirror of package.json's workspaces.
This keeps the two files consistent: if the user kept the deleted workspace in package.json, the lockfile reflects that; if they cleaned it up, the lockfile is also clean.
The earlier proposal in #5478 mutated root.workspaces independently of package.json, which would have decoupled the two files and broken the existing "save some stuff" test that relies on the array surviving extraneous-flagged synthetic nodes.

An existsSync(node.realpath) gate was considered to limit the prune to "directory really gone" cases, but it has two practical bypasses that defeat it:

  • Gitignored build artifacts (e.g. dist/, *.tsbuildinfo) commonly survive a manual rm -rf packages/<ws>, so the directory still exists even though the user clearly intended to remove the workspace.
  • The linked install strategy re-materialises the workspace directory during reify, so by the time commit() runs the directory is back on disk regardless of whether the user removed it.

Pruning purely on extraneous && not-in-node_modules handles both of these correctly without an fs call inside the lockfile write path.

References

Fixes #5463
Supersedes #5478

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] package-lock is not cleaned up completely when workspace is removed

1 participant