diff --git a/src/install/PackageManager/PackageManagerEnqueue.rs b/src/install/PackageManager/PackageManagerEnqueue.rs index b21bc8ff9788..a0a15b6a1e07 100644 --- a/src/install/PackageManager/PackageManagerEnqueue.rs +++ b/src/install/PackageManager/PackageManagerEnqueue.rs @@ -1976,9 +1976,10 @@ fn get_or_put_resolved_package_with_find_result( // `manager.workspace_package_json_cache` only — disjoint from // `manager.lockfile`. this.to_update - // If updating, only update packages in the current workspace - && unsafe { &*(*this_ptr).lockfile } - .is_root_dependency(unsafe { &mut *this_ptr }, dependency_id) + // Update direct deps of the current workspace; catalogs are root-scoped. + && (dependency.version.tag == dependency::version::Tag::Catalog + || unsafe { &*(*this_ptr).lockfile } + .is_root_dependency(unsafe { &mut *this_ptr }, dependency_id)) // no need to do a look up if update requests are empty (`bun update` with no args) && (this.update_requests.is_empty() || this.updating_packages.contains( diff --git a/test/cli/install/catalogs.test.ts b/test/cli/install/catalogs.test.ts index 4a5d31c121a7..1169bbf81b7b 100644 --- a/test/cli/install/catalogs.test.ts +++ b/test/cli/install/catalogs.test.ts @@ -380,6 +380,75 @@ describe("update", () => { }); } + for (const args of [[], ["-r"]] as const) { + test(`update without --latest from root moves catalogs within range (${args.join(" ") || "no args"})`, async () => { + // The lockfile pins no-deps@1.0.0 (as if 1.1.0 was published after install). + // `bun update` from the workspace root must re-resolve catalog references + // within range the same as a direct dependency would be. + const { packageDir } = await registry.createTestDir(); + const url = registry.registryUrl(); + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "catalog-update-from-root", + workspaces: { + packages: ["packages/*"], + catalog: { "no-deps": "^1.0.0" }, + }, + }), + ), + write( + join(packageDir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + dependencies: { "no-deps": "catalog:" }, + }), + ), + write( + join(packageDir, "bun.lock"), + JSON.stringify({ + lockfileVersion: 1, + configVersion: 1, + workspaces: { + "": { name: "catalog-update-from-root" }, + "packages/pkg1": { name: "pkg1", dependencies: { "no-deps": "catalog:" } }, + }, + catalog: { "no-deps": "^1.0.0" }, + packages: { + "no-deps": [ + "no-deps@1.0.0", + `${url}no-deps/-/no-deps-1.0.0.tgz`, + {}, + "sha512-v4w12JRjUGvfHDUP8vFDwu0gUWu04j0cv9hLb1Abf9VdaXu4XcrddYFTMVBVvmldKViGWH7jrb6xPJRF0wq6gw==", + ], + "pkg1": ["pkg1@workspace:packages/pkg1"], + }, + }), + ), + ]); + + const { err, exitCode } = await runUpdate(packageDir, ...args); + expect(err).not.toContain("error:"); + + const root = await file(join(packageDir, "package.json")).json(); + expect(root.workspaces.catalog).toEqual({ "no-deps": "^1.1.0" }); + + expect((await file(join(packageDir, "packages", "pkg1", "package.json")).json()).dependencies).toEqual({ + "no-deps": "catalog:", + }); + expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ + name: "no-deps", + version: "1.1.0", + }); + + const lock = await file(join(packageDir, "bun.lock")).text(); + expect(lock).toContain("no-deps@1.1.0"); + expect(lock).not.toContain("no-deps@1.0.0"); + expect(exitCode).toBe(0); + }); + } + test("update without --latest stays in range and keeps catalog references", async () => { const { packageDir } = await registry.createTestDir(); await Promise.all([