diff --git a/src/install/lockfile/bun.lock.rs b/src/install/lockfile/bun.lock.rs index 482783f96467..9b21c0d97ec1 100644 --- a/src/install/lockfile/bun.lock.rs +++ b/src/install/lockfile/bun.lock.rs @@ -163,6 +163,18 @@ impl<'a> TreeDepsSortCtx<'a> { r.name.slice(self.string_buf), ) } + + fn sort(&self, dep_ids: &mut [DependencyID]) { + index_sort::sort_indices(dep_ids, &mut |a, b| { + if self.is_less_than(a, b) { + core::cmp::Ordering::Less + } else if self.is_less_than(b, a) { + core::cmp::Ordering::Greater + } else { + core::cmp::Ordering::Equal + } + }); + } } /// The slot order every existing bun.lock has its `trustedDependencies` and @@ -673,21 +685,11 @@ impl Stringifier { tree_deps_sort_buf.clear(); tree_deps_sort_buf.extend_from_slice(dependencies); - { - let ctx = TreeDepsSortCtx { - string_buf: buf, - deps_buf, - }; - index_sort::sort_indices(&mut tree_deps_sort_buf, &mut |a, b| { - if ctx.is_less_than(a, b) { - core::cmp::Ordering::Less - } else if ctx.is_less_than(b, a) { - core::cmp::Ordering::Greater - } else { - core::cmp::Ordering::Equal - } - }); + TreeDepsSortCtx { + string_buf: buf, + deps_buf, } + .sort(&mut tree_deps_sort_buf); for &dep_id in &tree_deps_sort_buf { let pkg_id = resolution_buf[dep_id as usize]; @@ -772,21 +774,11 @@ impl Stringifier { // there might be duplicate names due to dependency behaviors, // but we print behaviors in different groups so it won't affect // the result - { - let ctx = TreeDepsSortCtx { - string_buf: buf, - deps_buf, - }; - index_sort::sort_indices(&mut pkg_deps_sort_buf, &mut |a, b| { - if ctx.is_less_than(a, b) { - core::cmp::Ordering::Less - } else if ctx.is_less_than(b, a) { - core::cmp::Ordering::Greater - } else { - core::cmp::Ordering::Equal - } - }); + TreeDepsSortCtx { + string_buf: buf, + deps_buf, } + .sort(&mut pkg_deps_sort_buf); // INFO = { prod/dev/optional/peer dependencies, os, cpu, libc (TODO), bin, binDir } @@ -1336,9 +1328,20 @@ impl Stringifier { any = true; } + // Re-sort by current name: a no-alias git/tarball dep sorts under its version + // literal at parse time, and `assign_resolution` renames it without a re-sort. + let deps_list = pkg_deps[pkg_id as usize]; + let mut deps_sort_buf: Vec = (deps_list.begin()..deps_list.end()).collect(); + TreeDepsSortCtx { + string_buf: buf, + deps_buf, + } + .sort(&mut deps_sort_buf); + for &(group_name, group_behavior) in WORKSPACE_DEPENDENCY_GROUPS.iter() { let mut first = true; - for dep in pkg_deps[pkg_id as usize].get(deps_buf) { + for &dep_id in &deps_sort_buf { + let dep = &deps_buf[dep_id as usize]; if !dep.behavior.intersects(group_behavior) { continue; } diff --git a/test/cli/install/bun-install-git-deps.test.ts b/test/cli/install/bun-install-git-deps.test.ts index a78422deebb3..394b6df79a3e 100644 --- a/test/cli/install/bun-install-git-deps.test.ts +++ b/test/cli/install/bun-install-git-deps.test.ts @@ -586,3 +586,36 @@ test.concurrent("installs a git+file:// dependency", async () => { expect(await lockedPackages(project)).toEqual(locked); expect(exitCode).toBe(0); }); + +// issue #40803: `bun install ` (no alias) sorted the workspace dep +// under its version literal. The real name is only known once the repo is +// fetched; it is rewritten in place after resolution, so the written key +// landed at the literal's position ("git..." here, between nothing and +// "hhh-first") instead of its own. +test.concurrent("bun install sorts the workspace dependency by its resolved name", async () => { + using dir = tempDir("git-dep-sort", { + "project/package.json": JSON.stringify({ + name: "project", + version: "1.0.0", + dependencies: { "hhh-first": "file:./hhh-first", "jjj-last": "file:./jjj-last" }, + }), + "project/hhh-first/package.json": JSON.stringify({ name: "hhh-first", version: "1.0.0" }), + "project/jjj-last/package.json": JSON.stringify({ name: "jjj-last", version: "1.0.0" }), + }); + const root = String(dir); + const project = join(root, "project"); + const bare = await makeSharedRepo(root, [{ name: "iii-middle", branch: "main" }], "sort-repo.git"); + + const first = await runInstall(project, join(root, "cache"), {}); + expect(first.stderr).toContain("Saved lockfile"); + expect(first.exitCode).toBe(0); + + const second = await runInstall(project, join(root, "cache"), {}, `git+${pathToFileURL(bare)}#main`); + expect(second.stderr).toContain("Saved lockfile"); + expect(second.exitCode).toBe(0); + + const lockfile = Bun.JSONC.parse(await Bun.file(join(project, "bun.lock")).text()) as { + workspaces: Record }>; + }; + expect(Object.keys(lockfile.workspaces[""].dependencies)).toEqual(["hhh-first", "iii-middle", "jjj-last"]); +}); diff --git a/test/cli/install/migration/__snapshots__/yarn-lock-migration.test.ts.snap b/test/cli/install/migration/__snapshots__/yarn-lock-migration.test.ts.snap index b52a34874284..67ccf289fcd8 100644 --- a/test/cli/install/migration/__snapshots__/yarn-lock-migration.test.ts.snap +++ b/test/cli/install/migration/__snapshots__/yarn-lock-migration.test.ts.snap @@ -275,15 +275,15 @@ exports[`yarn.lock migration basic migration with realistic complex yarn.lock: c "": { "name": "complex-app", "dependencies": { + "@babel/core": "^7.20.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "@babel/core": "^7.20.0", "webpack": "^5.75.0", }, "devDependencies": { "@types/react": "^18.0.0", - "typescript": "^4.9.0", "eslint": "^8.0.0", + "typescript": "^4.9.0", }, "optionalDependencies": { "fsevents": "^2.3.2", @@ -3186,8 +3186,8 @@ exports[`bun pm migrate for existing yarn.lock yarn.lock with packages that have "": { "name": "os-cpu-test", "dependencies": { - "fsevents": "^2.3.2", "esbuild": "^0.17.0", + "fsevents": "^2.3.2", }, }, },