Skip to content

fix(arborist): skip moving back nodes that were never retired - #10194

Open
tillschweneker wants to merge 1 commit into
npm:latestfrom
tillschweneker:fix/arborist-unretired-change
Open

tillschweneker wants to merge 1 commit into
npm:latestfrom
tillschweneker:fix/arborist-unretired-change

Conversation

@tillschweneker

@tillschweneker tillschweneker commented Oct 30, 2026 •

Copy link
Copy Markdown

This implements fix 1 from @amurashincertinia's root-cause analysis in #4907, with a regression test for the bundleDependencies path they predicted. Their other suggestions aren't included: never letting a rollback replace the original error, refreshing the retire map after each re-diff, and handling ENOTEMPTY in [_renamePath]. Those are separate improvements, and fix 1 already keeps an unretired folder out of the rollback.

npm install crashes with ERR_INVALID_ARG_TYPE: The "from" argument must be of type string. Received undefined when three things meet in one install:

  1. A changed node and an unchanged node link the same bin, e.g. sanity and @sanity/cli both install .bin/sanity.
  2. Retiring the changed node moves that bin aside.
  3. A changed package with bundleDependencies (or a shrinkwrap) makes reify re-diff the trees. getAction now finds the unchanged node's bin missing and marks it CHANGE, but it was never retired.

[moveBackRetiredUnchanged] then looks up an undefined retire folder and resolve() throws. [rollbackMoveBackRetiredUnchanged] throws the same error on relative(), which hides the first one. The debug-only check above it already called this case "should be impossible".

That node was never moved, so its unchanged children are still in place. This PR skips it instead of throwing.

The regression test packs four tiny local tarballs (two packages sharing a bin, one bundler and one nested dep), installs, then updates. Without the fix it fails with the error users report. With the fix, the arborist suite passes at 100% coverage.

I also checked the patch against a real project of about 2,800 packages that hit this nightly. The resulting node_modules, with every package version and every .bin link, is identical to a fresh npm ci of the same lockfile.

References

Fixes #4907
Fixes #4448

🤖 Generated with Claude Code

When a changed node and an unchanged node link the same bin, retiring the
changed node moves that bin aside. Loading a changed bundler (or a
shrinkwrap) re-diffs the trees; the unchanged node's bin is now missing,
so it becomes a CHANGE that was never retired. moveBackRetiredUnchanged
then resolves its children against an undefined retire folder and throws
ERR_INVALID_ARG_TYPE, which the rollback masks with the same error on
the "from" argument.

The node was never moved, so its unchanged children are still in place:
skip it instead of throwing.

Fixes: npm#4907
Fixes: npm#4448

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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] TypeError [ERR_INVALID_ARG_TYPE]: The "from" argument must be of type string. [BUG] Windows Arborist Crash on rollbackMoveBackRetiredUnchanged

1 participant