diff --git a/src/install/install.zig b/src/install/install.zig index 17c5bc4ee0..fd0aaf16bb 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -5512,63 +5512,65 @@ pub const PackageManager = struct { const child_cwd = this_cwd; // Check if this is a workspace; if so, use root package var found = false; - if (!created_package_json) { - while (std.fs.path.dirname(this_cwd)) |parent| : (this_cwd = parent) { - const parent_without_trailing_slash = strings.withoutTrailingSlash(parent); - var buf2: [bun.MAX_PATH_BYTES + 1]u8 = undefined; - @memcpy(buf2[0..parent_without_trailing_slash.len], parent_without_trailing_slash); - buf2[parent_without_trailing_slash.len..buf2.len][0.."/package.json".len].* = "/package.json".*; - buf2[parent_without_trailing_slash.len + "/package.json".len] = 0; + if (comptime subcommand != .link) { + if (!created_package_json) { + while (std.fs.path.dirname(this_cwd)) |parent| : (this_cwd = parent) { + const parent_without_trailing_slash = strings.withoutTrailingSlash(parent); + var buf2: [bun.MAX_PATH_BYTES + 1]u8 = undefined; + @memcpy(buf2[0..parent_without_trailing_slash.len], parent_without_trailing_slash); + buf2[parent_without_trailing_slash.len..buf2.len][0.."/package.json".len].* = "/package.json".*; + buf2[parent_without_trailing_slash.len + "/package.json".len] = 0; - const json_file = std.fs.cwd().openFileZ( - buf2[0 .. parent_without_trailing_slash.len + "/package.json".len :0].ptr, - .{ .mode = .read_write }, - ) catch { - continue; - }; - defer if (!found) json_file.close(); - const json_stat_size = try json_file.getEndPos(); - const json_buf = try ctx.allocator.alloc(u8, json_stat_size + 64); - defer ctx.allocator.free(json_buf); - const json_len = try json_file.preadAll(json_buf, 0); - const json_path = try bun.getFdPath(json_file.handle, &package_json_cwd_buf); - const json_source = logger.Source.initPathString(json_path, json_buf[0..json_len]); - initializeStore(); - const json = try json_parser.ParseJSONUTF8(&json_source, ctx.log, ctx.allocator); - if (json.asProperty("workspaces")) |prop| { - const json_array = switch (prop.expr.data) { - .e_array => |arr| arr, - .e_object => |obj| if (obj.get("packages")) |packages| switch (packages.data) { - .e_array => |arr| arr, - else => break, - } else break, - else => break, + const json_file = std.fs.cwd().openFileZ( + buf2[0 .. parent_without_trailing_slash.len + "/package.json".len :0].ptr, + .{ .mode = .read_write }, + ) catch { + continue; }; - var log = logger.Log.init(ctx.allocator); - defer log.deinit(); - const workspace_packages_count = Package.processWorkspaceNamesArray( - &workspace_names, - ctx.allocator, - &log, - json_array, - &json_source, - prop.loc, - null, - ) catch break; - _ = workspace_packages_count; - for (workspace_names.keys()) |path| { - if (strings.eql(child_cwd, path)) { - fs.top_level_dir = parent; - if (comptime subcommand == .install) { - found = true; - child_json.close(); - break :brk json_file; - } else { - break :brk child_json; + defer if (!found) json_file.close(); + const json_stat_size = try json_file.getEndPos(); + const json_buf = try ctx.allocator.alloc(u8, json_stat_size + 64); + defer ctx.allocator.free(json_buf); + const json_len = try json_file.preadAll(json_buf, 0); + const json_path = try bun.getFdPath(json_file.handle, &package_json_cwd_buf); + const json_source = logger.Source.initPathString(json_path, json_buf[0..json_len]); + initializeStore(); + const json = try json_parser.ParseJSONUTF8(&json_source, ctx.log, ctx.allocator); + if (json.asProperty("workspaces")) |prop| { + const json_array = switch (prop.expr.data) { + .e_array => |arr| arr, + .e_object => |obj| if (obj.get("packages")) |packages| switch (packages.data) { + .e_array => |arr| arr, + else => break, + } else break, + else => break, + }; + var log = logger.Log.init(ctx.allocator); + defer log.deinit(); + const workspace_packages_count = Package.processWorkspaceNamesArray( + &workspace_names, + ctx.allocator, + &log, + json_array, + &json_source, + prop.loc, + null, + ) catch break; + _ = workspace_packages_count; + for (workspace_names.keys()) |path| { + if (strings.eql(child_cwd, path)) { + fs.top_level_dir = parent; + if (comptime subcommand == .install) { + found = true; + child_json.close(); + break :brk json_file; + } else { + break :brk child_json; + } } } + break; } - break; } } } diff --git a/test/cli/install/bun-link.test.ts b/test/cli/install/bun-link.test.ts index 5d2fffc395..7883bedc8a 100644 --- a/test/cli/install/bun-link.test.ts +++ b/test/cli/install/bun-link.test.ts @@ -1,7 +1,7 @@ -import { spawn } from "bun"; +import { spawn, file } from "bun"; import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test"; import { bunExe, bunEnv as env } from "harness"; -import { access, mkdtemp, readlink, realpath, rm, writeFile } from "fs/promises"; +import { access, mkdtemp, readlink, realpath, rm, writeFile, mkdir } from "fs/promises"; import { basename, join } from "path"; import { tmpdir } from "os"; import { @@ -27,6 +27,171 @@ afterEach(async () => { await dummyAfterEach(); }); +it("should link and unlink workspace package", async () => { + await writeFile( + join(link_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "1.0.0", + workspaces: ["packages/*"], + }), + ); + await mkdir(join(link_dir, "packages", "moo"), { recursive: true }); + await mkdir(join(link_dir, "packages", "boba"), { recursive: true }); + await writeFile( + join(link_dir, "packages", "moo", "package.json"), + JSON.stringify({ + name: "moo", + version: "0.0.1", + }), + ); + await writeFile( + join(link_dir, "packages", "boba", "package.json"), + JSON.stringify({ + name: "boba", + version: "0.0.1", + }), + ); + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: link_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr).toBeDefined(); + var err = await new Response(stderr).text(); + expect(err.replace(/^(.*?) v[^\n]+/, "$1").split(/\r?\n/)).toEqual(["bun install", " Saved lockfile", ""]); + expect(stdout).toBeDefined(); + var out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ + " + boba@workspace:packages/boba", + " + moo@workspace:packages/moo", + "", + " 2 packages installed", + ]); + expect(await exited).toBe(0); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "link"], + cwd: join(link_dir, "packages", "moo"), + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + })); + + expect(stderr).toBeDefined(); + err = await new Response(stderr).text(); + expect(err.replace(/^(.*?) v[^\n]+/, "$1").split(/\r?\n/)).toEqual(["bun link", ""]); + expect(stdout).toBeDefined(); + expect(await new Response(stdout).text()).toContain(`Success! Registered "moo"`); + expect(await exited).toBe(0); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "link", "moo"], + cwd: join(link_dir, "packages", "boba"), + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + })); + + expect(stderr).toBeDefined(); + err = await new Response(stderr).text(); + expect(err.replace(/^(.*?) v[^\n]+/, "$1").split(/\r?\n/)).toEqual(["bun link", ""]); + expect(stdout).toBeDefined(); + expect((await new Response(stdout).text()).replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ + "", + ` installed moo@link:moo`, + "", + "", + " 1 package installed", + ]); + expect(await exited).toBe(0); + expect(await file(join(link_dir, "packages", "boba", "node_modules", "moo", "package.json")).json()).toEqual({ + name: "moo", + version: "0.0.1", + }); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "unlink"], + cwd: join(link_dir, "packages", "moo"), + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + })); + + expect(stderr).toBeDefined(); + err = await new Response(stderr).text(); + expect(err.replace(/^(.*?) v[^\n]+/, "$1").split(/\r?\n/)).toEqual(["bun unlink", ""]); + expect(stdout).toBeDefined(); + expect(await new Response(stdout).text()).toContain(`success: unlinked package "moo"`); + expect(await exited).toBe(0); + + // link the workspace root package to a workspace package + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "link"], + cwd: link_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + })); + + expect(stderr).toBeDefined(); + err = await new Response(stderr).text(); + expect(err.replace(/^(.*?) v[^\n]+/, "$1").split(/\r?\n/)).toEqual(["bun link", ""]); + expect(stdout).toBeDefined(); + expect(await new Response(stdout).text()).toContain(`Success! Registered "foo"`); + expect(await exited).toBe(0); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "link", "foo"], + cwd: join(link_dir, "packages", "boba"), + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + })); + + expect(stderr).toBeDefined(); + err = await new Response(stderr).text(); + expect(err.replace(/^(.*?) v[^\n]+/, "$1").split(/\r?\n/)).toEqual(["bun link", ""]); + expect(stdout).toBeDefined(); + expect((await new Response(stdout).text()).replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ + "", + ` installed foo@link:foo`, + "", + "", + " 1 package installed", + ]); + expect(await file(join(link_dir, "packages", "boba", "node_modules", "foo", "package.json")).json()).toEqual({ + name: "foo", + version: "1.0.0", + workspaces: ["packages/*"], + }); + expect(await exited).toBe(0); + + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "unlink"], + cwd: link_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + })); + + expect(stderr).toBeDefined(); + err = await new Response(stderr).text(); + expect(err.replace(/^(.*?) v[^\n]+/, "$1").split(/\r?\n/)).toEqual(["bun unlink", ""]); + expect(stdout).toBeDefined(); + expect(await new Response(stdout).text()).toContain(`success: unlinked package "foo"`); + expect(await exited).toBe(0); +}); + it("should link package", async () => { const link_name = basename(link_dir).slice("bun-link.".length); await writeFile(