From 21be069534a6d05de19612654bf29d7342e89bbe Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 13:32:22 +0000 Subject: [PATCH 1/5] install: name yarn.lock npm: alias packages after the alias spec, not the tarball URL An alias entry in a yarn v1 lockfile is keyed alias@npm:@, so the key line carries the real package name. The migrator instead guessed it from the resolved tarball URL and fell back to the alias name, which named the package wrongly for registries without the /-/ tarball layout and for URLs with /-/ directly after the host, and it consolidated entries by alias name and version, merging aliases of the same name that point at different packages into one package. Each entry now records the package name taken from its alias spec (or the spec name when the key line has no alias), and consolidation, both naming passes and the alias index registration use it. The URL lookup for alias names is deleted; tarball entries are still named by the name_to_use block. --- src/install/yarn.rs | 150 ++++++------- .../migration/yarn-lock-migration.test.ts | 197 +++++++++++++++--- 2 files changed, 222 insertions(+), 125 deletions(-) diff --git a/src/install/yarn.rs b/src/install/yarn.rs index cbcc3613b691..58863701d680 100644 --- a/src/install/yarn.rs +++ b/src/install/yarn.rs @@ -43,6 +43,8 @@ pub(crate) struct YarnLock<'a> { pub struct Entry<'a> { pub(crate) specs: Vec<&'a [u8]>, + /// Name of the package the entry resolves to; see `package_name_of`. + pub(crate) name: &'a [u8], pub(crate) version: &'a [u8], // Usually borrows from the input; owned when `parse_git_url` rewrites a // `github:` spec to a `https://github.com/...` URL. @@ -65,6 +67,7 @@ impl<'a> Default for Entry<'a> { fn default() -> Self { Self { specs: Vec::new(), + name: b"", version: b"", resolved: None, integrity: None, @@ -93,10 +96,39 @@ pub(crate) struct ParsedGitUrl<'a> { } pub(crate) struct ParsedNpmAlias<'a> { + pub(crate) name: &'a [u8], pub(crate) version: &'a [u8], } impl<'a> Entry<'a> { + /// An alias entry is keyed `alias@npm:@`, so the key line itself carries the + /// package name. yarn puts every pattern that resolved to the same tarball on one key line, + /// so the alias spec may come after plain `@` specs of the same package. + pub(crate) fn package_name_of(specs: &[&'a [u8]]) -> &'a [u8] { + if let Some(name) = specs.iter().find_map(|spec| Self::npm_alias_target(spec)) { + return name; + } + match specs.first() { + Some(spec) => Self::get_name_from_spec(spec), + None => b"", + } + } + + /// `alias@npm:@` -> ``; `None` for every other kind of spec. + pub(crate) fn npm_alias_target(spec: &[u8]) -> Option<&[u8]> { + let alias_end = strings::index_of(spec, b"@npm:")?; + let name = Self::parse_npm_alias(&spec[alias_end + 1..]).name; + (!name.is_empty()).then_some(name) + } + + /// Whether the dependency was declared as a tarball URL (`name@https://...`). + pub(crate) fn has_direct_url_spec(&self) -> bool { + self.specs.iter().any(|spec| { + strings::index_of(spec, b"@https://").is_some() + || strings::index_of(spec, b"@http://").is_some() + }) + } + pub(crate) fn get_name_from_spec(spec: &[u8]) -> &[u8] { let unquoted = if spec[0] == b'"' && spec[spec.len() - 1] == b'"' { &spec[1..spec.len() - 1] @@ -218,37 +250,23 @@ impl<'a> Entry<'a> { }) } + /// Splits `npm:@`. `` may be scoped, so the `@` that starts it is + /// skipped before looking for the separator; a missing range means `*`. pub(crate) fn parse_npm_alias(version: &[u8]) -> ParsedNpmAlias<'_> { - if version.len() <= 4 { - return ParsedNpmAlias { version: b"*" }; - } - - let npm_part = &version[4..]; - if let Some(at_idx) = strings::index_of(npm_part, b"@") { + let target = version.strip_prefix(b"npm:").unwrap_or(version); + let scope_len = usize::from(target.starts_with(b"@")); + let Some(at_idx) = strings::index_of_char_usize(&target[scope_len..], b'@') else { return ParsedNpmAlias { - version: if at_idx + 1 < npm_part.len() { - &npm_part[at_idx + 1..] - } else { - b"*" - }, + name: target, + version: b"*", }; + }; + let (name, range) = target.split_at(scope_len + at_idx); + let range = &range[b"@".len()..]; + ParsedNpmAlias { + name, + version: if range.is_empty() { b"*" } else { range }, } - ParsedNpmAlias { version: b"*" } - } - - /// Registry tarball URLs look like `//-/-.tgz`, - /// where `` spans two path segments (`@scope/name`) for scoped packages. - pub(crate) fn get_package_name_from_resolved_url(url: &[u8]) -> Option<&[u8]> { - let path = &url[..strings::index_of(url, b"/-/")?]; - let (prefix, name) = strings::rsplit_once_char(path, b'/')?; - if name.is_empty() { - return None; - } - let scope_start = strings::last_index_of_char(prefix, b'/').map_or(0, |slash| slash + 1); - if prefix[scope_start..].starts_with(b"@") { - return Some(&path[scope_start..]); - } - Some(name) } } @@ -308,6 +326,7 @@ impl<'a> YarnLock<'a> { let mut new_entry = Entry::<'a> { specs: current_specs.clone(), + name: Entry::package_name_of(¤t_specs), version: b"", // assigned below when "version" key is parsed ..Default::default() }; @@ -469,15 +488,10 @@ impl<'a> YarnLock<'a> { if new_entry.specs.is_empty() { return Ok(()); } - let package_name = Entry::get_name_from_spec(new_entry.specs[0]); for existing_entry in self.entries.iter_mut() { - if existing_entry.specs.is_empty() { - continue; - } - let existing_name = Entry::get_name_from_spec(existing_entry.specs[0]); - - if package_name == existing_name && new_entry.version == existing_entry.version { + if new_entry.name == existing_entry.name && new_entry.version == existing_entry.version + { let old_len = existing_entry.specs.len(); let mut combined_specs: Vec<&'a [u8]> = Vec::with_capacity(old_len + new_entry.specs.len()); @@ -811,30 +825,9 @@ pub(crate) fn migrate_yarn_lockfile<'a>( let mut next_package_id: PackageID = 1; // 0 is root for (yarn_idx, entry) in yarn_lock.entries.iter().enumerate() { - let mut is_npm_alias = false; - let mut is_direct_url = false; - for spec in entry.specs.iter() { - if strings::index_of(spec, b"@npm:").is_some() { - is_npm_alias = true; - break; - } - if strings::index_of(spec, b"@https://").is_some() - || strings::index_of(spec, b"@http://").is_some() - { - is_direct_url = true; - } - } - - let name: &[u8] = if let (true, Some(resolved)) = (is_npm_alias, entry.resolved.as_deref()) - { - Entry::get_package_name_from_resolved_url(resolved) - .unwrap_or_else(|| Entry::get_name_from_spec(entry.specs[0])) - } else if is_direct_url { - Entry::get_name_from_spec(entry.specs[0]) - } else if let Some(repo_name) = &entry.git_repo_name { - repo_name - } else { - Entry::get_name_from_spec(entry.specs[0]) + let name: &[u8] = match &entry.git_repo_name { + Some(repo_name) if !entry.has_direct_url_spec() => repo_name, + _ => entry.name, }; let version = entry.version; @@ -904,31 +897,8 @@ pub(crate) fn migrate_yarn_lockfile<'a>( let _ = &created_packages; // never populated for (yarn_idx, entry) in yarn_lock.entries.iter().enumerate() { - let mut is_npm_alias = false; - for spec in entry.specs.iter() { - if strings::index_of(spec, b"@npm:").is_some() { - is_npm_alias = true; - break; - } - } - - let mut is_direct_url_dep = false; - for spec in entry.specs.iter() { - if strings::index_of(spec, b"@https://").is_some() - || strings::index_of(spec, b"@http://").is_some() - { - is_direct_url_dep = true; - break; - } - } - - let base_name: &[u8] = - if let (true, Some(resolved)) = (is_npm_alias, entry.resolved.as_deref()) { - Entry::get_package_name_from_resolved_url(resolved) - .unwrap_or_else(|| Entry::get_name_from_spec(entry.specs[0])) - } else { - Entry::get_name_from_spec(entry.specs[0]) - }; + let is_direct_url_dep = entry.has_direct_url_spec(); + let base_name: &[u8] = entry.name; let package_id = yarn_entry_to_package_id[yarn_idx]; if (package_id as usize) < package_id_to_yarn_idx.len() @@ -1584,16 +1554,12 @@ pub(crate) fn migrate_yarn_lockfile<'a>( continue; } - if let Some(resolved) = entry.resolved.as_deref() { - if let Some(real_name) = Entry::get_package_name_from_resolved_url(resolved) { - for spec in entry.specs.iter() { - let alias_name = Entry::get_name_from_spec(spec); + for spec in entry.specs.iter() { + let alias_name = Entry::get_name_from_spec(spec); - if alias_name != real_name { - let alias_hash = string_hash(alias_name); - this.get_or_put_id(package_id, alias_hash)?; - } - } + if alias_name != entry.name { + let alias_hash = string_hash(alias_name); + this.get_or_put_id(package_id, alias_hash)?; } } } diff --git a/test/cli/install/migration/yarn-lock-migration.test.ts b/test/cli/install/migration/yarn-lock-migration.test.ts index de69143e155b..9f589aacac17 100644 --- a/test/cli/install/migration/yarn-lock-migration.test.ts +++ b/test/cli/install/migration/yarn-lock-migration.test.ts @@ -3,6 +3,41 @@ import fs from "fs"; import { bunEnv, bunExe, tempDir } from "harness"; import { join } from "path"; +// After migrating, bun fetches the manifest of every migrated package by the name it recorded, +// so the registry sees which package each alias was resolved to. +async function migrateYarnLock(dir: string) { + const requestedManifests = new Set(); + await using registry = Bun.serve({ + port: 0, + fetch(req) { + requestedManifests.add(decodeURIComponent(new URL(req.url).pathname.slice(1))); + return new Response("not found", { status: 404 }); + }, + }); + + await using proc = Bun.spawn({ + cmd: [bunExe(), "pm", "migrate", "-f"], + cwd: dir, + env: { + ...bunEnv, + BUN_CONFIG_REGISTRY: registry.url.href, + BUN_INSTALL_CACHE_DIR: join(dir, ".bun-cache"), + }, + stdout: "pipe", + stderr: "pipe", + stdin: "ignore", + }); + + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + + expect(stdout).toBe(""); + expect(stderr).toContain("migrated lockfile from yarn.lock"); + expect(exitCode).toBe(0); + + const lock = Bun.JSONC.parse(await Bun.file(join(dir, "bun.lock")).text()) as { packages: unknown }; + return { packages: lock.packages, requestedManifests: [...requestedManifests].sort() }; +} + describe("yarn.lock migration basic", () => { test("simple yarn.lock migration produces correct bun.lock", async () => { await using tmpDir = tempDir("yarn-migration-simple", { @@ -856,38 +891,8 @@ needs-node-types@1.0.0: `, }); - // After migrating, bun fetches the manifest of every migrated package by the name it - // recorded, so the registry sees which package each alias was resolved to. - const requestedManifests = new Set(); - await using registry = Bun.serve({ - port: 0, - fetch(req) { - requestedManifests.add(decodeURIComponent(new URL(req.url).pathname.slice(1))); - return new Response("not found", { status: 404 }); - }, - }); - - await using proc = Bun.spawn({ - cmd: [bunExe(), "pm", "migrate", "-f"], - cwd: tmpDir, - env: { - ...bunEnv, - BUN_CONFIG_REGISTRY: registry.url.href, - BUN_INSTALL_CACHE_DIR: join(tmpDir, ".bun-cache"), - }, - stdout: "pipe", - stderr: "pipe", - stdin: "ignore", - }); - - const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - - expect(stdout).toBe(""); - expect(stderr).toContain("migrated lockfile from yarn.lock"); - expect(exitCode).toBe(0); - - const lock = Bun.JSONC.parse(await Bun.file(join(tmpDir, "bun.lock")).text()) as { packages: unknown }; - expect(lock.packages).toStrictEqual({ + const { packages, requestedManifests } = await migrateYarnLock(tmpDir); + expect(packages).toStrictEqual({ "@aliased/node-types": ["@types/node@20.11.5", "", {}, typesNode20Integrity], "@types/node": ["@types/node@18.19.0", "", {}, typesNode18Integrity], "my-node-types": ["@types/node@20.11.5", "", {}, typesNode20Integrity], @@ -900,7 +905,133 @@ needs-node-types@1.0.0: "types-alias": ["@types/node@20.11.5", "", {}, typesNode20Integrity], }); - expect([...requestedManifests].sort()).toStrictEqual(["@types/node", "needs-node-types"]); + expect(requestedManifests).toStrictEqual(["@types/node", "needs-node-types"]); + }); + + test.concurrent("yarn.lock npm aliases are named after the alias spec, not the tarball URL", async () => { + const corpBareIntegrity = "sha512-" + Buffer.alloc(64, 2).toString("base64"); + const corpFooIntegrity = "sha512-" + Buffer.alloc(64, 3).toString("base64"); + const barIntegrity = "sha512-" + Buffer.alloc(64, 4).toString("base64"); + const stringWidthIntegrity = "sha512-" + Buffer.alloc(64, 5).toString("base64"); + // GitHub Packages does not use the registry's `/-/.tgz` tarball layout. + const corpFooTarball = + "https://npm.pkg.github.com/download/@corp/foo/1.0.0/0123456789abcdef0123456789abcdef01234567"; + + // Four alias shapes: an alias without a range, a tarball URL with no package name in it, a + // tarball URL whose "/-/" directly follows the host, and yarn's usual key line on which the + // alias spec sorts before the plain specs of the package it points at. + await using tmpDir = tempDir("yarn-migration-alias-spec-names", { + "package.json": JSON.stringify( + { + name: "alias-spec-names", + version: "1.0.0", + dependencies: { + "bare-alias": "npm:@corp/bare", + "gh-alias": "npm:@corp/foo@1.0.0", + "host-alias": "npm:bar@1.0.0", + "string-width": "^4.1.0", + "string-width-cjs": "npm:string-width@^4.2.0", + }, + }, + null, + 2, + ), + "yarn.lock": `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"bare-alias@npm:@corp/bare": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@corp/bare/-/bare-2.0.0.tgz#2222222222222222222222222222222222222222" + integrity ${corpBareIntegrity} + +"gh-alias@npm:@corp/foo@1.0.0": + version "1.0.0" + resolved "${corpFooTarball}" + integrity ${corpFooIntegrity} + +"host-alias@npm:bar@1.0.0": + version "1.0.0" + resolved "https://registry.npmjs.org/-/bar-1.0.0.tgz#4444444444444444444444444444444444444444" + integrity ${barIntegrity} + +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#5555555555555555555555555555555555555555" + integrity ${stringWidthIntegrity} +`, + }); + + const { packages, requestedManifests } = await migrateYarnLock(tmpDir); + expect(packages).toStrictEqual({ + "bare-alias": ["@corp/bare@2.0.0", "", {}, corpBareIntegrity], + "gh-alias": ["@corp/foo@1.0.0", corpFooTarball, {}, corpFooIntegrity], + "host-alias": ["bar@1.0.0", "", {}, barIntegrity], + "string-width": ["string-width@4.2.3", "", {}, stringWidthIntegrity], + "string-width-cjs": ["string-width@4.2.3", "", {}, stringWidthIntegrity], + }); + + expect(requestedManifests).toStrictEqual(["@corp/bare", "@corp/foo", "bar", "string-width"]); + }); + + test.concurrent("yarn.lock aliases of one name and version to different packages stay separate", async () => { + const xIntegrity = "sha512-" + Buffer.alloc(64, 6).toString("base64"); + const yIntegrity = "sha512-" + Buffer.alloc(64, 7).toString("base64"); + const fooIntegrity = "sha512-" + Buffer.alloc(64, 8).toString("base64"); + const bazIntegrity = "sha512-" + Buffer.alloc(64, 9).toString("base64"); + + await using tmpDir = tempDir("yarn-migration-alias-same-name", { + "package.json": JSON.stringify( + { + name: "alias-same-name", + version: "1.0.0", + dependencies: { + x: "1.0.0", + y: "1.0.0", + }, + }, + null, + 2, + ), + "yarn.lock": `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"same@npm:baz@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/baz/-/baz-1.0.0.tgz#9999999999999999999999999999999999999999" + integrity ${bazIntegrity} + +"same@npm:foo@1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/foo/-/foo-1.0.0.tgz#8888888888888888888888888888888888888888" + integrity ${fooIntegrity} + +x@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/x/-/x-1.0.0.tgz#6666666666666666666666666666666666666666" + integrity ${xIntegrity} + dependencies: + same "npm:foo@1.0.0" + +y@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/y/-/y-1.0.0.tgz#7777777777777777777777777777777777777777" + integrity ${yIntegrity} + dependencies: + same "npm:baz@1.0.0" +`, + }); + + const { packages, requestedManifests } = await migrateYarnLock(tmpDir); + expect(packages).toStrictEqual({ + "same": ["foo@1.0.0", "", {}, fooIntegrity], + "x": ["x@1.0.0", "", { dependencies: { same: "npm:foo@1.0.0" } }, xIntegrity], + "y": ["y@1.0.0", "", { dependencies: { same: "npm:baz@1.0.0" } }, yIntegrity], + "y/same": ["baz@1.0.0", "", {}, bazIntegrity], + }); + + expect(requestedManifests).toStrictEqual(["baz", "foo", "x", "y"]); }); test("yarn.lock with resolutions", async () => { From 44c4497ab04fcac25b644f9b2997c352c78144d2 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 15:01:43 +0000 Subject: [PATCH 2/5] install: consolidate yarn.lock entries by the same identity the package pass uses Keying consolidation on the package name alone merged a git entry keyed under a package's name (a fork keeping upstream's name and version) with an npm: alias of that package. The package-id pass identifies git entries by their repository; consolidation now uses the same Entry::dedupe_name, which keeps the two apart as before. --- src/install/yarn.rs | 19 ++++++--- .../migration/yarn-lock-migration.test.ts | 42 +++++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/install/yarn.rs b/src/install/yarn.rs index 58863701d680..a900f7167ff7 100644 --- a/src/install/yarn.rs +++ b/src/install/yarn.rs @@ -129,6 +129,16 @@ impl<'a> Entry<'a> { }) } + /// The name that, together with `version`, decides which entries become one package: the + /// repository for a git entry (its spec name is whatever the dependent chose to call it), + /// otherwise the package name. Consolidation and the package-id pass must agree on this. + pub(crate) fn dedupe_name(&self) -> &[u8] { + match &self.git_repo_name { + Some(repo_name) if !self.has_direct_url_spec() => repo_name, + _ => self.name, + } + } + pub(crate) fn get_name_from_spec(spec: &[u8]) -> &[u8] { let unquoted = if spec[0] == b'"' && spec[spec.len() - 1] == b'"' { &spec[1..spec.len() - 1] @@ -488,9 +498,11 @@ impl<'a> YarnLock<'a> { if new_entry.specs.is_empty() { return Ok(()); } + let dedupe_name = new_entry.dedupe_name(); for existing_entry in self.entries.iter_mut() { - if new_entry.name == existing_entry.name && new_entry.version == existing_entry.version + if dedupe_name == existing_entry.dedupe_name() + && new_entry.version == existing_entry.version { let old_len = existing_entry.specs.len(); let mut combined_specs: Vec<&'a [u8]> = @@ -825,10 +837,7 @@ pub(crate) fn migrate_yarn_lockfile<'a>( let mut next_package_id: PackageID = 1; // 0 is root for (yarn_idx, entry) in yarn_lock.entries.iter().enumerate() { - let name: &[u8] = match &entry.git_repo_name { - Some(repo_name) if !entry.has_direct_url_spec() => repo_name, - _ => entry.name, - }; + let name: &[u8] = entry.dedupe_name(); let version = entry.version; if let Some(existing) = package_versions.get(name).cloned() { diff --git a/test/cli/install/migration/yarn-lock-migration.test.ts b/test/cli/install/migration/yarn-lock-migration.test.ts index 9f589aacac17..0e93cc9959f8 100644 --- a/test/cli/install/migration/yarn-lock-migration.test.ts +++ b/test/cli/install/migration/yarn-lock-migration.test.ts @@ -1034,6 +1034,48 @@ y@1.0.0: expect(requestedManifests).toStrictEqual(["baz", "foo", "x", "y"]); }); + test.concurrent("yarn.lock git dependency keyed by a package name stays separate from an alias of it", async () => { + const lodashIntegrity = "sha512-" + Buffer.alloc(64, 10).toString("base64"); + const forkCommit = "0123456789abcdef0123456789abcdef01234567"; + + // The fork keeps upstream's name and version, so the two entries share a name and a version. + await using tmpDir = tempDir("yarn-migration-git-vs-alias", { + "package.json": JSON.stringify( + { + name: "git-vs-alias", + version: "1.0.0", + dependencies: { + "lodash": "git+https://github.com/me/lodash-fork.git", + "my-lodash": "npm:lodash@4.17.21", + }, + }, + null, + 2, + ), + "yarn.lock": `# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"lodash@git+https://github.com/me/lodash-fork.git": + version "4.17.21" + resolved "git+https://github.com/me/lodash-fork.git#${forkCommit}" + +"my-lodash@npm:lodash@4.17.21": + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + integrity ${lodashIntegrity} +`, + }); + + const { packages, requestedManifests } = await migrateYarnLock(tmpDir); + expect(packages).toStrictEqual({ + "lodash": [`lodash-fork@github:me/lodash-fork#${forkCommit.slice(0, 7)}`, {}, ""], + "my-lodash": ["lodash@4.17.21", "", {}, lodashIntegrity], + }); + + expect(requestedManifests).toStrictEqual(["lodash"]); + }); + test("yarn.lock with resolutions", async () => { await using tmpDir = tempDir("yarn-migration-resolutions", { "package.json": JSON.stringify( From c9a39ca59689ff6c190c47946140d985ae04d23b Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 15:05:49 +0000 Subject: [PATCH 3/5] install: shorten the yarn Entry doc comments --- src/install/yarn.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/install/yarn.rs b/src/install/yarn.rs index a900f7167ff7..e871f7477943 100644 --- a/src/install/yarn.rs +++ b/src/install/yarn.rs @@ -101,9 +101,8 @@ pub(crate) struct ParsedNpmAlias<'a> { } impl<'a> Entry<'a> { - /// An alias entry is keyed `alias@npm:@`, so the key line itself carries the - /// package name. yarn puts every pattern that resolved to the same tarball on one key line, - /// so the alias spec may come after plain `@` specs of the same package. + /// yarn lists every spec that resolved to the same tarball on one key line, so the alias spec + /// naming the package may come after plain specs of it. pub(crate) fn package_name_of(specs: &[&'a [u8]]) -> &'a [u8] { if let Some(name) = specs.iter().find_map(|spec| Self::npm_alias_target(spec)) { return name; @@ -129,9 +128,8 @@ impl<'a> Entry<'a> { }) } - /// The name that, together with `version`, decides which entries become one package: the - /// repository for a git entry (its spec name is whatever the dependent chose to call it), - /// otherwise the package name. Consolidation and the package-id pass must agree on this. + /// Package identity (with `version`) for both consolidation and the package-id pass: a git + /// entry is its repository, since its spec name is whatever the dependent called it. pub(crate) fn dedupe_name(&self) -> &[u8] { match &self.git_repo_name { Some(repo_name) if !self.has_direct_url_spec() => repo_name, @@ -260,8 +258,7 @@ impl<'a> Entry<'a> { }) } - /// Splits `npm:@`. `` may be scoped, so the `@` that starts it is - /// skipped before looking for the separator; a missing range means `*`. + /// Splits `npm:@`; the `@` of a scoped `` is not the separator. pub(crate) fn parse_npm_alias(version: &[u8]) -> ParsedNpmAlias<'_> { let target = version.strip_prefix(b"npm:").unwrap_or(version); let scope_len = usize::from(target.starts_with(b"@")); From 1072523b5af8b50c991636b720b8384340b1ee68 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 15:12:35 +0000 Subject: [PATCH 4/5] install: one-line doc comments on the yarn Entry helpers --- src/install/yarn.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/install/yarn.rs b/src/install/yarn.rs index e871f7477943..b4e7693ad16e 100644 --- a/src/install/yarn.rs +++ b/src/install/yarn.rs @@ -101,8 +101,7 @@ pub(crate) struct ParsedNpmAlias<'a> { } impl<'a> Entry<'a> { - /// yarn lists every spec that resolved to the same tarball on one key line, so the alias spec - /// naming the package may come after plain specs of it. + /// The alias spec may follow plain specs of the same package on yarn's shared key line. pub(crate) fn package_name_of(specs: &[&'a [u8]]) -> &'a [u8] { if let Some(name) = specs.iter().find_map(|spec| Self::npm_alias_target(spec)) { return name; @@ -128,8 +127,7 @@ impl<'a> Entry<'a> { }) } - /// Package identity (with `version`) for both consolidation and the package-id pass: a git - /// entry is its repository, since its spec name is whatever the dependent called it. + /// Identity shared by consolidation and the package-id pass; git entries go by repository. pub(crate) fn dedupe_name(&self) -> &[u8] { match &self.git_repo_name { Some(repo_name) if !self.has_direct_url_spec() => repo_name, From 272e01029a7ba295634a48013754f8ba3f22aabb Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 15 Aug 2026 20:24:53 +0000 Subject: [PATCH 5/5] ci: retrigger