diff --git a/docs/runtime/bunfig.md b/docs/runtime/bunfig.md index 4ee923e8c2..0d94e6838c 100644 --- a/docs/runtime/bunfig.md +++ b/docs/runtime/bunfig.md @@ -382,6 +382,17 @@ registry = { url = "https://registry.npmjs.org", token = "123456" } registry = "https://username:password@registry.npmjs.org" ``` +### `install.linkWorkspacePackages` + +To configure how workspace packages are linked, use the `install.linkWorkspacePackages` option. + +Whether to link workspace packages from the monorepo root to their respective `node_modules` directories. Default `true`. + +```toml +[install] +linkWorkspacePackages = true +``` + ### `install.scopes` To configure a registry for a particular scope (e.g. `@myorg/`) use `install.scopes`. You can reference environment variables with `$variable` notation. diff --git a/src/api/schema.zig b/src/api/schema.zig index 8e7bf2b590..fca846537f 100644 --- a/src/api/schema.zig +++ b/src/api/schema.zig @@ -3002,6 +3002,8 @@ pub const Api = struct { ignore_scripts: ?bool = null, + link_workspace_packages: ?bool = null, + pub fn decode(reader: anytype) anyerror!BunInstall { var this = std.mem.zeroes(BunInstall); diff --git a/src/bunfig.zig b/src/bunfig.zig index 09f62008db..40cc8ec7a0 100644 --- a/src/bunfig.zig +++ b/src/bunfig.zig @@ -582,6 +582,12 @@ pub const Bunfig = struct { } } } + + if (install_obj.get("linkWorkspacePackages")) |link_workspace| { + if (link_workspace.asBool()) |value| { + install.link_workspace_packages = value; + } + } } if (json.get("run")) |run_expr| { diff --git a/src/install/PackageManager/PackageManagerOptions.zig b/src/install/PackageManager/PackageManagerOptions.zig index 765d75cb47..6e2275848f 100644 --- a/src/install/PackageManager/PackageManagerOptions.zig +++ b/src/install/PackageManager/PackageManagerOptions.zig @@ -19,6 +19,7 @@ do: Do = .{}, positionals: []const string = &[_]string{}, update: Update = .{}, dry_run: bool = false, +link_workspace_packages: bool = true, remote_package_features: Features = .{ .optional_dependencies = true, }, @@ -205,6 +206,9 @@ pub fn load( if (config.default_registry) |registry| { base = registry; } + if (config.link_workspace_packages) |link_workspace_packages| { + this.link_workspace_packages = link_workspace_packages; + } } if (base.url.len == 0) base.url = Npm.Registry.default_url; diff --git a/src/install/install.zig b/src/install/install.zig index 6b201685f4..db5740645a 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -4690,12 +4690,12 @@ pub const PackageManager = struct { const workspace_path = if (this.lockfile.workspace_paths.count() > 0) this.lockfile.workspace_paths.get(name_hash) else null; const workspace_version = this.lockfile.workspace_versions.get(name_hash); const buf = this.lockfile.buffers.string_bytes.items; - if ((workspace_version != null and version.value.npm.version.satisfies(workspace_version.?, buf, buf)) or - - // https://github.com/oven-sh/bun/pull/10899#issuecomment-2099609419 - // if the workspace doesn't have a version, it can still be used if - // dependency version is wildcard - (workspace_path != null and version.value.npm.version.@"is *"())) + if (this.options.link_workspace_packages and + (((workspace_version != null and version.value.npm.version.satisfies(workspace_version.?, buf, buf)) or + // https://github.com/oven-sh/bun/pull/10899#issuecomment-2099609419 + // if the workspace doesn't have a version, it can still be used if + // dependency version is wildcard + (workspace_path != null and version.value.npm.version.@"is *"())))) { const root_package = this.lockfile.rootPackage() orelse break :resolve_from_workspace; const root_dependencies = root_package.dependencies.get(this.lockfile.buffers.dependencies.items); diff --git a/src/install/lockfile/Package.zig b/src/install/lockfile/Package.zig index 7b48673e0e..09e1ebb295 100644 --- a/src/install/lockfile/Package.zig +++ b/src/install/lockfile/Package.zig @@ -1088,7 +1088,7 @@ pub const Package = extern struct { .npm => { const npm = dependency_version.value.npm; if (workspace_version != null) { - if (npm.version.satisfies(workspace_version.?, buf, buf)) { + if (pm.options.link_workspace_packages and npm.version.satisfies(workspace_version.?, buf, buf)) { const path = workspace_path.?.sliced(buf); if (Dependency.parseWithTag( allocator, diff --git a/test/cli/install/bun-workspaces.test.ts b/test/cli/install/bun-workspaces.test.ts index 2ba756ba46..a249d5e32b 100644 --- a/test/cli/install/bun-workspaces.test.ts +++ b/test/cli/install/bun-workspaces.test.ts @@ -1,7 +1,8 @@ import { file, spawn, write } from "bun"; import { install_test_helpers } from "bun:internal-for-testing"; -import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"; +import { afterAll, beforeAll, beforeEach, describe, expect, test, afterEach } from "bun:test"; import { mkdirSync, rmSync, writeFileSync } from "fs"; +import { readlink } from "fs/promises"; import { cp, exists, mkdir, rm } from "fs/promises"; import { assertManifestsPopulated, @@ -13,6 +14,7 @@ import { VerdaccioRegistry, } from "harness"; import { join } from "path"; + const { parseLockfile } = install_test_helpers; expect.extend({ toMatchNodeModulesAt }); @@ -1681,3 +1683,138 @@ test("can override npm package with workspace package under a different name", a version: "2.2.2", }); }); + +describe("LinkWorkspacePackages", () => { + let bunfigPath: string; + + beforeEach(async () => { + bunfigPath = join(packageDir, "bunfig.toml"); + + await Promise.all([ + write( + join(packageDir, "package.json"), + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + }), + ), + + write( + join(packageDir, "packages", "mono", "package.json"), + JSON.stringify({ + name: "no-deps", + version: "2.0.0", + }), + ), + ]); + }); + + afterEach(async () => { + await Promise.all([ + rm(bunfigPath, { force: true }), + rm(join(packageDir, "node_modules"), { recursive: true, force: true }), + rm(join(packageDir, "packages"), { recursive: true, force: true }), + rm(join(packageDir, "package.json"), { force: true }), + ]); + }); + + test("linkWorkspacePackages = false uses registry instead of linking workspace packages", async () => { + // Create bunfig.toml with linkWorkspacePackages set to false + await Promise.all([ + write( + bunfigPath, + ` +[install] +linkWorkspacePackages = false +registry = "${verdaccio.registryUrl()}" +`, + ), + + write( + join(packageDir, "packages", "bar", "package.json"), + JSON.stringify({ + name: "bar", + version: "1.0.0", + dependencies: { + "no-deps": "2.0.0", // Use Same version as workspace package and it shouldn't link + }, + }), + ), + ]); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), `-c=${bunfigPath}`, "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + const err = await Bun.readableStreamToText(stderr); + const out = await Bun.readableStreamToText(stdout); + + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + const lockfile = parseLockfile(packageDir); + + // Check the resolution tag to ensure it's not a workspace link + const barPackage = lockfile.packages.find(p => p.name === "bar"); + expect(barPackage.dependencies.length).toEqual(1); + const barDependency = lockfile.dependencies.find(p => p.id === barPackage.dependencies[0]); + expect(barDependency).toBeDefined(); + + // Verify that the dependency linked to the bar package is the npm version, not the workspace version + expect(lockfile.packages.find(p => p.id === barDependency?.package_id).resolution.tag).toEqual("npm"); + }); + + test("linkWorkspacePackages = false but workspace: prefix still links workspace", async () => { + // Create bunfig.toml with linkWorkspacePackages set to false + await Promise.all([ + write( + bunfigPath, + ` +[install] +linkWorkspacePackages = false +registry = "${verdaccio.registryUrl()}" +`, + ), + + write( + join(packageDir, "packages", "bar", "package.json"), + JSON.stringify({ + name: "bar", + version: "1.0.0", + dependencies: { + "no-deps": "workspace:*", // Explicit workspace: prefix should still link + }, + }), + ), + ]); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), `-c=${bunfigPath}`, "install"], + cwd: packageDir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + const err = await Bun.readableStreamToText(stderr); + const out = await Bun.readableStreamToText(stdout); + + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + const lockfile = parseLockfile(packageDir); + + // Check the resolution tag to ensure it's not a workspace link + const barPackage = lockfile.packages.find(p => p.name === "bar"); + expect(barPackage.dependencies.length).toEqual(1); + const barDependency = lockfile.dependencies.find(p => p.id === barPackage.dependencies[0]); + expect(barDependency).toBeDefined(); + + // Verify that the dependency linked to the bar package is the workspace version (using the workspace: prefix), not the npm version + expect(lockfile.packages.find(p => p.id === barDependency?.package_id).resolution.tag).toEqual("workspace"); + }); +});