diff options
-rw-r--r-- | src/install/install.zig | 116 | ||||
-rw-r--r-- | src/install/lockfile.zig | 22 | ||||
-rw-r--r-- | test/bun.js/install/bun-add.test.ts | 218 | ||||
-rw-r--r-- | test/bun.js/install/bun-install.test.ts | 194 | ||||
-rw-r--r-- | test/bun.js/install/dummy.registry.ts | 83 |
5 files changed, 448 insertions, 185 deletions
diff --git a/src/install/install.zig b/src/install/install.zig index 17848a044..3a165d234 100644 --- a/src/install/install.zig +++ b/src/install/install.zig @@ -4261,7 +4261,8 @@ pub const PackageManager = struct { ) !void { const G = JSAst.G; - var remaining: usize = updates.len; + var remaining = updates.len; + var replacing: usize = 0; // There are three possible scenarios here // 1. There is no "dependencies" (or equivalent list) or it is empty @@ -4275,8 +4276,12 @@ pub const PackageManager = struct { if (query.expr.data == .e_object) { if (query.expr.asProperty(update.name)) |value| { if (value.expr.data == .e_string) { - updates[i].e_string = value.expr.data.e_string; - remaining -= 1; + if (update.resolved_name.isEmpty()) { + updates[i].e_string = value.expr.data.e_string; + remaining -= 1; + } else { + replacing += 1; + } } break :outer; } @@ -4295,7 +4300,7 @@ pub const PackageManager = struct { } } - var new_dependencies = try allocator.alloc(G.Property, dependencies.len + remaining); + var new_dependencies = try allocator.alloc(G.Property, dependencies.len + remaining - replacing); std.mem.copy(G.Property, new_dependencies, dependencies); std.mem.set(G.Property, new_dependencies[dependencies.len..], G.Property{}); @@ -4305,11 +4310,32 @@ pub const PackageManager = struct { var k: usize = 0; while (k < new_dependencies.len) : (k += 1) { + if (new_dependencies[k].key) |key| { + if (key.data.e_string.eql(string, update.name)) { + if (update.resolved_name.isEmpty()) { + // This actually is a duplicate + // like "react" appearing in both "dependencies" and "optionalDependencies" + // For this case, we'll just swap remove it + if (new_dependencies.len > 1) { + new_dependencies[k] = new_dependencies[new_dependencies.len - 1]; + new_dependencies = new_dependencies[0 .. new_dependencies.len - 1]; + } else { + new_dependencies = &[_]G.Property{}; + } + continue; + } + new_dependencies[k].key = null; + } + } + if (new_dependencies[k].key == null) { new_dependencies[k].key = JSAst.Expr.init( JSAst.E.String, JSAst.E.String{ - .data = update.name, + .data = if (update.resolved_name.isEmpty()) + update.name + else + try allocator.dupe(u8, update.resolved_name.slice(update.version_buf)), }, logger.Loc.Empty, ); @@ -4325,18 +4351,6 @@ pub const PackageManager = struct { updates[j].e_string = new_dependencies[k].value.?.data.e_string; continue :outer; } - - // This actually is a duplicate - // like "react" appearing in both "dependencies" and "optionalDependencies" - // For this case, we'll just swap remove it - if (new_dependencies[k].key.?.data.e_string.eql(string, update.name)) { - if (new_dependencies.len > 1) { - new_dependencies[k] = new_dependencies[new_dependencies.len - 1]; - new_dependencies = new_dependencies[0 .. new_dependencies.len - 1]; - } else { - new_dependencies = &[_]G.Property{}; - } - } } } @@ -4400,13 +4414,19 @@ pub const PackageManager = struct { } for (updates) |*update| { - var str = update.e_string.?; - - if (update.version.tag == .uninitialized) { - str.data = latest; - } else { - str.data = update.version.literal.slice(update.version_buf); - } + update.e_string.?.data = switch (update.resolution.tag) { + .npm => if (update.version.tag == .npm and update.version.value.npm.version.input.len == 0) + std.fmt.allocPrint(allocator, "^{}", .{ + update.resolution.value.npm.version.fmt(update.version_buf), + }) catch unreachable + else + null, + .uninitialized => switch (update.version.tag) { + .uninitialized => latest, + else => null, + }, + else => null, + } orelse update.version.literal.slice(update.version_buf); } } }; @@ -5241,9 +5261,10 @@ pub const PackageManager = struct { pub const UpdateRequest = struct { name: string = "", name_hash: PackageNameHash = 0, - resolved_version_buf: string = "", - version: Dependency.Version = Dependency.Version{}, + version: Dependency.Version = .{}, version_buf: []const u8 = "", + resolution: Resolution = .{}, + resolved_name: String = .{}, missing_version: bool = false, failed: bool = false, // This must be cloned to handle when the AST store resets @@ -5262,7 +5283,8 @@ pub const PackageManager = struct { // add // remove outer: for (positionals) |positional| { - var value = std.mem.trim(u8, positional, " \n\r\t"); + var input = std.mem.trim(u8, positional, " \n\r\t"); + var value = input; switch (op) { .link, .unlink => if (!strings.hasPrefixComptime(value, "link:")) { value = std.fmt.allocPrint(allocator, "link:{s}", .{value}) catch unreachable; @@ -5297,23 +5319,31 @@ pub const PackageManager = struct { }); Global.exit(1); }; + if (switch (version.tag) { + .dist_tag => version.value.dist_tag.name.eql(placeholder, value, value), + .npm => version.value.npm.name.eql(placeholder, value, value), + else => false, + }) { + value = std.fmt.allocPrint(allocator, "npm:{s}", .{value}) catch unreachable; + version = Dependency.parseWithOptionalTag( + allocator, + placeholder, + value, + null, + &SlicedString.init(value, value), + log, + ) orelse { + Output.prettyErrorln("<r><red>error<r><d>:<r> unrecognised dependency format: {s}", .{ + positional, + }); + Global.exit(1); + }; + } switch (version.tag) { - .dist_tag => if (version.value.dist_tag.name.eql(placeholder, value, value)) { - value = std.fmt.allocPrint(allocator, "npm:{s}", .{value}) catch unreachable; - version = Dependency.parseWithOptionalTag( - allocator, - placeholder, - value, - null, - &SlicedString.init(value, value), - log, - ) orelse { - Output.prettyErrorln("<r><red>error<r><d>:<r> unrecognised dependency format: {s}", .{ - positional, - }); - Global.exit(1); - }; - }, + .dist_tag, .npm => version.literal = if (strings.lastIndexOfChar(value, '@')) |at| + String.init(value, value[at + 1 ..]) + else + String.from(""), else => {}, } diff --git a/src/install/lockfile.zig b/src/install/lockfile.zig index df2436ae8..b948557bf 100644 --- a/src/install/lockfile.zig +++ b/src/install/lockfile.zig @@ -689,19 +689,25 @@ pub fn clean(old: *Lockfile, updates: []PackageManager.UpdateRequest) !*Lockfile // Don't allow invalid memory to happen if (updates.len > 0) { - const dep_list = new.packages.items(.dependencies)[0]; - const res_list = new.packages.items(.resolutions)[0]; + const slice = new.packages.slice(); + const names = slice.items(.name); + const resolutions = slice.items(.resolution); + const dep_list = slice.items(.dependencies)[0]; + const res_list = slice.items(.resolutions)[0]; const root_deps: []const Dependency = dep_list.get(new.buffers.dependencies.items); - const new_resolutions: []const PackageID = res_list.get(new.buffers.resolutions.items); + const resolved_ids: []const PackageID = res_list.get(new.buffers.resolutions.items); for (updates) |update, update_i| { - if (update.version.tag == .uninitialized) { + if (update.resolution.tag == .uninitialized) { + const name_hash = String.Builder.stringHash(update.name); for (root_deps) |dep, i| { - if (dep.name_hash == String.Builder.stringHash(update.name)) { - if (new_resolutions[i] > new.packages.len) continue; + if (dep.name_hash == name_hash) { + const package_id = resolved_ids[i]; + if (package_id > new.packages.len) continue; updates[update_i].version_buf = new.buffers.string_bytes.items; updates[update_i].version = dep.version; - updates[update_i].resolved_version_buf = new.buffers.string_bytes.items; + updates[update_i].resolution = resolutions[package_id]; + updates[update_i].resolved_name = names[package_id]; updates[update_i].missing_version = true; } } @@ -2367,7 +2373,7 @@ pub const Package = extern struct { continue; }; - if (to_deps[to_i].eql(from_dep, from_lockfile.buffers.string_bytes.items, to_lockfile.buffers.string_bytes.items)) { + if (to_deps[to_i].eql(from_dep, to_lockfile.buffers.string_bytes.items, from_lockfile.buffers.string_bytes.items)) { mapping[to_i] = @truncate(PackageID, i); continue; } diff --git a/test/bun.js/install/bun-add.test.ts b/test/bun.js/install/bun-add.test.ts index 2061ce5ec..42affcb70 100644 --- a/test/bun.js/install/bun-add.test.ts +++ b/test/bun.js/install/bun-add.test.ts @@ -1,25 +1,49 @@ -import { spawn } from "bun"; +import { file, spawn } from "bun"; import { + afterAll, afterEach, + beforeAll, beforeEach, expect, it, } from "bun:test"; import { bunExe } from "bunExe"; import { bunEnv as env } from "bunEnv"; -import { mkdtemp, rm, writeFile } from "fs/promises"; -import { basename, join, relative } from "path"; +import { + access, + mkdir, + mkdtemp, + readlink, + rm, + writeFile, +} from "fs/promises"; +import { join, relative } from "path"; import { tmpdir } from "os"; +import { + dummyAfterAll, + dummyAfterEach, + dummyBeforeAll, + dummyBeforeEach, + dummyRegistry, + package_dir, + readdirSorted, + requested, + root_url, + setHandler, +} from "./dummy.registry"; + +beforeAll(dummyBeforeAll); +afterAll(dummyAfterAll); -let package_dir, add_dir; +let add_dir; beforeEach(async () => { add_dir = await mkdtemp(join(tmpdir(), "bun-add.test")); - package_dir = await mkdtemp(join(tmpdir(), "bun-add.pkg")); + await dummyBeforeEach(); }); afterEach(async () => { await rm(add_dir, { force: true, recursive: true }); - await rm(package_dir, { force: true, recursive: true }); + await dummyAfterEach(); }); it("should add existing package", async () => { @@ -33,7 +57,7 @@ it("should add existing package", async () => { })); const add_path = relative(package_dir, add_dir); const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "add",`file:${add_path}`], + cmd: [bunExe(), "add", `file:${add_path}`], cwd: package_dir, stdout: null, stdin: "pipe", @@ -57,6 +81,13 @@ it("should add existing package", async () => { " 1 packages installed", ]); expect(await exited).toBe(0); + expect(await file(join(package_dir, "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + dependencies: { + foo: `file:${add_path}`, + }, + }); }); it("should reject missing package", async () => { @@ -66,7 +97,7 @@ it("should reject missing package", async () => { })); const add_path = relative(package_dir, add_dir); const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "add",`file:${add_path}`], + cmd: [bunExe(), "add", `file:${add_path}`], cwd: package_dir, stdout: null, stdin: "pipe", @@ -84,6 +115,10 @@ it("should reject missing package", async () => { const out = await new Response(stdout).text(); expect(out).toBe(""); expect(await exited).toBe(1); + expect(await file(join(package_dir, "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); }); it("should reject invalid path without segfault", async () => { @@ -97,7 +132,7 @@ it("should reject invalid path without segfault", async () => { })); const add_path = relative(package_dir, add_dir); const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "add",`file://${add_path}`], + cmd: [bunExe(), "add", `file://${add_path}`], cwd: package_dir, stdout: null, stdin: "pipe", @@ -115,4 +150,169 @@ it("should reject invalid path without segfault", async () => { const out = await new Response(stdout).text(); expect(out).toBe(""); expect(await exited).toBe(1); + expect(await file(join(package_dir, "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); +}); + +it("should add dependency with specified semver", async () => { + const urls: string[] = []; + setHandler(dummyRegistry(urls, "0.0.3", { + bin: { + "baz-run": "index.js", + }, + })); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "add", "baz@~0.0.2", "--config", import.meta.dir + "/basic.toml"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(stdout).toBeDefined(); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ + "", + " installed baz@0.0.3 with binaries:", + " - baz-run", + "", + "", + " 1 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls).toEqual([ + `${root_url}/baz`, + `${root_url}/baz.tgz`, + ]); + expect(requested).toBe(2); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ + ".bin", + ".cache", + "baz", + ]); + expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toEqual([ + "baz-run", + ]); + expect(await readlink(join(package_dir, "node_modules", ".bin", "baz-run"))).toBe( + join("..", "baz", "index.js"), + ); + expect(await readdirSorted(join(package_dir, "node_modules", "baz"))).toEqual([ + "index.js", + "package.json", + ]); + expect(await file(join(package_dir, "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + expect(await file(join(package_dir, "package.json")).json()).toEqual({ + name: "foo", + version: "0.0.1", + dependencies: { + baz: "~0.0.2", + }, + }); + await access(join(package_dir, "bun.lockb")); +}); + +it("should add dependency alongside workspaces", async () => { + const urls: string[] = []; + setHandler(dummyRegistry(urls, "0.0.3", { + bin: { + "baz-run": "index.js", + }, + })); + await writeFile( + join(package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + workspaces: ["packages/bar"], + }), + ); + await mkdir(join(package_dir, "packages", "bar"), { recursive: true }); + await writeFile( + join(package_dir, "packages", "bar", "package.json"), + JSON.stringify({ + name: "bar", + version: "0.0.2", + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "add", "baz", "--config", import.meta.dir + "/basic.toml"], + cwd: package_dir, + stdout: null, + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(stderr).toBeDefined(); + const err = await new Response(stderr).text(); + expect(err).toContain("Saved lockfile"); + expect(stdout).toBeDefined(); + const out = await new Response(stdout).text(); + expect(out.replace(/\s*\[[0-9\.]+ms\]\s*$/, "").split(/\r?\n/)).toEqual([ + " + bar@workspace:packages/bar", + "", + " installed baz@0.0.3 with binaries:", + " - baz-run", + "", + "", + " 2 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls).toEqual([ + `${root_url}/baz`, + `${root_url}/baz.tgz`, + ]); + expect(requested).toBe(2); + expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ + ".bin", + ".cache", + "bar", + "baz", + ]); + expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toEqual([ + "baz-run", + ]); + expect(await readlink(join(package_dir, "node_modules", ".bin", "baz-run"))).toBe( + join("..", "baz", "index.js"), + ); + expect(await readlink(join(package_dir, "node_modules", "bar"))).toBe( + join("..", "packages", "bar"), + ); + expect(await readdirSorted(join(package_dir, "node_modules", "baz"))).toEqual([ + "index.js", + "package.json", + ]); + expect(await file(join(package_dir, "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + expect(await file(join(package_dir, "package.json")).json()).toEqual({ + name: "foo", + version: "0.0.1", + workspaces: [ "packages/bar" ], + dependencies: { + baz: "^0.0.3", + }, + }); + await access(join(package_dir, "bun.lockb")); }); diff --git a/test/bun.js/install/bun-install.test.ts b/test/bun.js/install/bun-install.test.ts index f1139bdd5..2bf59211e 100644 --- a/test/bun.js/install/bun-install.test.ts +++ b/test/bun.js/install/bun-install.test.ts @@ -1,4 +1,4 @@ -import { file, resolveSync, spawn } from "bun"; +import { file, spawn } from "bun"; import { afterAll, afterEach, @@ -12,87 +12,31 @@ import { bunEnv as env } from "bunEnv"; import { access, mkdir, - mkdtemp, - readdir, readlink, - rm, writeFile, } from "fs/promises"; -import { basename, join } from "path"; -import { tmpdir } from "os"; -import { realpathSync } from "fs"; - -let handler, package_dir, requested, server; - -function dummyRegistry(urls, version = "0.0.2", props = {}) { - return async (request) => { - urls.push(request.url); - expect(request.method).toBe("GET"); - if (request.url.endsWith(".tgz")) { - return new Response(file(join(import.meta.dir, basename(request.url)))); - } - expect(request.headers.get("accept")).toBe( - "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*", - ); - expect(request.headers.get("npm-auth-type")).toBe(null); - expect(await request.text()).toBe(""); - const name = request.url.slice(request.url.lastIndexOf("/") + 1); - return new Response( - JSON.stringify({ - name, - versions: { - [version]: { - name, - version, - dist: { - tarball: `${request.url}.tgz`, - }, - ...props, - }, - }, - "dist-tags": { - latest: version, - }, - }), - ); - }; -} - -async function readdirSorted(path: PathLike): Promise<string[]> { - const results = await readdir(path); - results.sort(); - return results; -} - -function resetHanlder() { - handler = () => new Response("Tea Break~", { status: 418 }); -} +import { join } from "path"; +import { + dummyAfterAll, + dummyAfterEach, + dummyBeforeAll, + dummyBeforeEach, + dummyRegistry, + package_dir, + readdirSorted, + requested, + root_url, + setHandler, +} from "./dummy.registry"; -beforeAll(() => { - server = Bun.serve({ - async fetch(request) { - requested++; - return await handler(request); - }, - port: 54321, - }); -}); -afterAll(() => { - server.stop(); -}); -beforeEach(async () => { - resetHanlder(); - requested = 0; - package_dir = realpathSync(await mkdtemp(join(tmpdir(), "bun-install.test"))); -}); -afterEach(async () => { - resetHanlder(); - await rm(package_dir, { force: true, recursive: true }); -}); +beforeAll(dummyBeforeAll); +afterAll(dummyAfterAll); +beforeEach(dummyBeforeEach); +afterEach(dummyAfterEach); it("should handle missing package", async () => { const urls: string[] = []; - handler = async (request) => { + setHandler(async (request) => { expect(request.method).toBe("GET"); expect(request.headers.get("accept")).toBe( "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*", @@ -101,7 +45,7 @@ it("should handle missing package", async () => { expect(await request.text()).toBe(""); urls.push(request.url); return new Response("bar", { status: 404 }); - }; + }); const { stdout, stderr, exited } = spawn({ cmd: [ bunExe(), @@ -124,7 +68,7 @@ it("should handle missing package", async () => { expect(stdout).toBeDefined(); expect(await new Response(stdout).text()).toBe(""); expect(await exited).toBe(1); - expect(urls).toEqual(["http://localhost:54321/foo"]); + expect(urls).toEqual([`${root_url}/foo`]); expect(requested).toBe(1); try { await access(join(package_dir, "bun.lockb")); @@ -136,9 +80,9 @@ it("should handle missing package", async () => { it("should handle @scoped authentication", async () => { let seen_token = false; - const url = "http://localhost:54321/@foo/bar"; + const url = `${root_url}/@foo/bar`; const urls: string[] = []; - handler = async (request) => { + setHandler(async (request) => { expect(request.method).toBe("GET"); expect(request.headers.get("accept")).toBe( "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*", @@ -153,7 +97,7 @@ it("should handle @scoped authentication", async () => { expect(await request.text()).toBe(""); urls.push(request.url); return new Response("Feeling lucky?", { status: 555 }); - }; + }); const { stdout, stderr, exited } = spawn({ cmd: [ bunExe(), @@ -187,7 +131,7 @@ it("should handle @scoped authentication", async () => { it("should handle empty string in dependencies", async () => { const urls: string[] = []; - handler = dummyRegistry(urls); + setHandler(dummyRegistry(urls)); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -218,8 +162,8 @@ it("should handle empty string in dependencies", async () => { ]); expect(await exited).toBe(0); expect(urls).toEqual([ - "http://localhost:54321/bar", - "http://localhost:54321/bar.tgz", + `${root_url}/bar`, + `${root_url}/bar.tgz`, ]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ @@ -601,7 +545,7 @@ it("should handle life-cycle scripts within workspaces", async () => { it("should handle ^0 in dependencies", async () => { const urls: string[] = []; - handler = dummyRegistry(urls); + setHandler(dummyRegistry(urls)); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -632,8 +576,8 @@ it("should handle ^0 in dependencies", async () => { ]); expect(await exited).toBe(0); expect(urls).toEqual([ - "http://localhost:54321/bar", - "http://localhost:54321/bar.tgz", + `${root_url}/bar`, + `${root_url}/bar.tgz`, ]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ @@ -654,7 +598,7 @@ it("should handle ^0 in dependencies", async () => { it("should handle ^1 in dependencies", async () => { const urls: string[] = []; - handler = dummyRegistry(urls); + setHandler(dummyRegistry(urls)); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -681,7 +625,7 @@ it("should handle ^1 in dependencies", async () => { expect(stdout).toBeDefined(); expect(await new Response(stdout).text()).toBe(""); expect(await exited).toBe(1); - expect(urls).toEqual(["http://localhost:54321/bar"]); + expect(urls).toEqual([`${root_url}/bar`]); expect(requested).toBe(1); try { await access(join(package_dir, "bun.lockb")); @@ -693,7 +637,7 @@ it("should handle ^1 in dependencies", async () => { it("should handle ^0.0 in dependencies", async () => { const urls: string[] = []; - handler = dummyRegistry(urls); + setHandler(dummyRegistry(urls)); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -724,8 +668,8 @@ it("should handle ^0.0 in dependencies", async () => { ]); expect(await exited).toBe(0); expect(urls).toEqual([ - "http://localhost:54321/bar", - "http://localhost:54321/bar.tgz", + `${root_url}/bar`, + `${root_url}/bar.tgz`, ]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ @@ -746,7 +690,7 @@ it("should handle ^0.0 in dependencies", async () => { it("should handle ^0.1 in dependencies", async () => { const urls: string[] = []; - handler = dummyRegistry(urls); + setHandler(dummyRegistry(urls)); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -773,7 +717,7 @@ it("should handle ^0.1 in dependencies", async () => { expect(stdout).toBeDefined(); expect(await new Response(stdout).text()).toBe(""); expect(await exited).toBe(1); - expect(urls).toEqual(["http://localhost:54321/bar"]); + expect(urls).toEqual([`${root_url}/bar`]); expect(requested).toBe(1); try { await access(join(package_dir, "bun.lockb")); @@ -785,7 +729,7 @@ it("should handle ^0.1 in dependencies", async () => { it("should handle ^0.0.0 in dependencies", async () => { const urls: string[] = []; - handler = dummyRegistry(urls); + setHandler(dummyRegistry(urls)); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -812,7 +756,7 @@ it("should handle ^0.0.0 in dependencies", async () => { expect(stdout).toBeDefined(); expect(await new Response(stdout).text()).toBe(""); expect(await exited).toBe(1); - expect(urls).toEqual(["http://localhost:54321/bar"]); + expect(urls).toEqual([`${root_url}/bar`]); expect(requested).toBe(1); try { await access(join(package_dir, "bun.lockb")); @@ -824,7 +768,7 @@ it("should handle ^0.0.0 in dependencies", async () => { it("should handle ^0.0.2 in dependencies", async () => { const urls: string[] = []; - handler = dummyRegistry(urls); + setHandler(dummyRegistry(urls)); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -855,8 +799,8 @@ it("should handle ^0.0.2 in dependencies", async () => { ]); expect(await exited).toBe(0); expect(urls).toEqual([ - "http://localhost:54321/bar", - "http://localhost:54321/bar.tgz", + `${root_url}/bar`, + `${root_url}/bar.tgz`, ]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ @@ -877,7 +821,7 @@ it("should handle ^0.0.2 in dependencies", async () => { it("should handle ^0.0.2-rc in dependencies", async () => { const urls: string[] = []; - handler = dummyRegistry(urls, "0.0.2-rc"); + setHandler(dummyRegistry(urls, "0.0.2-rc")); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -908,8 +852,8 @@ it("should handle ^0.0.2-rc in dependencies", async () => { ]); expect(await exited).toBe(0); expect(urls).toEqual([ - "http://localhost:54321/bar", - "http://localhost:54321/bar.tgz", + `${root_url}/bar`, + `${root_url}/bar.tgz`, ]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ @@ -930,7 +874,7 @@ it("should handle ^0.0.2-rc in dependencies", async () => { it("should handle ^0.0.2-alpha.3+b4d in dependencies", async () => { const urls: string[] = []; - handler = dummyRegistry(urls, "0.0.2-alpha.3"); + setHandler(dummyRegistry(urls, "0.0.2-alpha.3")); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -961,8 +905,8 @@ it("should handle ^0.0.2-alpha.3+b4d in dependencies", async () => { ]); expect(await exited).toBe(0); expect(urls).toEqual([ - "http://localhost:54321/bar", - "http://localhost:54321/bar.tgz", + `${root_url}/bar`, + `${root_url}/bar.tgz`, ]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ @@ -983,11 +927,11 @@ it("should handle ^0.0.2-alpha.3+b4d in dependencies", async () => { it("should handle dependency aliasing", async () => { const urls = []; - handler = dummyRegistry(urls, "0.0.3", { + setHandler(dummyRegistry(urls, "0.0.3", { bin: { "baz-run": "index.js", }, - }); + })); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -1018,8 +962,8 @@ it("should handle dependency aliasing", async () => { ]); expect(await exited).toBe(0); expect(urls).toEqual([ - "http://localhost:54321/baz", - "http://localhost:54321/baz.tgz", + `${root_url}/baz`, + `${root_url}/baz.tgz`, ]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ @@ -1047,11 +991,11 @@ it("should handle dependency aliasing", async () => { it("should handle dependency aliasing (versioned)", async () => { const urls: string[] = []; - handler = dummyRegistry(urls, "0.0.3", { + setHandler(dummyRegistry(urls, "0.0.3", { bin: { "baz-run": "index.js", }, - }); + })); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -1082,8 +1026,8 @@ it("should handle dependency aliasing (versioned)", async () => { ]); expect(await exited).toBe(0); expect(urls).toEqual([ - "http://localhost:54321/baz", - "http://localhost:54321/baz.tgz", + `${root_url}/baz`, + `${root_url}/baz.tgz`, ]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ @@ -1111,11 +1055,11 @@ it("should handle dependency aliasing (versioned)", async () => { it("should handle dependency aliasing (dist-tagged)", async () => { const urls: string[] = []; - handler = dummyRegistry(urls, "0.0.3", { + setHandler(dummyRegistry(urls, "0.0.3", { bin: { "baz-run": "index.js", }, - }); + })); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -1146,8 +1090,8 @@ it("should handle dependency aliasing (dist-tagged)", async () => { ]); expect(await exited).toBe(0); expect(urls).toEqual([ - "http://localhost:54321/baz", - "http://localhost:54321/baz.tgz", + `${root_url}/baz`, + `${root_url}/baz.tgz`, ]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ @@ -1175,11 +1119,11 @@ it("should handle dependency aliasing (dist-tagged)", async () => { it("should not reinstall aliased dependencies", async () => { const urls = []; - handler = dummyRegistry(urls, "0.0.3", { + setHandler(dummyRegistry(urls, "0.0.3", { bin: { "baz-run": "index.js", }, - }); + })); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -1214,8 +1158,8 @@ it("should not reinstall aliased dependencies", async () => { ]); expect(await exited1).toBe(0); expect(urls).toEqual([ - "http://localhost:54321/baz", - "http://localhost:54321/baz.tgz", + `${root_url}/baz`, + `${root_url}/baz.tgz`, ]); expect(requested).toBe(2); expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ @@ -1290,7 +1234,7 @@ it("should not reinstall aliased dependencies", async () => { it("should handle GitHub URL in dependencies (user/repo)", async () => { const urls: string[] = []; - handler = dummyRegistry(urls); + setHandler(dummyRegistry(urls)); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -1357,7 +1301,7 @@ it("should handle GitHub URL in dependencies (user/repo)", async () => { it("should handle GitHub URL in dependencies (user/repo#commit-id)", async () => { const urls: string[] = []; - handler = dummyRegistry(urls); + setHandler(dummyRegistry(urls)); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -1442,7 +1386,7 @@ it("should handle GitHub URL in dependencies (user/repo#commit-id)", async () => it("should handle GitHub URL in dependencies (user/repo#tag)", async () => { const urls: string[] = []; - handler = dummyRegistry(urls); + setHandler(dummyRegistry(urls)); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -1527,7 +1471,7 @@ it("should handle GitHub URL in dependencies (user/repo#tag)", async () => { it("should handle GitHub URL in dependencies (github:user/repo#tag)", async () => { const urls: string[] = []; - handler = dummyRegistry(urls); + setHandler(dummyRegistry(urls)); await writeFile( join(package_dir, "package.json"), JSON.stringify({ @@ -1612,7 +1556,7 @@ it("should handle GitHub URL in dependencies (github:user/repo#tag)", async () = it("should handle GitHub URL in dependencies (https://github.com/user/repo.git)", async () => { const urls: string[] = []; - handler = dummyRegistry(urls); + setHandler(dummyRegistry(urls)); await writeFile( join(package_dir, "package.json"), JSON.stringify({ diff --git a/test/bun.js/install/dummy.registry.ts b/test/bun.js/install/dummy.registry.ts new file mode 100644 index 000000000..dd2680b97 --- /dev/null +++ b/test/bun.js/install/dummy.registry.ts @@ -0,0 +1,83 @@ +import { file } from "bun"; +import { expect } from "bun:test"; +import { realpathSync } from "fs"; +import { mkdtemp, readdir, rm } from "fs/promises"; +import { tmpdir } from "os"; +import { basename, join } from "path"; + +let handler, server; +export let package_dir, requested, root_url; + +export function dummyRegistry(urls, version = "0.0.2", props = {}) { + return async (request) => { + urls.push(request.url); + expect(request.method).toBe("GET"); + if (request.url.endsWith(".tgz")) { + return new Response(file(join(import.meta.dir, basename(request.url)))); + } + expect(request.headers.get("accept")).toBe( + "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*", + ); + expect(request.headers.get("npm-auth-type")).toBe(null); + expect(await request.text()).toBe(""); + const name = request.url.slice(request.url.lastIndexOf("/") + 1); + return new Response( + JSON.stringify({ + name, + versions: { + [version]: { + name, + version, + dist: { + tarball: `${request.url}.tgz`, + }, + ...props, + }, + }, + "dist-tags": { + latest: version, + }, + }), + ); + }; +} + +export async function readdirSorted(path: PathLike): Promise<string[]> { + const results = await readdir(path); + results.sort(); + return results; +} + +export function setHandler(newHandler) { + handler = newHandler; +} + +function resetHanlder() { + setHandler(() => new Response("Tea Break~", { status: 418 })); +} + +export function dummyBeforeAll() { + server = Bun.serve({ + async fetch(request) { + requested++; + return await handler(request); + }, + port: 54321, + }); + root_url = "http://localhost:54321"; +} + +export function dummyAfterAll() { + server.stop(); +} + +export async function dummyBeforeEach() { + resetHanlder(); + requested = 0; + package_dir = realpathSync(await mkdtemp(join(tmpdir(), "bun-install.test"))); +} + +export async function dummyAfterEach() { + resetHanlder(); + await rm(package_dir, { force: true, recursive: true }); +} |