install: migrate Yarn Berry (v2+) yarn.lock to bun.lock
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.lock | bun.lock |
|---|---|
| root and workspace entries | built from their package.json (workspaces from the root workspaces globs), so the dependency groups are the real ones |
name@npm:1.2.3 | npm package. The tarball URL is ::__archiveUrl= when present, otherwise built on the registry Bun is configured with |
alias@npm:real@^1 | npm: 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/r | tarball / 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, bin | optional / peer behaviours, os / cpu, bins |
catalog: ranges | resolved through .yarnrc.yml; the catalogs are copied into package.json |
resolutions | applied 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.yml | the dependencies they add are added to the packages they match |
checksum | not 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.lockis older thanpackage.json), or a workspace inyarn.lockthat theworkspacesglobs do not match. - A registry that Yarn is configured with and Bun is not. Yarn's registry is read from
.yarnrc.ymlin the project, in each parent folder and in the home folder, and fromYARN_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
__archiveUrlon another host. - A patch that
patchedDependenciescannot 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
dependenciesanddevDependencieswhen the version Yarn installed for thedevDependenciesrange is not in thedependenciesrange. Yarn installs the first, Bun reads the second. - A
__metadata.versionoutside 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 throughpackage_json_write_back, so they are written whenbun.lockis saved and not before.bun pm migrateandbun install --lockfile-onlynow run that step. bun removewrites the cachedpackage.json, not the copy printed before the install, so an edit made while the lockfile loads is kept.bun patch --commitfetches 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_migrationgets a second const parameter that fills integrity from the manifest. Only the Berry migration turns it on.- Docs:
docs/pm/cli/install.mdxhas a "Yarn migration" section that lists what is migrated and what stops a migration.
Known limits
- The extensions built into Yarn (the
packageExtensionsYarn ships for well-known packages) are not applied. A freshbun installdoes 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 needsgit. libcconditions,nodeLinkerandsupportedArchitecturesare 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 fromyarn.lock. Each "not migrated" case checks the message, exit code 1, that nobun.lockwas written and thatpackage.jsonis unchanged. All 58 fail on Bun 1.4.2.- The migration (at c114b88a80) was run with
bun pm migrateon 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.lockholds no npm version thatyarn.lockdoes not. 10 are not migrated, each with one of the reasons above: nested workspaces (Yarn's own repository, 3 versions), acondition:plugin protocol (babel 7.14, 7.20), a root dependency on the published copy of a workspace (babel main), a lockfile older thanpackage.json(jest 27.0.0, storybook 6.5.0), a patch on top of a patch (MetaMask extension), a folder independenciesand an npm version indevDependencies(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.