install: migrate Yarn Berry (v2+) yarn.lock to bun.lock

Merged
MA
1mo ago
+5140-17

What does this PR do?

bun install already migrates package-lock.json, pnpm-lock.yaml and Yarn v1 yarn.lock when there is no bun.lock. A Yarn 2+ ("Berry") yarn.lock (same filename, but YAML with a __metadata: header) was rejected with UnsupportedYarnLockfileVersion, so the first bun install in a Berry project resolved every dependency again. This PR adds a Berry migration.

The rule the migration follows: every dependency in bun.lock is the package, version and source that yarn.lock pinned, or the lockfile is not migrated. When something cannot be carried over exactly, Bun prints the reason and resolves from package.json, which is what it did before this PR. bun pm migrate exits 1 in that case. A migration that is not done writes nothing: no bun.lock, and package.json is left byte for byte as it was.

What is migrated

yarn.lockbun.lock
root and workspace entriesbuilt from their package.json (workspaces from the root workspaces globs), so the dependency groups are the real ones
name@npm:1.2.3npm package. The tarball URL is ::__archiveUrl= when present, otherwise built on the registry Bun is configured with
alias@npm:real@^1npm: aliases
ranges as Yarn 4 writes them (npm:^1) and as Yarn 2/3 write them (^1, and unquoted 1 or 2.10, which YAML reads as numbers)the range text, unchanged
tarball URLs, git URLs with #commit=<sha>, github:o/rtarball / git dependency pinned to the locked commit
file:, portal:, link:folder dependency. Bun has no protocol for a linked folder outside workspaces, so portal: and link: ranges in package.json become file: and a notice says the folder is now copied
patch:<locator>#<file>the patch file goes to patchedDependencies; the patch: range in package.json becomes the range it patches. Yarn's builtin compatibility patches are dropped
dependenciesMeta, peerDependencies, peerDependenciesMeta, conditions, binoptional / peer behaviours, os / cpu, bins
catalog: rangesresolved through .yarnrc.yml; the catalogs are copied into package.json
resolutionsapplied the way Yarn applies them: the first matching entry wins, and parent@version/name names the version the parent is locked to
packageExtensions in .yarnrc.ymlthe dependencies they add are added to the packages they match
checksumnot used. It is the hash of Yarn's own zip of the package. Integrity comes from the registry manifest

What stops a migration

  • A dependency with no lockfile entry (yarn.lock is older than package.json), or a workspace in yarn.lock that the workspaces globs do not match.
  • A registry that Yarn is configured with and Bun is not. Yarn's registry is read from .yarnrc.yml in the project, in each parent folder and in the home folder, and from YARN_NPM_REGISTRY_SERVER. Without this check Bun would ask its own registry for a name that Yarn fetched from another one.
  • An npm package with no integrity from the registry manifest, for example an __archiveUrl on another host.
  • A patch that patchedDependencies cannot express: several patch files for one package, a patch on a folder dependency, a patch file outside the project.
  • A protocol Bun does not have (exec:, plugin protocols), or a git dependency on a package inside a repository (#workspace=).
  • A root dependency that has the name of a workspace and that Yarn installed from somewhere else. Bun links the workspace under that name.
  • A name in both dependencies and devDependencies when the version Yarn installed for the devDependencies range is not in the dependencies range. Yarn installs the first, Bun reads the second.
  • A __metadata.version outside 4 to 10, a key written twice, or a YAML merge key (<<). Yarn and Bun's YAML parser read the last two differently.
  • A command that does not write package.json (--dry-run, --no-save) when the migration has to edit it.

Other changes

  • The edits to package.json (rewritten ranges, patchedDependencies, catalogs) go through package_json_write_back, so they are written when bun.lock is saved and not before. bun pm migrate and bun install --lockfile-only now run that step.
  • bun remove writes the cached package.json, not the copy printed before the install, so an edit made while the lockfile loads is kept. bun patch --commit fetches the cache entry again after the lockfile loads. Both also apply to a pnpm migration.
  • Credentials in a registry URL are not printed in the error messages and are not written to the tarball URLs in bun.lock.
  • Lockfile::fetch_necessary_package_metadata_after_yarn_or_pnpm_migration gets a second const parameter that fills integrity from the manifest. Only the Berry migration turns it on.
  • Docs: docs/pm/cli/install.mdx has a "Yarn migration" section that lists what is migrated and what stops a migration.

Known limits

  • The extensions built into Yarn (the packageExtensions Yarn ships for well-known packages) are not applied. A fresh bun install does not apply them either.
  • A github: dependency is migrated as a git dependency pinned to the full commit, not as a GitHub tarball, so the first install needs git.
  • libc conditions, nodeLinker and supportedArchitectures are not migrated.
  • The pnpm migration writes registry credentials into tarball URLs in the same way the Berry one did before this PR (src/install/pnpm.rs). That is not changed here.

How did you verify your code works?

  • test/cli/install/migration/yarn-berry.test.ts: 58 tests against a local Verdaccio registry. Every fixture pins versions that are not the newest the registry has for the range, so a test passes only when the pin came from yarn.lock. Each "not migrated" case checks the message, exit code 1, that no bun.lock was written and that package.json is unchanged. All 58 fail on Bun 1.4.2.
  • The migration (at c114b88a80) was run with bun pm migrate on 30 public Yarn Berry projects (lockfile versions 4 to 10; babel, jest, storybook, grafana, backstage, cal.com, AFFiNE, Rocket.Chat and others). 20 migrate. In all 20, bun.lock holds no npm version that yarn.lock does not. 10 are not migrated, each with one of the reasons above: nested workspaces (Yarn's own repository, 3 versions), a condition: plugin protocol (babel 7.14, 7.20), a root dependency on the published copy of a workspace (babel main), a lockfile older than package.json (jest 27.0.0, storybook 6.5.0), a patch on top of a patch (MetaMask extension), a folder in dependencies and an npm version in devDependencies (grafana 9.0.0). None crashed.
  • Those runs found four defects that are fixed here and have tests: the workspace-name case, packageExtensions, a range that ends in a space, and a name in both dependency groups.