Skip to content

install: skip yarn.lock entries the migration cannot build a resolution for - #39001

Open
robobun wants to merge 1 commit into
mainfrom
farm/ef2aa52b/yarn-migrate-skip-unresolvable
Open

install: skip yarn.lock entries the migration cannot build a resolution for#39001
robobun wants to merge 1 commit into
mainfrom
farm/ef2aa52b/yarn-migrate-skip-unresolvable

Conversation

@robobun

@robobun robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

Problem

  • A yarn.lock entry the migration cannot build a resolution for is still appended to the lockfile as a package whose resolution tag is Uninitialized. Inputs that do this: a version line that is not semver (with or without resolved), no version line, and no resolved line on an npm: alias or URL-spec entry (install: migrate yarn.lock registry entries that have no resolved field #38985 handles plain registry entries without resolved; the inputs above still fall through with it).
  • bun install on such a project, debug build: panic: internal error: entered unreachable code at Resolution::eql (src/install/resolution.rs:523, no arm for Uninitialized), reached from the debug_assert! in Lockfile::append_package_with_id (src/install/lockfile.rs:2287) while Lockfile::clean_with_logger clones the package. Exit 134.
  • bun install (release) and bun pm migrate (any build): the bun.lock writer skips Uninitialized packages (src/install/lockfile/bun.lock.rs:707) but the edges pointing at them are still written, so the file lists the dependency without a package for it and the next bun install rejects it (Failed to resolve root prod dependency 'foo', InvalidLockfile, "Ignoring lockfile"). Nothing is reported at migration time; bun pm migrate exits 0.
  • Cause: migrate_yarn_lockfile (src/install/yarn.rs) assigns package ids in a first pass over the entries and computes resolutions in a second pass, and the second pass appended the package even when the resolution block fell through to Resolution::default().

Fix

  • When the resolution block comes back Uninitialized, the entry is not appended. A warning names the entry and the field at fault, in the shape the package-lock.json and pnpm-lock.yaml migrators use for entries they skip: skipped "foo@^1.0.0" from yarn.lock: invalid version "not-a-version" (or missing "version" field / missing "resolved" field). Suppressed under --silent like the other migrators' warnings.
  • Package ids are now taken from the position a package is appended at, and the first pass's ids are mapped through appended_package_ids afterwards (both the per-entry map and the multi-version scoped_packages lists), so the remaining packages still have id == index after a skip. A skipped entry maps to INVALID_PACKAGE_ID, so the later passes, which already check for it, give its dependents the same unresolved edge a spec with no yarn.lock entry gets today; the writer and bun install already handle that edge (bun install reports foo@^1.0.0 failed to resolve, exit 1, instead of crashing or writing a lockfile it will reject).
  • Why skipping the entry is the right shape: the other migrators skip and warn per entry rather than failing the whole file (failing would discard every other pin of the lockfile), and a stanza bun cannot use is exactly the situation the migrator's existing missing-entry path models. It also keeps the change out of the resolution block itself, which install: fix default-trusted lifecycle scripts being blocked after yarn.lock migration #38795, install: fix yarn.lock migration panic on tarball URLs with "/-/" right after the host #38803, install: name yarn.lock npm: alias packages after the alias spec, not the tarball URL #38948 and install: migrate yarn.lock registry entries that have no resolved field #38985 are all editing; the block is unchanged here.
  • Resolution::eql's unreachable!() is left alone: it is the assertion that caught this, and no valid lockfile has an Uninitialized package.
  • The unused package_dependents pass is removed: it indexed by package id without the invalid-id check and was never read.
  • Verified with test/cli/install/migration/yarn-lock-migration.test.ts, new describe "yarn.lock entries the migration cannot build a resolution for": bun install with a root dependency and with a transitive dependency on a skipped entry (warning, failed to resolve, exit 1), bun pm migrate over the five input shapes above plus a multi-spec entry (warning text, exit 0, the other entry still written), and a package whose other version is skipped (exercises the scoped_packages remap). On the unfixed debug build the install cases exit 134 with the panic above and none of the cases print the warning.
  • The rest of the file passes, including the large yarn-cli-repo snapshot byte for byte (it needs a timeout above the default under the debug build with or without this change, test(install): fix yarn-lock-migration yarn-cli-repo case under debug+ASAN #35377); a real-world 7900-line yarn.lock skips nothing.
  • The yarn cases of migrate.test.ts, lockfile-only.test.ts and nested-overrides.test.ts pass; cargo fmt --check and the source lints are clean.

Background

  • yarn v1 lockfile entry: a header of one or more name@range specs, then version, usually resolved (the tarball URL), usually integrity, and the entry's dependency ranges. The migrator works out a package's origin from these fields.
  • Resolution (src/install/resolution.rs): the tagged value a lockfile package carries saying where it comes from (Npm, RemoteTarball, Git, Workspace, ...). Resolution::default() is the Uninitialized tag; it is what the migrator's resolution block returns when no arm matched, and no consumer expects to see it on a package.
  • Package ids in a Lockfile are indices into lockfile.packages; a dependency edge is a PackageID in buffers.resolutions, and INVALID_PACKAGE_ID there means the edge is unresolved. The bun.lock writer skips such edges, and bun install reports non-optional ones after resolving.
  • Lockfile::clean_with_logger rebuilds the lockfile from the packages reachable from the root after every install, re-appending each one through append_package_with_id, whose debug assertion looks the package up again by name and resolution; that lookup is where eql ran on the Uninitialized resolution.
Before / after on the repro from the report

package.json {"dependencies":{"foo":"^1.0.0"}}, yarn.lock with one foo@^1.0.0 entry whose version line is "not-a-version", registry pointed at a closed port.

Before (debug build):

$ bun install
[..] migrated lockfile from yarn.lock
panic: internal error: entered unreachable code
  <bun_install::resolution::ResolutionType<u64>>::eql            src/install/resolution.rs:523
  <bun_install::lockfile_real::Lockfile>::get_package_id           src/install/lockfile.rs:2093
  <bun_install::lockfile_real::Lockfile>::append_package_with_id   src/install/lockfile.rs:2287
  <bun_install::lockfile_real::package::Package>::clone
  <bun_install::lockfile_real::Cloner>::flush
  <bun_install::lockfile_real::Lockfile>::clean_with_logger
exit 134

$ bun pm migrate -f        # exit 0, also what release `bun install` writes
  "workspaces": { "": { "dependencies": { "foo": "^1.0.0" } } },
  "packages": {}

After:

$ bun install
warn: skipped "foo@^1.0.0" from yarn.lock: invalid version "not-a-version"
[..] migrated lockfile from yarn.lock
error: foo@^1.0.0 failed to resolve
exit 1

$ bun pm migrate -f
warn: skipped "foo@^1.0.0" from yarn.lock: invalid version "not-a-version"
[..] migrated lockfile from yarn.lock
exit 0

bun pm migrate still writes the dependency without a package, as it does today for a spec with no entry at all; the warning is what changes there.

…on for

migrate_yarn_lockfile appended a package with an Uninitialized resolution
for entries whose version is not semver, that have no version line, or
that have no resolved line (npm: aliases, URL specs). Debug builds then
panicked in Resolution::eql while cleaning the lockfile, and release
builds wrote a bun.lock that listed the dependency without a package, so
the next install rejected it.

Such entries are now skipped with a warning naming the entry and the
field at fault. Package ids are assigned as packages are appended, and
the entry-to-package map is rewritten through them, so a skipped entry
leaves its dependents with INVALID_PACKAGE_ID edges, the same as a spec
with no yarn.lock entry. Also drops the unused package_dependents pass,
which would have indexed by the invalid id.
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 27 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: fffdafb3-e2a9-48e6-a94f-601af25ce5ce

📥 Commits

Reviewing files that changed from the base of the PR and between 732491c and 83d7dfe.

📒 Files selected for processing (2)
  • src/install/yarn.rs
  • test/cli/install/migration/yarn-lock-migration.test.ts

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Status: fix and tests pushed, self-review in progress.

Reproduced on an unfixed debug build with a one-entry yarn.lock whose version line is "not-a-version": bun install panics in Resolution::eql (src/install/resolution.rs:523) from Lockfile::clean_with_logger, exit 134; bun pm migrate -f exits 0 and writes a bun.lock that lists foo under the root with no package for it. With this branch, both print warn: skipped "foo@^1.0.0" from yarn.lock: invalid version "not-a-version"; install then reports foo@^1.0.0 failed to resolve (exit 1) and migrate exits 0.

Tests: test/cli/install/migration/yarn-lock-migration.test.ts, describe "yarn.lock entries the migration cannot build a resolution for" (9 cases). Related: #38985 migrates plain registry entries without resolved; this PR covers what still falls through there.

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. install: migrate yarn.lock registry entries that have no resolved field #38985 - Fixes the same Resolution::default()/Uninitialized fall-through in migrate_yarn_lockfile that panics Resolution::eql, but resolves the shared "registry entry with no resolved field" case by building an Npm resolution instead of skipping it.

🤖 Generated with Claude Code

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Not a duplicate of #38985; the two are complementary and both PR descriptions point at each other.

  • install: migrate yarn.lock registry entries that have no resolved field #38985 changes one arm of the resolution block so a plain registry entry with a version but no resolved line produces an Npm resolution. It leaves the other fall-throughs in place (its own description lists them): a version that is not semver, with or without resolved; no version line; and npm: alias or URL-spec entries without resolved, which it turns into an explicit Resolution::default().
  • This PR does not touch the resolution block. It changes what happens to an entry once the block has returned Uninitialized, whichever arm produced it: the entry is skipped with a warning instead of being appended as a package, and the package ids are remapped so the remaining packages stay consistent.
  • Every case in the describe added here uses one of the inputs install: migrate yarn.lock registry entries that have no resolved field #38985 still falls through on (the one "no resolved" row also has a non-semver version), so with install: migrate yarn.lock registry entries that have no resolved field #38985 applied and this change absent they still panic (debug) or write the ghost dependency, and with both applied they pass unchanged. The only expected merge overlap is both PRs appending a describe to the end of the same test file.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed this PR and didn't find any bugs. Because it introduces a two-pass package-id remapping in the yarn.lock migrator and deletes ~80 lines of the package_dependents pass, a human look would still be worthwhile.

What was reviewed:

  • Traced every downstream reader of yarn_entry_to_package_id after the remap — each already guards on INVALID_PACKAGE_ID or writes it into resolutions as an unresolved edge; the one that didn't (package_names) gets a guard here.
  • Confirmed package_versions and package_id_to_yarn_idx are not read after the append loop, so they need no remapping; appended_package_ids[0] (root) is never indexed.
  • Verified package_dependents had no readers anywhere in src/ — the pass was dead, and would have panicked on a remapped INVALID_PACKAGE_ID index if kept.
  • New tests are hermetic (local Bun.serve registry, per-test cache dir), drain pipes concurrently, and cover root/transitive deps, all five uninitialized-resolution shapes, multi-spec entries, and the scoped_packages retain path.
Extended reasoning...

Overview

The PR changes migrate_yarn_lockfile in src/install/yarn.rs so that a yarn.lock entry whose resolution block returns ResolutionTag::Uninitialized is skipped (with a warning) instead of being appended as a package with an invalid resolution. Because package ids must equal their index in this.packages, it introduces an appended_package_ids vector to remap the first-pass ids to the actually-appended positions, then rewrites yarn_entry_to_package_id and prunes scoped_packages through it. It also removes an ~80-line package_dependents computation that was never read, resizes package_names to this.packages.len(), and adds an INVALID_PACKAGE_ID guard to the package_names loop. A new describe.concurrent block in yarn-lock-migration.test.ts covers the change.

Security risks

None. This is lockfile-format migration logic operating on local file input; no auth, crypto, network trust, or path handling changes. The removed code was pure in-memory bookkeeping.

Level of scrutiny

Medium-high. The migrator writes bun.lock, and a wrong package-id after remapping would silently corrupt migrated lockfiles for users whose yarn.lock has both a skippable entry and other packages after it. The id-remapping is the load-bearing part and is subtle enough (two id spaces, one used only as a dedup key inside the append loop, one used everywhere after) that a maintainer familiar with the install lockfile invariants should confirm the shape.

Other factors

  • I traced every consumer of yarn_entry_to_package_id after the remap: process_deps (writes the id into res_buf, INVALID is the intended unresolved-edge value), the root-deps first pass (same), the per-entry deps loop, package_names, usage_count/root_packages/scoped_names/spec_to_package_id/final deps loops — all either already checked INVALID_PACKAGE_ID or now do. package_versions and package_id_to_yarn_idx are only read inside their own building loops (pre-remap) so need no update; index 0 of appended_package_ids is never accessed because first-pass ids start at 1.
  • The deleted package_dependents block has no remaining references in src/; it built a Vec<Vec> and never read it. Keeping it would have required its own INVALID guard (it indexed by yarn_entry_to_package_id[idx]), so removal is both cleanup and correctness.
  • Test coverage is thorough: both bun install (exercises the crash path) and bun pm migrate, root vs transitive dependents, five distinct input shapes plus a multi-spec header, and a same-name/other-version case for the scoped_packages retain. Tests use a local port: 0 manifest-only registry and a per-test cache dir, so they're hermetic. The PR states the existing yarn-cli-repo snapshot is byte-identical, which is the main guard against regressing the no-skip path.
  • Not approving because this is not a simple/mechanical change: the remapping scheme and the dead-code deletion are the kind of thing an install-code owner should sanity-check, even though I found nothing wrong.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

For whoever picks this up, the id remapping reduces to two things to check:

  • id == index holds by construction: appended_id is this.packages.len() read immediately before the only packages.append in the loop, and it is what goes into meta.id and into appended_package_ids.
  • The first-pass ids survive the loop in exactly two places, yarn_entry_to_package_id and the scoped_packages lists, and both are rewritten through appended_package_ids in the block right after the loop, so everything below it only ever sees appended ids or INVALID_PACKAGE_ID. When nothing is skipped the map is the identity, which is what the unchanged yarn-cli-repo snapshot (about 1300 package entries) checks; the "other version is skipped" test is the non-identity case.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 10:05 AM PT - Aug 15th, 2026

@robobun, your commit 83d7dfe has 1 failures in Build #97930 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 39001

That installs a local version of the PR into your bun-39001 executable, so you can run:

bun-39001 --bun

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant