diff --git a/src/install/resolvers/folder_resolver.zig b/src/install/resolvers/folder_resolver.zig index 968ee57ca99732..c16f0c8ed97b07 100644 --- a/src/install/resolvers/folder_resolver.zig +++ b/src/install/resolvers/folder_resolver.zig @@ -33,7 +33,7 @@ pub const FolderResolution = union(Tag) { return std.hash.Wyhash.hash(0, normalized_path); } - pub fn NewResolver(comptime tag: Resolution.Tag) type { + fn NewResolver(comptime tag: Resolution.Tag) type { return struct { folder_path: string, @@ -50,11 +50,10 @@ pub const FolderResolution = union(Tag) { }; } - pub const Resolver = NewResolver(Resolution.Tag.folder); - pub const SymlinkResolver = NewResolver(Resolution.Tag.symlink); - pub const WorkspaceResolver = NewResolver(Resolution.Tag.workspace); - pub const CacheFolderResolver = struct { - folder_path: []const u8 = "", + const Resolver = NewResolver(Resolution.Tag.folder); + const SymlinkResolver = NewResolver(Resolution.Tag.symlink); + const WorkspaceResolver = NewResolver(Resolution.Tag.workspace); + const CacheFolderResolver = struct { version: Semver.Version, pub fn resolve(this: @This(), comptime Builder: type, _: Builder, _: JSAst.Expr) !Resolution { @@ -182,14 +181,18 @@ pub const FolderResolution = union(Tag) { if (entry.found_existing) return entry.value_ptr.*; const package: Lockfile.Package = switch (global_or_relative) { - .global => readPackageJSONFromDisk( - manager, - abs, - version, - Features.link, - SymlinkResolver, - SymlinkResolver{ .folder_path = non_normalized_path }, - ), + .global => brk: { + var path: [bun.MAX_PATH_BYTES]u8 = undefined; + std.mem.copy(u8, &path, non_normalized_path); + break :brk readPackageJSONFromDisk( + manager, + abs, + version, + Features.link, + SymlinkResolver, + SymlinkResolver{ .folder_path = path[0..non_normalized_path.len] }, + ); + }, .relative => |tag| switch (tag) { .folder => readPackageJSONFromDisk( manager, diff --git a/test/bun.js/install/bun-link.test.ts b/test/bun.js/install/bun-link.test.ts index c2cf459e28a6e2..137242cc45cf17 100644 --- a/test/bun.js/install/bun-link.test.ts +++ b/test/bun.js/install/bun-link.test.ts @@ -1,24 +1,35 @@ import { spawn } from "bun"; -import { afterEach, beforeEach, expect, it } from "bun:test"; +import { afterAll, afterEach, beforeAll, beforeEach, expect, it } from "bun:test"; import { bunExe } from "bunExe"; import { bunEnv as env } from "bunEnv"; -import { mkdtemp, realpath, rm, writeFile } from "fs/promises"; +import { access, mkdtemp, readlink, realpath, rm, writeFile } from "fs/promises"; import { basename, join } from "path"; import { tmpdir } from "os"; +import { + dummyAfterAll, + dummyAfterEach, + dummyBeforeAll, + dummyBeforeEach, + package_dir, + readdirSorted, +} from "./dummy.registry"; -let package_dir, link_dir; +beforeAll(dummyBeforeAll); +afterAll(dummyAfterAll); + +let link_dir; beforeEach(async () => { link_dir = await mkdtemp(join(await realpath(tmpdir()), "bun-link.test")); - package_dir = await mkdtemp(join(await realpath(tmpdir()), "bun-link.pkg")); + await dummyBeforeEach(); }); afterEach(async () => { await rm(link_dir, { force: true, recursive: true }); - await rm(package_dir, { force: true, recursive: true }); + await dummyAfterEach(); }); it("should link package", async () => { - var link_name = basename(link_dir).slice("bun-link.".length); + const link_name = basename(link_dir).slice("bun-link.".length); await writeFile( join(link_dir, "package.json"), JSON.stringify({ @@ -114,13 +125,12 @@ it("should link package", async () => { const err4 = await new Response(stderr4).text(); expect(err4).toContain(`error: package "${link_name}" is not linked`); expect(stdout4).toBeDefined(); - const out4 = await new Response(stdout4).text(); expect(await new Response(stdout4).text()).toBe(""); expect(await exited4).toBe(1); }); it("should link scoped package", async () => { - var link_name = `@${basename(link_dir).slice("bun-link.".length)}/foo`; + const link_name = `@${basename(link_dir).slice("bun-link.".length)}/foo`; await writeFile( join(link_dir, "package.json"), JSON.stringify({ @@ -216,7 +226,121 @@ it("should link scoped package", async () => { const err4 = await new Response(stderr4).text(); expect(err4).toContain(`error: package "${link_name}" is not linked`); expect(stdout4).toBeDefined(); - const out4 = await new Response(stdout4).text(); expect(await new Response(stdout4).text()).toBe(""); expect(await exited4).toBe(1); }); + +it("should link dependency without crashing", async () => { + const link_name = basename(link_dir).slice("bun-link.".length) + "-really-long-name"; + await writeFile( + join(link_dir, "package.json"), + JSON.stringify({ + name: link_name, + version: "0.0.1", + bin: { + [link_name]: `${link_name}.js`, + }, + }), + ); + await writeFile(join(link_dir, `${link_name}.js`), "console.log(42);"); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.2", + dependencies: { + [link_name]: `link:${link_name}`, + }, + }), + ); + + const { + stdout: stdout1, + stderr: stderr1, + exited: exited1, + } = spawn({ + cmd: [bunExe(), "link"], + cwd: link_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr1).toBeDefined(); + const err1 = await new Response(stderr1).text(); + expect(err1.replace(/^(.*?) v[^\n]+/, "$1").split(/\r?\n/)).toEqual(["bun link", ""]); + expect(stdout1).toBeDefined(); + expect(await new Response(stdout1).text()).toContain(`Success! Registered \\"${link_name}\\"`); + expect(await exited1).toBe(0); + + const { + stdout: stdout2, + stderr: stderr2, + exited: exited2, + } = spawn({ + cmd: [bunExe(), "install", "--config", import.meta.dir + "/basic.toml"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr2).toBeDefined(); + const err2 = await new Response(stderr2).text(); + expect(err2.replace(/^(.*?) v[^\n]+/, "$1").split(/\r?\n/)).toEqual(["bun install", " Saved lockfile", ""]); + expect(stdout2).toBeDefined(); + const out2 = await new Response(stdout2).text(); + expect(out2.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ + ` + ${link_name}@link:${link_name}`, + "", + " 1 packages installed", + ]); + expect(await exited2).toBe(0); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", link_name].sort()); + expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toEqual([link_name]); + expect(await readlink(join(package_dir, "node_modules", ".bin", link_name))).toBe( + join("..", link_name, `${link_name}.js`), + ); + expect(await readdirSorted(join(package_dir, "node_modules", link_name))).toEqual( + ["package.json", `${link_name}.js`].sort(), + ); + await access(join(package_dir, "bun.lockb")); + + const { + stdout: stdout3, + stderr: stderr3, + exited: exited3, + } = spawn({ + cmd: [bunExe(), "unlink"], + cwd: link_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr3).toBeDefined(); + const err3 = await new Response(stderr3).text(); + expect(err3.replace(/^(.*?) v[^\n]+/, "$1").split(/\r?\n/)).toEqual(["bun unlink", ""]); + expect(stdout3).toBeDefined(); + expect(await new Response(stdout3).text()).toContain(`success: unlinked package "${link_name}"`); + expect(await exited3).toBe(0); + + const { + stdout: stdout4, + stderr: stderr4, + exited: exited4, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr4).toBeDefined(); + const err4 = await new Response(stderr4).text(); + expect(err4).toContain(`error: FileNotFound installing ${link_name}`); + expect(stdout4).toBeDefined(); + expect(await new Response(stdout4).text()).toBe(""); + expect(await exited4).toBe(0); +}); diff --git a/test/fixtures/bun-link-to-pkg-fixture/bun.lockb b/test/fixtures/bun-link-to-pkg-fixture/bun.lockb index 91f9f0e3507a41..1f6e25aa6afbe6 100755 Binary files a/test/fixtures/bun-link-to-pkg-fixture/bun.lockb and b/test/fixtures/bun-link-to-pkg-fixture/bun.lockb differ