From b57f3bfec11bf67612565cedabbffa84d6979330 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 29 Jan 2026 08:35:01 +0100 Subject: [PATCH] fix(install): resolve dist-tag dependencies to local workspace packages When a workspace package has the same name as an npm package and a dependency uses a dist-tag specifier (e.g. "latest" or ""), bun install would incorrectly fetch from the npm registry instead of symlinking to the local workspace. This extends the workspace resolution logic in both the lockfile package resolver and the enqueue step to handle dist-tag versions the same way as other version specifiers, preferring local workspace packages when `link_workspace_packages` is enabled. --- .../PackageManager/PackageManagerEnqueue.zig | 25 + src/install/lockfile/Package.zig | 18 + .../__snapshots__/bun-workspaces.test.ts.snap | 597 ++++++++---------- test/cli/install/bun-workspaces.test.ts | 15 +- 4 files changed, 296 insertions(+), 359 deletions(-) diff --git a/src/install/PackageManager/PackageManagerEnqueue.zig b/src/install/PackageManager/PackageManagerEnqueue.zig index 9df5508e74..d7bea30549 100644 --- a/src/install/PackageManager/PackageManagerEnqueue.zig +++ b/src/install/PackageManager/PackageManagerEnqueue.zig @@ -1611,6 +1611,31 @@ fn getOrPutResolvedPackage( } } } + } else if (version.tag == .dist_tag) { + if (this.options.link_workspace_packages) { + const workspace_path = if (this.lockfile.workspace_paths.count() > 0) + this.lockfile.workspace_paths.get(name_hash) + else + null; + if (workspace_path != null) { + const root_package = this.lockfile.rootPackage() orelse break :resolve_from_workspace; + const root_dependencies = root_package.dependencies.get(this.lockfile.buffers.dependencies.items); + const root_resolutions = root_package.resolutions.get(this.lockfile.buffers.resolutions.items); + + for (root_dependencies, root_resolutions) |root_dep, workspace_package_id| { + if (workspace_package_id != invalid_package_id and + root_dep.version.tag == .workspace and + root_dep.name_hash == name_hash) + { + successFn(this, dependency_id, workspace_package_id); + return .{ + .package = this.lockfile.packages.get(workspace_package_id), + .is_first_time = false, + }; + } + } + } + } } } diff --git a/src/install/lockfile/Package.zig b/src/install/lockfile/Package.zig index 48c8970699..39f0fad0f4 100644 --- a/src/install/lockfile/Package.zig +++ b/src/install/lockfile/Package.zig @@ -1121,6 +1121,24 @@ pub fn Package(comptime SemverIntType: type) type { } } }, + .dist_tag => { + if (workspace_path != null and pm.options.link_workspace_packages) { + const path = workspace_path.?.sliced(buf); + if (Dependency.parseWithTag( + allocator, + external_alias.value, + external_alias.hash, + path.slice, + .workspace, + &path, + log, + pm, + )) |dep| { + dependency_version.tag = dep.tag; + dependency_version.value = dep.value; + } + } + }, .workspace => workspace: { if (workspace_path) |path| { if (workspace_range) |range| { diff --git a/test/cli/install/__snapshots__/bun-workspaces.test.ts.snap b/test/cli/install/__snapshots__/bun-workspaces.test.ts.snap index ac3d396fea..cb0ab5362a 100644 --- a/test/cli/install/__snapshots__/bun-workspaces.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-workspaces.test.ts.snap @@ -738,6 +738,252 @@ exports[`dependency on workspace without version in package.json: version: *-pre }" `; +exports[`dependency on workspace without version in package.json: version: latest 1`] = ` +"{ + "format": "v3", + "meta_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "package_index": { + "no-deps": 2, + "foo": 0, + "bar": 1 + }, + "trees": [ + { + "id": 0, + "path": "node_modules", + "depth": 0, + "dependencies": { + "bar": { + "id": 0, + "package_id": 1 + }, + "no-deps": { + "id": 1, + "package_id": 2 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "", + "workspace": "packages/bar", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 0 + }, + { + "name": "no-deps", + "literal": "", + "workspace": "packages/mono", + "package_id": 2, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "latest", + "workspace": "packages/mono", + "package_id": 2, + "behavior": { + "prod": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } + ], + "workspace_paths": { + "11592711315645265694": "packages/bar", + "5128161233225832376": "packages/mono" + }, + "workspace_versions": { + "11592711315645265694": "1.0.0" + } +}" +`; + +exports[`dependency on workspace without version in package.json: version: 1`] = ` +"{ + "format": "v3", + "meta_hash": "0000000000000000000000000000000000000000000000000000000000000000", + "package_index": { + "no-deps": 2, + "foo": 0, + "bar": 1 + }, + "trees": [ + { + "id": 0, + "path": "node_modules", + "depth": 0, + "dependencies": { + "bar": { + "id": 0, + "package_id": 1 + }, + "no-deps": { + "id": 1, + "package_id": 2 + } + } + } + ], + "dependencies": [ + { + "name": "bar", + "literal": "", + "workspace": "packages/bar", + "package_id": 1, + "behavior": { + "workspace": true + }, + "id": 0 + }, + { + "name": "no-deps", + "literal": "", + "workspace": "packages/mono", + "package_id": 2, + "behavior": { + "workspace": true + }, + "id": 1 + }, + { + "name": "no-deps", + "literal": "", + "workspace": "packages/mono", + "package_id": 2, + "behavior": { + "prod": true + }, + "id": 2 + } + ], + "packages": [ + { + "id": 0, + "name": "foo", + "name_hash": "14841791273925386894", + "resolution": { + "tag": "root", + "value": "", + "resolved": "" + }, + "dependencies": [ + 0, + 1 + ], + "integrity": null, + "man_dir": "", + "origin": "local", + "bin": null, + "scripts": {} + }, + { + "id": 1, + "name": "bar", + "name_hash": "11592711315645265694", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/bar", + "resolved": "workspace:packages/bar" + }, + "dependencies": [ + 2 + ], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + }, + { + "id": 2, + "name": "no-deps", + "name_hash": "5128161233225832376", + "resolution": { + "tag": "workspace", + "value": "workspace:packages/mono", + "resolved": "workspace:packages/mono" + }, + "dependencies": [], + "integrity": null, + "man_dir": "", + "origin": "npm", + "bin": null, + "scripts": {} + } + ], + "workspace_paths": { + "11592711315645265694": "packages/bar", + "5128161233225832376": "packages/mono" + }, + "workspace_versions": { + "11592711315645265694": "1.0.0" + } +}" +`; + exports[`dependency on workspace without version in package.json: version: 1 1`] = ` "{ "format": "v3", @@ -1674,327 +1920,12 @@ exports[`dependency on workspace without version in package.json: version: *+bui }" `; -exports[`dependency on workspace without version in package.json: version: latest 1`] = ` -"{ - "format": "v3", - "meta_hash": "0000000000000000000000000000000000000000000000000000000000000000", - "package_index": { - "no-deps": [ - 2, - 3 - ], - "foo": 0, - "bar": 1 - }, - "trees": [ - { - "id": 0, - "path": "node_modules", - "depth": 0, - "dependencies": { - "bar": { - "id": 0, - "package_id": 1 - }, - "no-deps": { - "id": 1, - "package_id": 2 - } - } - }, - { - "id": 1, - "path": "node_modules/bar/node_modules", - "depth": 1, - "dependencies": { - "no-deps": { - "id": 2, - "package_id": 3 - } - } - } - ], - "dependencies": [ - { - "name": "bar", - "literal": "", - "workspace": "packages/bar", - "package_id": 1, - "behavior": { - "workspace": true - }, - "id": 0 - }, - { - "name": "no-deps", - "literal": "", - "workspace": "packages/mono", - "package_id": 2, - "behavior": { - "workspace": true - }, - "id": 1 - }, - { - "name": "no-deps", - "literal": "latest", - "dist_tag": { - "name": "no-deps", - "tag": "no-deps" - }, - "package_id": 3, - "behavior": { - "prod": true - }, - "id": 2 - } - ], - "packages": [ - { - "id": 0, - "name": "foo", - "name_hash": "14841791273925386894", - "resolution": { - "tag": "root", - "value": "", - "resolved": "" - }, - "dependencies": [ - 0, - 1 - ], - "integrity": null, - "man_dir": "", - "origin": "local", - "bin": null, - "scripts": {} - }, - { - "id": 1, - "name": "bar", - "name_hash": "11592711315645265694", - "resolution": { - "tag": "workspace", - "value": "workspace:packages/bar", - "resolved": "workspace:packages/bar" - }, - "dependencies": [ - 2 - ], - "integrity": null, - "man_dir": "", - "origin": "npm", - "bin": null, - "scripts": {} - }, - { - "id": 2, - "name": "no-deps", - "name_hash": "5128161233225832376", - "resolution": { - "tag": "workspace", - "value": "workspace:packages/mono", - "resolved": "workspace:packages/mono" - }, - "dependencies": [], - "integrity": null, - "man_dir": "", - "origin": "npm", - "bin": null, - "scripts": {} - }, - { - "id": 3, - "name": "no-deps", - "name_hash": "5128161233225832376", - "resolution": { - "tag": "npm", - "value": "2.0.0", - "resolved": "http://localhost:1234/no-deps/-/no-deps-2.0.0.tgz" - }, - "dependencies": [], - "integrity": "sha512-W3duJKZPcMIG5rA1io5cSK/bhW9rWFz+jFxZsKS/3suK4qHDkQNxUTEXee9/hTaAoDCeHWQqogukWYKzfr6X4g==", - "man_dir": "", - "origin": "npm", - "bin": null, - "scripts": {} - } - ], - "workspace_paths": { - "11592711315645265694": "packages/bar", - "5128161233225832376": "packages/mono" - }, - "workspace_versions": { - "11592711315645265694": "1.0.0" - } -}" -`; - -exports[`dependency on workspace without version in package.json: version: 1`] = ` -"{ - "format": "v3", - "meta_hash": "0000000000000000000000000000000000000000000000000000000000000000", - "package_index": { - "no-deps": [ - 2, - 3 - ], - "foo": 0, - "bar": 1 - }, - "trees": [ - { - "id": 0, - "path": "node_modules", - "depth": 0, - "dependencies": { - "bar": { - "id": 0, - "package_id": 1 - }, - "no-deps": { - "id": 1, - "package_id": 2 - } - } - }, - { - "id": 1, - "path": "node_modules/bar/node_modules", - "depth": 1, - "dependencies": { - "no-deps": { - "id": 2, - "package_id": 3 - } - } - } - ], - "dependencies": [ - { - "name": "bar", - "literal": "", - "workspace": "packages/bar", - "package_id": 1, - "behavior": { - "workspace": true - }, - "id": 0 - }, - { - "name": "no-deps", - "literal": "", - "workspace": "packages/mono", - "package_id": 2, - "behavior": { - "workspace": true - }, - "id": 1 - }, - { - "name": "no-deps", - "literal": "", - "dist_tag": { - "name": "no-deps", - "tag": "no-deps" - }, - "package_id": 3, - "behavior": { - "prod": true - }, - "id": 2 - } - ], - "packages": [ - { - "id": 0, - "name": "foo", - "name_hash": "14841791273925386894", - "resolution": { - "tag": "root", - "value": "", - "resolved": "" - }, - "dependencies": [ - 0, - 1 - ], - "integrity": null, - "man_dir": "", - "origin": "local", - "bin": null, - "scripts": {} - }, - { - "id": 1, - "name": "bar", - "name_hash": "11592711315645265694", - "resolution": { - "tag": "workspace", - "value": "workspace:packages/bar", - "resolved": "workspace:packages/bar" - }, - "dependencies": [ - 2 - ], - "integrity": null, - "man_dir": "", - "origin": "npm", - "bin": null, - "scripts": {} - }, - { - "id": 2, - "name": "no-deps", - "name_hash": "5128161233225832376", - "resolution": { - "tag": "workspace", - "value": "workspace:packages/mono", - "resolved": "workspace:packages/mono" - }, - "dependencies": [], - "integrity": null, - "man_dir": "", - "origin": "npm", - "bin": null, - "scripts": {} - }, - { - "id": 3, - "name": "no-deps", - "name_hash": "5128161233225832376", - "resolution": { - "tag": "npm", - "value": "2.0.0", - "resolved": "http://localhost:1234/no-deps/-/no-deps-2.0.0.tgz" - }, - "dependencies": [], - "integrity": "sha512-W3duJKZPcMIG5rA1io5cSK/bhW9rWFz+jFxZsKS/3suK4qHDkQNxUTEXee9/hTaAoDCeHWQqogukWYKzfr6X4g==", - "man_dir": "", - "origin": "npm", - "bin": null, - "scripts": {} - } - ], - "workspace_paths": { - "11592711315645265694": "packages/bar", - "5128161233225832376": "packages/mono" - }, - "workspace_versions": { - "11592711315645265694": "1.0.0" - } -}" -`; - exports[`dependency on same name as workspace and dist-tag: with version 1`] = ` "{ "format": "v3", "meta_hash": "0000000000000000000000000000000000000000000000000000000000000000", "package_index": { - "no-deps": [ - 2, - 3 - ], + "no-deps": 2, "foo": 0, "bar": 1 }, @@ -2013,17 +1944,6 @@ exports[`dependency on same name as workspace and dist-tag: with version 1`] = ` "package_id": 2 } } - }, - { - "id": 1, - "path": "node_modules/bar/node_modules", - "depth": 1, - "dependencies": { - "no-deps": { - "id": 2, - "package_id": 3 - } - } } ], "dependencies": [ @@ -2050,11 +1970,8 @@ exports[`dependency on same name as workspace and dist-tag: with version 1`] = ` { "name": "no-deps", "literal": "latest", - "dist_tag": { - "name": "no-deps", - "tag": "no-deps" - }, - "package_id": 3, + "workspace": "packages/mono", + "package_id": 2, "behavior": { "prod": true }, @@ -2114,22 +2031,6 @@ exports[`dependency on same name as workspace and dist-tag: with version 1`] = ` "origin": "npm", "bin": null, "scripts": {} - }, - { - "id": 3, - "name": "no-deps", - "name_hash": "5128161233225832376", - "resolution": { - "tag": "npm", - "value": "2.0.0", - "resolved": "http://localhost:1234/no-deps/-/no-deps-2.0.0.tgz" - }, - "dependencies": [], - "integrity": "sha512-W3duJKZPcMIG5rA1io5cSK/bhW9rWFz+jFxZsKS/3suK4qHDkQNxUTEXee9/hTaAoDCeHWQqogukWYKzfr6X4g==", - "man_dir": "", - "origin": "npm", - "bin": null, - "scripts": {} } ], "workspace_paths": { diff --git a/test/cli/install/bun-workspaces.test.ts b/test/cli/install/bun-workspaces.test.ts index b1a0a59ec3..4de1fac14c 100644 --- a/test/cli/install/bun-workspaces.test.ts +++ b/test/cli/install/bun-workspaces.test.ts @@ -66,17 +66,10 @@ test("dependency on workspace without version in package.json", async () => { "kjwoehcojrgjoj", // dist-tag does not exist, should choose local workspace "*.1.*", "*-pre", + "latest", // dist-tag exists, should choose local workspace + "", // empty string is treated as "latest" dist tag, should choose local workspace ]; - const shouldNotWork: string[] = [ - "1", - "1.*", - "1.1.*", - "1.1.0", - "*-pre+build", - "*+build", - "latest", // dist-tag exists, should choose package from npm - "", - ]; + const shouldNotWork: string[] = ["1", "1.*", "1.1.*", "1.1.0", "*-pre+build", "*+build"]; for (const version of shouldWork) { writeFileSync( @@ -213,7 +206,7 @@ test("dependency on same name as workspace and dist-tag", async () => { expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", - "3 packages installed", + "2 packages installed", ]); });