fix(arborist): prune removed-workspace entries from package-lock.json#9330
Open
manzoorwanijk wants to merge 1 commit intonpm:latestfrom
Open
fix(arborist): prune removed-workspace entries from package-lock.json#9330manzoorwanijk wants to merge 1 commit intonpm:latestfrom
manzoorwanijk wants to merge 1 commit intonpm:latestfrom
Conversation
2 tasks
This was referenced May 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
When a workspace is removed from a project,
package-lock.jsonkeeps a stale entry for the removed location withextraneous: true(and a leftovernode_modules/<name>link in lockfile v2'sdependenciesblock).This happens regardless of the install strategy and regardless of whether the user also removed the workspace from
package.json'sworkspacesarray.The root cause is in
Shrinkwrap#commit()inworkspaces/arborist/lib/shrinkwrap.js.On every save,
commit()rebuildsdata.packagesby 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
calcDepFlagsmarks itextraneous.The current loop writes that node back to
data.packagesunconditionally, so the lockfile carries forward the entry indefinitely.Fixed by skipping the write for nodes that are
extraneousand whose lockfile location is not undernode_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 undernode_modules/) untouched.The legacy
dependenciesfield 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 thepackagesentry.The lockfile's
root.workspacesarray is intentionally left as a verbatim mirror ofpackage.json'sworkspaces.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.workspacesindependently ofpackage.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:dist/,*.tsbuildinfo) commonly survive a manualrm -rf packages/<ws>, so the directory still exists even though the user clearly intended to remove the workspace.commit()runs the directory is back on disk regardless of whether the user removed it.Pruning purely on
extraneous && not-in-node_moduleshandles both of these correctly without an fs call inside the lockfile write path.References
Fixes #5463
Supersedes #5478