diff --git a/test/cli/install/__snapshots__/bun-install.test.ts.snap b/test/cli/install/__snapshots__/bun-install.test.ts.snap index 5ffa8496fe..a0e509edac 100644 --- a/test/cli/install/__snapshots__/bun-install.test.ts.snap +++ b/test/cli/install/__snapshots__/bun-install.test.ts.snap @@ -34,3 +34,38 @@ error: "workspaces.packages" expects an array of strings, e.g. `; exports[`should handle modified git resolutions in bun.lock 1`] = `"{"lockfileVersion":0,"configVersion":1,"workspaces":{"":{"dependencies":{"jquery":"3.7.1"}}},"packages":{"jquery":["jquery@git+ssh://git@github.com/dylan-conway/install-test-8.git#3a1288830817d13da39e9231302261896f8721ea",{},"3a1288830817d13da39e9231302261896f8721ea"]}}"`; + +exports[`bun-install should report error on invalid format for package.json 1`] = ` +"1 | foo + ^ +error: Unexpected foo + at [dir]/package.json:1:1 +ParserError: failed to parse '[dir]/package.json' +" +`; + +exports[`bun-install should report error on invalid format for dependencies 1`] = ` +"1 | {"name":"foo","version":"0.0.1","dependencies":[]} + ^ +error: dependencies expects a map of specifiers, e.g. + "dependencies": { + "bun": "latest" + } + at [dir]/package.json:1:33 +" +`; + +exports[`bun-install should report error on invalid format for workspaces 1`] = ` +"1 | {"name":"foo","version":"0.0.1","workspaces":{"packages":{"bar":true}}} + ^ +error: "workspaces.packages" expects an array of strings, e.g. + "workspaces": { + "packages": [ + "path/to/package" + ] + } + at [dir]/package.json:1:58 +" +`; + +exports[`bun-install should handle modified git resolutions in bun.lock 1`] = `"{"lockfileVersion":0,"configVersion":1,"workspaces":{"":{"dependencies":{"jquery":"3.7.1"}}},"packages":{"jquery":["jquery@git+ssh://git@github.com/dylan-conway/install-test-8.git#3a1288830817d13da39e9231302261896f8721ea",{},"3a1288830817d13da39e9231302261896f8721ea"]}}"`; diff --git a/test/cli/install/bun-install.test.ts b/test/cli/install/bun-install.test.ts index a772790949..58fb82ce8d 100644 --- a/test/cli/install/bun-install.test.ts +++ b/test/cli/install/bun-install.test.ts @@ -1,16 +1,5 @@ import { file, listen, Socket, spawn, write } from "bun"; -import { - afterAll, - afterEach, - beforeAll, - beforeEach, - describe, - expect, - it, - jest, - setDefaultTimeout, - test, -} from "bun:test"; +import { afterAll, beforeAll, describe, expect, it, jest, setDefaultTimeout, test } from "bun:test"; import { access, cp, exists, mkdir, readlink, rm, stat, writeFile } from "fs/promises"; import { bunEnv, @@ -28,16 +17,13 @@ import { } from "harness"; import { join, resolve, sep } from "path"; import { + createTestContext, + destroyTestContext, dummyAfterAll, - dummyAfterEach, dummyBeforeAll, - dummyBeforeEach, - dummyRegistry, - getPort, - package_dir, - requested, - root_url, - setHandler, + dummyRegistryForContext, + setContextHandler, + type TestContext, } from "./dummy.registry.js"; expect.extend({ @@ -67,429 +53,457 @@ beforeAll(() => { }); afterAll(dummyAfterAll); -beforeEach(async () => { - await dummyBeforeEach({ linker: "hoisted" }); -}); -afterEach(dummyAfterEach); -for (let input of ["abcdef", "65537", "-1"]) { - it(`bun install --network-concurrency=${input} fails`, async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - ` -{ - "name": "foo", - "version": "0.0.1", - "dependencies": { - "bar": "^1" +// Helper function that sets up test context and ensures cleanup +async function withContext( + opts: { linker?: "hoisted" | "isolated" } | undefined, + fn: (ctx: TestContext) => Promise, +): Promise { + const ctx = await createTestContext(opts ? { linker: opts.linker! } : undefined); + try { + await fn(ctx); + } finally { + destroyTestContext(ctx); } -}`, - ); - const { stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--network-concurrency", "abcdef"], - cwd: package_dir, - stdout: "inherit", - stdin: "inherit", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Expected --network-concurrency to be a number between 0 and 65535"); - expect(await exited).toBe(1); - expect(urls).toBeEmpty(); - }); } -it("bun install --network-concurrency=5 doesnt go over 5 concurrent requests", async () => { - const urls: string[] = []; - let maxConcurrentRequests = 0; - let concurrentRequestCounter = 0; - let totalRequests = 0; - setHandler(async function (request) { - concurrentRequestCounter++; - totalRequests++; - try { - await Bun.sleep(10); - maxConcurrentRequests = Math.max(maxConcurrentRequests, concurrentRequestCounter); +// Default context options for most tests +const defaultOpts = { linker: "hoisted" as const }; - if (concurrentRequestCounter > 20) { - throw new Error("Too many concurrent requests"); - } - } finally { - concurrentRequestCounter--; +describe.concurrent("bun-install", () => { + for (let input of ["abcdef", "65537", "-1"]) { + it(`bun install --network-concurrency=${input} fails`, async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + ` + { + "name": "foo", + "version": "0.0.1", + "dependencies": { + "bar": "^1" } - - return new Response("404", { status: 404 }); - }); - await writeFile( - join(package_dir, "package.json"), - ` -{ - "name": "foo", - "version": "0.0.1", - "dependencies": { - "bar1": "^1", - "bar2": "^1", - "bar3": "^1", - "bar4": "^1", - "bar5": "^1", - "bar6": "^1", - "bar7": "^1", - "bar8": "^1", - "bar9": "^1", - "bar10": "^1", - "bar11": "^1", - "bar12": "^1", - "bar13": "^1", - "bar14": "^1", - "bar15": "^1", - "bar16": "^1", - "bar17": "^1", - "bar18": "^1", - "bar19": "^1", - "bar20": "^1", - "bar21": "^1", - "bar22": "^1", - "bar23": "^1", - "bar24": "^1", - "bar25": "^1", - "bar26": "^1", - "bar27": "^1", - "bar28": "^1", - "bar29": "^1", - "bar30": "^1", - "bar31": "^1", - "bar32": "^1", - "bar33": "^1", - "bar34": "^1", - "bar35": "^1", - "bar36": "^1", - "bar37": "^1", - "bar38": "^1", - "bar39": "^1", - "bar40": "^1", - "bar41": "^1", - "bar42": "^1", - "bar43": "^1", - "bar44": "^1", - "bar45": "^1", - "bar46": "^1", - "bar47": "^1", - "bar48": "^1", - "bar49": "^1", - "bar50": "^1", - "bar51": "^1", - } -}`, - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--network-concurrency", "5"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(await exited).toBe(1); - expect(urls).toBeEmpty(); - expect(maxConcurrentRequests).toBeLessThanOrEqual(5); - expect(totalRequests).toBe(51); - - expect(err).toContain("failed to resolve"); - expect(await stdout.text()).toEqual(expect.stringContaining("bun install v1.")); -}); - -it("should not error when package.json has comments and trailing commas", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - ` - { - "name": "foo", - "version": "0.0.1", - "dependencies": { - "bar": "^1", - }, - } -`, - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain('error: No version matching "^1" found for specifier "bar" (but package exists)'); - expect(await stdout.text()).toEqual(expect.stringContaining("bun install v1.")); - expect(await exited).toBe(1); - expect(urls.sort()).toEqual([`${root_url}/bar`]); - expect(requested).toBe(1); - try { - await access(join(package_dir, "bun.lockb")); - expect.unreachable(); - } catch (err: any) { - expect(err.code).toBe("ENOENT"); - } -}); - -describe("chooses", () => { - async function runTest(latest: string, range: string, chosen = "0.0.5") { - const exeName: string = { - "0.0.5": "baz-exec", - "0.0.3": "baz-run", - }[chosen]!; - if (!exeName) throw new Error("exeName not found"); - - const urls: string[] = []; - setHandler( - dummyRegistry(urls, { - "0.0.5": { - bin: { - "baz-exec": "index.js", - }, - }, - - "0.0.3": { - bin: { - "baz-run": "index.js", - }, - }, - latest, - }), - ); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - baz: range, - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + }`, + ); + const { stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--network-concurrency", "abcdef"], + cwd: ctx.package_dir, + stdout: "inherit", + stdin: "inherit", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Expected --network-concurrency to be a number between 0 and 65535"); + expect(await exited).toBe(1); + expect(urls).toBeEmpty(); + }); }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ baz@${chosen}`, - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/baz`, `${root_url}/baz-${chosen}.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"))).toHaveBins([exeName]); - expect(join(package_dir, "node_modules", ".bin", exeName)).toBeValidBin(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: chosen, - bin: { - [exeName]: "index.js", - }, - } as any); - await access(join(package_dir, "bun.lockb")); } - describe("highest matching version", () => { - for (let latest of ["999.999.999", "0.0.4", "0.0.2"]) { - for (let range of ["0.0.x", "~0.0.4", "~0.0.2"]) { - it("when latest is " + latest + " and range is " + range, async () => { - await runTest(latest, range); + it("bun install --network-concurrency=5 doesnt go over 5 concurrent requests", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + let maxConcurrentRequests = 0; + let concurrentRequestCounter = 0; + let totalRequests = 0; + setContextHandler(ctx, async function (request) { + concurrentRequestCounter++; + totalRequests++; + try { + await Bun.sleep(10); + maxConcurrentRequests = Math.max(maxConcurrentRequests, concurrentRequestCounter); + + if (concurrentRequestCounter > 20) { + throw new Error("Too many concurrent requests"); + } + } finally { + concurrentRequestCounter--; + } + + return new Response("404", { status: 404 }); + }); + await writeFile( + join(ctx.package_dir, "package.json"), + ` + { + "name": "foo", + "version": "0.0.1", + "dependencies": { + "bar1": "^1", + "bar2": "^1", + "bar3": "^1", + "bar4": "^1", + "bar5": "^1", + "bar6": "^1", + "bar7": "^1", + "bar8": "^1", + "bar9": "^1", + "bar10": "^1", + "bar11": "^1", + "bar12": "^1", + "bar13": "^1", + "bar14": "^1", + "bar15": "^1", + "bar16": "^1", + "bar17": "^1", + "bar18": "^1", + "bar19": "^1", + "bar20": "^1", + "bar21": "^1", + "bar22": "^1", + "bar23": "^1", + "bar24": "^1", + "bar25": "^1", + "bar26": "^1", + "bar27": "^1", + "bar28": "^1", + "bar29": "^1", + "bar30": "^1", + "bar31": "^1", + "bar32": "^1", + "bar33": "^1", + "bar34": "^1", + "bar35": "^1", + "bar36": "^1", + "bar37": "^1", + "bar38": "^1", + "bar39": "^1", + "bar40": "^1", + "bar41": "^1", + "bar42": "^1", + "bar43": "^1", + "bar44": "^1", + "bar45": "^1", + "bar46": "^1", + "bar47": "^1", + "bar48": "^1", + "bar49": "^1", + "bar50": "^1", + "bar51": "^1", + } + }`, + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--network-concurrency", "5"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(await exited).toBe(1); + expect(urls).toBeEmpty(); + expect(maxConcurrentRequests).toBeLessThanOrEqual(5); + expect(totalRequests).toBe(51); + + expect(err).toContain("failed to resolve"); + expect(await stdout.text()).toEqual(expect.stringContaining("bun install v1.")); + }); + }); + + it("should not error when package.json has comments and trailing commas", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + ` + { + "name": "foo", + "version": "0.0.1", + "dependencies": { + "bar": "^1", + }, + } + `, + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain('error: No version matching "^1" found for specifier "bar" (but package exists)'); + expect(await stdout.text()).toEqual(expect.stringContaining("bun install v1.")); + expect(await exited).toBe(1); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`]); + expect(ctx.requested).toBe(1); + try { + await access(join(ctx.package_dir, "bun.lockb")); + expect.unreachable(); + } catch (err: any) { + expect(err.code).toBe("ENOENT"); + } + }); + }); + + describe("chooses", () => { + async function runTest(ctx: TestContext, latest: string, range: string, chosen = "0.0.5") { + const exeName: string = { + "0.0.5": "baz-exec", + "0.0.3": "baz-run", + }[chosen]!; + if (!exeName) throw new Error("exeName not found"); + + const urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { + "0.0.5": { + bin: { + "baz-exec": "index.js", + }, + }, + + "0.0.3": { + bin: { + "baz-run": "index.js", + }, + }, + latest, + }), + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + baz: range, + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ baz@${chosen}`, + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}baz`, `${ctx.registry_url}baz-${chosen}.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "baz"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins([exeName]); + expect(join(ctx.package_dir, "node_modules", ".bin", exeName)).toBeValidBin(join("..", "baz", "index.js")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: chosen, + bin: { + [exeName]: "index.js", + }, + } as any); + await access(join(ctx.package_dir, "bun.lockb")); + } + + describe("highest matching version", () => { + for (let latest of ["999.999.999", "0.0.4", "0.0.2"]) { + for (let range of ["0.0.x", "~0.0.4", "~0.0.2"]) { + it("when latest is " + latest + " and range is " + range, async () => { + await withContext(defaultOpts, async ctx => { + await runTest(ctx, latest, range); + }); + }); + } + } + }); + + describe('"latest" tag', () => { + for (let latest of ["0.0.5", "0.0.3"]) { + it(latest, async () => { + await withContext(defaultOpts, async ctx => { + await runTest(ctx, latest, "~0.0.3", latest); + }); }); } - } + }); }); - describe('"latest" tag', () => { - for (let latest of ["0.0.5", "0.0.3"]) { - it(latest, async () => { - await runTest(latest, "~0.0.3", latest); + it("should report connection errors", async () => { + await withContext(defaultOpts, async ctx => { + function end(socket: Socket) { + socket.end(); + } + const server = listen({ + socket: { + data: function data(socket) { + socket.end(); + }, + drain: function drain(socket) { + socket.end(); + }, + open: function open(socket) { + socket.end(); + }, + }, + hostname: "localhost", + port: 0, }); - } - }); -}); - -it("should report connection errors", async () => { - function end(socket: Socket) { - socket.end(); - } - const server = listen({ - socket: { - data: function data(socket) { - socket.end(); - }, - drain: function drain(socket) { - socket.end(); - }, - open: function open(socket) { - socket.end(); - }, - }, - hostname: "localhost", - port: 0, - }); - await writeFile( - join(package_dir, "bunfig.toml"), - ` -[install] -cache = false -registry = "http://${server.hostname}:${server.port}/" -`, - ); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - bar: "0.0.2", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toMatch(/error: (ConnectionRefused|ConnectionClosed) downloading package manifest bar/gm); - expect(await stdout.text()).toEqual(expect.stringContaining("bun install v1.")); - expect(await exited).toBe(1); - try { - await access(join(package_dir, "bun.lockb")); - expect.unreachable(); - } catch (err: any) { - expect(err.code).toBe("ENOENT"); - } -}); - -it("should support --registry CLI flag", async () => { - const connected = jest.fn(); - function end(socket: Socket) { - connected(); - socket.end(); - } - const server = listen({ - socket: { - data: function data(socket) { - end(socket); - }, - drain: function drain(socket) { - end(socket); - }, - open: function open(socket) { - end(socket); - }, - }, - hostname: "localhost", - port: 0, - }); - await writeFile( - join(package_dir, "bunfig.toml"), - ` -[install] -cache = false -registry = "https://badssl.com:bad" -`, - ); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - bar: "0.0.2", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--registry", `http://${server.hostname}:${server.port}/`], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toMatch(/error: (ConnectionRefused|ConnectionClosed) downloading package manifest bar/gm); - expect(await stdout.text()).toEqual(expect.stringContaining("bun install v1.")); - expect(await exited).toBe(1); - try { - await access(join(package_dir, "bun.lockb")); - expect.unreachable(); - } catch (err: any) { - expect(err.code).toBe("ENOENT"); - } - expect(connected).toHaveBeenCalled(); -}); - -it("should work when moving workspace packages", async () => { - const package_dir = tempDirWithFiles("lol", { - "package.json": JSON.stringify({ - "name": "my-workspace", - private: "true", - version: "0.0.1", - "devDependencies": { - "@repo/ui": "*", - "@repo/eslint-config": "*", - "@repo/typescript-config": "*", - }, - workspaces: ["packages/*"], - }), - packages: { - "eslint-config": { - "package.json": JSON.stringify({ - name: "@repo/eslint-config", - "version": "0.0.0", - private: "true", - }), - }, - "typescript-config": { - "package.json": JSON.stringify({ - "name": "@repo/typescript-config", - "version": "0.0.0", - private: "true", - }), - }, - "ui": { - "package.json": JSON.stringify({ - name: "@repo/ui", - version: "0.0.0", - private: "true", - devDependencies: { - "@repo/eslint-config": "*", - "@repo/typescript-config": "*", + await writeFile( + join(ctx.package_dir, "bunfig.toml"), + ` + [install] + cache = false + registry = "http://${server.hostname}:${server.port}/" + `, + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + bar: "0.0.2", }, }), - }, - }, + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toMatch(/error: (ConnectionRefused|ConnectionClosed) downloading package manifest bar/gm); + expect(await stdout.text()).toEqual(expect.stringContaining("bun install v1.")); + expect(await exited).toBe(1); + try { + await access(join(ctx.package_dir, "bun.lockb")); + expect.unreachable(); + } catch (err: any) { + expect(err.code).toBe("ENOENT"); + } + }); }); - await Bun.$`${bunExe()} i`.env(bunEnv).cwd(package_dir); + it("should support --registry CLI flag", async () => { + await withContext(defaultOpts, async ctx => { + const connected = jest.fn(); + function end(socket: Socket) { + connected(); + socket.end(); + } + const server = listen({ + socket: { + data: function data(socket) { + end(socket); + }, + drain: function drain(socket) { + end(socket); + }, + open: function open(socket) { + end(socket); + }, + }, + hostname: "localhost", + port: 0, + }); + await writeFile( + join(ctx.package_dir, "bunfig.toml"), + ` + [install] + cache = false + registry = "https://badssl.com:bad" + `, + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + bar: "0.0.2", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--registry", `http://${server.hostname}:${server.port}/`], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toMatch(/error: (ConnectionRefused|ConnectionClosed) downloading package manifest bar/gm); + expect(await stdout.text()).toEqual(expect.stringContaining("bun install v1.")); + expect(await exited).toBe(1); + try { + await access(join(ctx.package_dir, "bun.lockb")); + expect.unreachable(); + } catch (err: any) { + expect(err.code).toBe("ENOENT"); + } + expect(connected).toHaveBeenCalled(); + }); + }); - await Bun.$ /* sh */ ` + it("should work when moving workspace packages", async () => { + const package_dir = tempDirWithFiles("lol", { + "package.json": JSON.stringify({ + "name": "my-workspace", + private: "true", + version: "0.0.1", + "devDependencies": { + "@repo/ui": "*", + "@repo/eslint-config": "*", + "@repo/typescript-config": "*", + }, + workspaces: ["packages/*"], + }), + packages: { + "eslint-config": { + "package.json": JSON.stringify({ + name: "@repo/eslint-config", + "version": "0.0.0", + private: "true", + }), + }, + "typescript-config": { + "package.json": JSON.stringify({ + "name": "@repo/typescript-config", + "version": "0.0.0", + private: "true", + }), + }, + "ui": { + "package.json": JSON.stringify({ + name: "@repo/ui", + version: "0.0.0", + private: "true", + devDependencies: { + "@repo/eslint-config": "*", + "@repo/typescript-config": "*", + }, + }), + }, + }, + }); + + await Bun.$`${bunExe()} i`.env(bunEnv).cwd(package_dir); + + await Bun.$ /* sh */ ` mkdir config # change workspaces from "packages/*" to "config/*" @@ -511,57 +525,57 @@ it("should work when moving workspace packages", async () => { rm -rf packages rm -rf apps ` - .env(bunEnv) - .cwd(package_dir); + .env(bunEnv) + .cwd(package_dir); - await Bun.$`${bunExe()} i`.env(bunEnv).cwd(package_dir); -}); - -it("should work when renaming a single workspace package", async () => { - const package_dir = tempDirWithFiles("lol", { - "package.json": JSON.stringify({ - "name": "my-workspace", - private: "true", - version: "0.0.1", - "devDependencies": { - "@repo/ui": "*", - "@repo/eslint-config": "*", - "@repo/typescript-config": "*", - }, - workspaces: ["packages/*"], - }), - packages: { - "eslint-config": { - "package.json": JSON.stringify({ - name: "@repo/eslint-config", - "version": "0.0.0", - private: "true", - }), - }, - "typescript-config": { - "package.json": JSON.stringify({ - "name": "@repo/typescript-config", - "version": "0.0.0", - private: "true", - }), - }, - "ui": { - "package.json": JSON.stringify({ - name: "@repo/ui", - version: "0.0.0", - private: "true", - devDependencies: { - "@repo/eslint-config": "*", - "@repo/typescript-config": "*", - }, - }), - }, - }, + await Bun.$`${bunExe()} i`.env(bunEnv).cwd(package_dir); }); - await Bun.$`${bunExe()} i`.env(bunEnv).cwd(package_dir); + it("should work when renaming a single workspace package", async () => { + const package_dir = tempDirWithFiles("lol", { + "package.json": JSON.stringify({ + "name": "my-workspace", + private: "true", + version: "0.0.1", + "devDependencies": { + "@repo/ui": "*", + "@repo/eslint-config": "*", + "@repo/typescript-config": "*", + }, + workspaces: ["packages/*"], + }), + packages: { + "eslint-config": { + "package.json": JSON.stringify({ + name: "@repo/eslint-config", + "version": "0.0.0", + private: "true", + }), + }, + "typescript-config": { + "package.json": JSON.stringify({ + "name": "@repo/typescript-config", + "version": "0.0.0", + private: "true", + }), + }, + "ui": { + "package.json": JSON.stringify({ + name: "@repo/ui", + version: "0.0.0", + private: "true", + devDependencies: { + "@repo/eslint-config": "*", + "@repo/typescript-config": "*", + }, + }), + }, + }, + }); - await Bun.$ /* sh */ ` + await Bun.$`${bunExe()} i`.env(bunEnv).cwd(package_dir); + + await Bun.$ /* sh */ ` echo ${JSON.stringify({ "name": "my-workspace", version: "0.0.1", @@ -589,7633 +603,8089 @@ it("should work when renaming a single workspace package", async () => { }, })} > packages/ui/package.json ` - .env(bunEnv) - .cwd(package_dir); + .env(bunEnv) + .cwd(package_dir); - await Bun.$`${bunExe()} i`.env(bunEnv).cwd(package_dir); -}); + await Bun.$`${bunExe()} i`.env(bunEnv).cwd(package_dir); + }); -it("should handle missing package", async () => { - const urls: string[] = []; - 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, */*", - ); - expect(request.headers.get("npm-auth-type")).toBe(null); - expect(await request.text()).toBeEmpty(); - urls.push(request.url); - return new Response("bar", { status: 404 }); + it("should handle missing package", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, 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, */*", + ); + expect(request.headers.get("npm-auth-type")).toBe(null); + expect(await request.text()).toBeEmpty(); + urls.push(request.url); + return new Response("bar", { status: 404 }); + }); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "foo"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err.split(/\r?\n/)).toContain(`error: GET ${ctx.registry_url}foo - 404`); + expect(await stdout.text()).toEqual(expect.stringContaining("bun add v1.")); + expect(await exited).toBe(1); + expect(urls.sort()).toEqual([`${ctx.registry_url}foo`]); + expect(ctx.requested).toBe(1); + try { + await access(join(ctx.package_dir, "bun.lockb")); + expect.unreachable(); + } catch (err: any) { + expect(err.code).toBe("ENOENT"); + } + }); }); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "foo"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err.split(/\r?\n/)).toContain(`error: GET http://localhost:${new URL(root_url).port}/foo - 404`); - expect(await stdout.text()).toEqual(expect.stringContaining("bun add v1.")); - expect(await exited).toBe(1); - expect(urls.sort()).toEqual([`${root_url}/foo`]); - expect(requested).toBe(1); - try { - await access(join(package_dir, "bun.lockb")); - expect.unreachable(); - } catch (err: any) { - expect(err.code).toBe("ENOENT"); - } -}); -it("should handle @scoped authentication", async () => { - let seen_token = false; - const url = `${root_url}/@foo%2fbar`; - const urls: string[] = []; - 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, */*", - ); - if (request.url === url) { - expect(request.headers.get("authorization")).toBe("Bearer bar"); - expect(request.headers.get("npm-auth-type")).toBe("legacy"); - seen_token = true; - } else { - expect(request.headers.get("npm-auth-type")).toBe(null); - } - expect(await request.text()).toBeEmpty(); - urls.push(request.url); - return new Response("Feeling lucky?", { status: 422 }); + it("should handle @scoped authentication", async () => { + await withContext(defaultOpts, async ctx => { + let seen_token = false; + const url = `${ctx.registry_url}@foo%2fbar`; + const urls: string[] = []; + setContextHandler(ctx, 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, */*", + ); + if (request.url === url) { + expect(request.headers.get("authorization")).toBe("Bearer bar"); + expect(request.headers.get("npm-auth-type")).toBe("legacy"); + seen_token = true; + } else { + expect(request.headers.get("npm-auth-type")).toBe(null); + } + expect(await request.text()).toBeEmpty(); + urls.push(request.url); + return new Response("Feeling lucky?", { status: 422 }); + }); + // workaround against `writeFile(..., { flag: "a" })` + await writeFile( + join(ctx.package_dir, "bunfig.toml"), + `${await file(join(ctx.package_dir, "bunfig.toml")).text()} + [install.scopes] + foo = { token = "bar" } + `, + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "@foo/bar"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err.split(/\r?\n/)).toContain(`error: GET ${url} - 422`); + expect(await stdout.text()).toEqual(expect.stringContaining("bun add v1.")); + expect(await exited).toBe(1); + expect(urls.sort()).toEqual([url]); + expect(seen_token).toBe(true); + expect(ctx.requested).toBe(1); + try { + await access(join(ctx.package_dir, "bun.lockb")); + expect.unreachable(); + } catch (err: any) { + expect(err.code).toBe("ENOENT"); + } + }); }); - // workaround against `writeFile(..., { flag: "a" })` - await writeFile( - join(package_dir, "bunfig.toml"), - `${await file(join(package_dir, "bunfig.toml")).text()} -[install.scopes] -foo = { token = "bar" } -`, - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "@foo/bar"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err.split(/\r?\n/)).toContain(`error: GET ${url} - 422`); - expect(await stdout.text()).toEqual(expect.stringContaining("bun add v1.")); - expect(await exited).toBe(1); - expect(urls.sort()).toEqual([url]); - expect(seen_token).toBe(true); - expect(requested).toBe(1); - try { - await access(join(package_dir, "bun.lockb")); - expect.unreachable(); - } catch (err: any) { - expect(err.code).toBe("ENOENT"); - } -}); -it("should handle empty string in dependencies", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - bar: "", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should handle empty string in dependencies", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + bar: "", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@0.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`, `${ctx.registry_url}bar-0.0.2.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@0.0.2", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - await access(join(package_dir, "bun.lockb")); -}); -it("should handle workspaces", async () => { - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - workspaces: ["bar", "packages/*"], - }), - ); - await mkdir(join(package_dir, "bar")); - await writeFile( - join(package_dir, "bar", "package.json"), - JSON.stringify({ - name: "Bar", - version: "0.0.2", - }), - ); + it("should handle workspaces", async () => { + await withContext(defaultOpts, async ctx => { + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + workspaces: ["bar", "packages/*"], + }), + ); + await mkdir(join(ctx.package_dir, "bar")); + await writeFile( + join(ctx.package_dir, "bar", "package.json"), + JSON.stringify({ + name: "Bar", + version: "0.0.2", + }), + ); - await mkdir(join(package_dir, "packages", "nominally-scoped"), { recursive: true }); - await writeFile( - join(package_dir, "packages", "nominally-scoped", "package.json"), - JSON.stringify({ - name: "@org/nominally-scoped", - version: "0.1.4", - }), - ); + await mkdir(join(ctx.package_dir, "packages", "nominally-scoped"), { recursive: true }); + await writeFile( + join(ctx.package_dir, "packages", "nominally-scoped", "package.json"), + JSON.stringify({ + name: "@org/nominally-scoped", + version: "0.1.4", + }), + ); - await mkdir(join(package_dir, "packages", "second-asterisk"), { recursive: true }); - await writeFile( - join(package_dir, "packages", "second-asterisk", "package.json"), - JSON.stringify({ - name: "AsteriskTheSecond", - version: "0.1.4", - }), - ); + await mkdir(join(ctx.package_dir, "packages", "second-asterisk"), { recursive: true }); + await writeFile( + join(ctx.package_dir, "packages", "second-asterisk", "package.json"), + JSON.stringify({ + name: "AsteriskTheSecond", + version: "0.1.4", + }), + ); - await mkdir(join(package_dir, "packages", "asterisk"), { recursive: true }); - await writeFile( - join(package_dir, "packages", "asterisk", "package.json"), - JSON.stringify({ - name: "Asterisk", - version: "0.0.4", - }), - ); + await mkdir(join(ctx.package_dir, "packages", "asterisk"), { recursive: true }); + await writeFile( + join(ctx.package_dir, "packages", "asterisk", "package.json"), + JSON.stringify({ + name: "Asterisk", + version: "0.0.4", + }), + ); - const { - stdout: stdout1, - stderr: stderr1, - exited: exited1, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err1 = await new Response(stderr1).text(); - expect(err1).toContain("Saved lockfile"); - const out1 = await new Response(stdout1).text(); - expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "4 packages installed", - ]); - expect(await exited1).toBe(0); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ - ".cache", - "@org", - "Asterisk", - "AsteriskTheSecond", - "Bar", - ]); - expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); - expect(package_dir).toHaveWorkspaceLink(["Asterisk", "packages/asterisk"]); - expect(package_dir).toHaveWorkspaceLink(["AsteriskTheSecond", "packages/second-asterisk"]); - // prettier-ignore - expect(package_dir).toHaveWorkspaceLink2(["@org/nominally-scoped", "../packages/nominally-scoped", "packages/nominally-scoped"]); - await access(join(package_dir, "bun.lockb")); + const { + stdout: stdout1, + stderr: stderr1, + exited: exited1, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err1 = await new Response(stderr1).text(); + expect(err1).toContain("Saved lockfile"); + const out1 = await new Response(stdout1).text(); + expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "4 packages installed", + ]); + expect(await exited1).toBe(0); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([ + ".cache", + "@org", + "Asterisk", + "AsteriskTheSecond", + "Bar", + ]); + expect(ctx.package_dir).toHaveWorkspaceLink(["Bar", "bar"]); + expect(ctx.package_dir).toHaveWorkspaceLink(["Asterisk", "packages/asterisk"]); + expect(ctx.package_dir).toHaveWorkspaceLink(["AsteriskTheSecond", "packages/second-asterisk"]); + // prettier-ignore + expect(ctx.package_dir).toHaveWorkspaceLink2(["@org/nominally-scoped", "../packages/nominally-scoped", "packages/nominally-scoped"]); + await access(join(ctx.package_dir, "bun.lockb")); - // Perform `bun install` again but with lockfile from before - await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); - const { - stdout: stdout2, - stderr: stderr2, - exited: exited2, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + // Perform `bun install` again but with lockfile from before + await rm(join(ctx.package_dir, "node_modules"), { force: true, recursive: true }); + const { + stdout: stdout2, + stderr: stderr2, + exited: exited2, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err2 = await new Response(stderr2).text(); + expect(err2).not.toContain("Saved lockfile"); + const out2 = await new Response(stdout2).text(); + expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "4 packages installed", + ]); + expect(await exited2).toBe(0); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([ + "@org", + "Asterisk", + "AsteriskTheSecond", + "Bar", + ]); + expect(ctx.package_dir).toHaveWorkspaceLink(["Bar", "bar"]); + expect(ctx.package_dir).toHaveWorkspaceLink(["Asterisk", "packages/asterisk"]); + expect(ctx.package_dir).toHaveWorkspaceLink(["AsteriskTheSecond", "packages/second-asterisk"]); + // prettier-ignore + expect(ctx.package_dir).toHaveWorkspaceLink2(["@org/nominally-scoped", "../packages/nominally-scoped", "packages/nominally-scoped"]); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - const err2 = await new Response(stderr2).text(); - expect(err2).not.toContain("Saved lockfile"); - const out2 = await new Response(stdout2).text(); - expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "4 packages installed", - ]); - expect(await exited2).toBe(0); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ - "@org", - "Asterisk", - "AsteriskTheSecond", - "Bar", - ]); - expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); - expect(package_dir).toHaveWorkspaceLink(["Asterisk", "packages/asterisk"]); - expect(package_dir).toHaveWorkspaceLink(["AsteriskTheSecond", "packages/second-asterisk"]); - // prettier-ignore - expect(package_dir).toHaveWorkspaceLink2(["@org/nominally-scoped", "../packages/nominally-scoped", "packages/nominally-scoped"]); - await access(join(package_dir, "bun.lockb")); -}); -it("should handle `workspace:` specifier", async () => { - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - dependencies: { - Bar: "workspace:path/to/bar", - }, - }), - ); - await mkdir(join(package_dir, "path", "to", "bar"), { recursive: true }); - await writeFile( - join(package_dir, "path", "to", "bar", "package.json"), - JSON.stringify({ - name: "Bar", - version: "0.0.2", - }), - ); - const { - stdout: stdout1, - stderr: stderr1, - exited: exited1, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should handle `workspace:` specifier", async () => { + await withContext(defaultOpts, async ctx => { + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + Bar: "workspace:path/to/bar", + }, + }), + ); + await mkdir(join(ctx.package_dir, "path", "to", "bar"), { recursive: true }); + await writeFile( + join(ctx.package_dir, "path", "to", "bar", "package.json"), + JSON.stringify({ + name: "Bar", + version: "0.0.2", + }), + ); + const { + stdout: stdout1, + stderr: stderr1, + exited: exited1, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err1 = await new Response(stderr1).text(); + expect(err1).toContain("Saved lockfile"); + const out1 = await new Response(stdout1).text(); + expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ Bar@workspace:path/to/bar`, + "", + "1 package installed", + ]); + expect(await exited1).toBe(0); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "Bar"]); + expect(ctx.package_dir).toHaveWorkspaceLink(["Bar", "path/to/bar"]); + await access(join(ctx.package_dir, "bun.lockb")); + // Perform `bun install` again but with lockfile from before + await rm(join(ctx.package_dir, "node_modules"), { force: true, recursive: true }); + const { + stdout: stdout2, + stderr: stderr2, + exited: exited2, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err2 = await new Response(stderr2).text(); + expect(err2).not.toContain("Saved lockfile"); + const out2 = await new Response(stdout2).text(); + expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ Bar@workspace:path/to/bar`, + "", + "1 package installed", + ]); + expect(await exited2).toBe(0); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual(["Bar"]); + expect(ctx.package_dir).toHaveWorkspaceLink(["Bar", "path/to/bar"]); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - const err1 = await new Response(stderr1).text(); - expect(err1).toContain("Saved lockfile"); - const out1 = await new Response(stdout1).text(); - expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ Bar@workspace:path/to/bar`, - "", - "1 package installed", - ]); - expect(await exited1).toBe(0); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "Bar"]); - expect(package_dir).toHaveWorkspaceLink(["Bar", "path/to/bar"]); - await access(join(package_dir, "bun.lockb")); - // Perform `bun install` again but with lockfile from before - await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); - const { - stdout: stdout2, - stderr: stderr2, - exited: exited2, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err2 = await new Response(stderr2).text(); - expect(err2).not.toContain("Saved lockfile"); - const out2 = await new Response(stdout2).text(); - expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ Bar@workspace:path/to/bar`, - "", - "1 package installed", - ]); - expect(await exited2).toBe(0); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual(["Bar"]); - expect(package_dir).toHaveWorkspaceLink(["Bar", "path/to/bar"]); - await access(join(package_dir, "bun.lockb")); -}); -it("should handle workspaces with packages array", async () => { - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - workspaces: { packages: ["bar"] }, - }), - ); - await mkdir(join(package_dir, "bar")); - await writeFile( - join(package_dir, "bar", "package.json"), - JSON.stringify({ - name: "Bar", - version: "0.0.2", - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); + it("should handle workspaces with packages array", async () => { + await withContext(defaultOpts, async ctx => { + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + workspaces: { packages: ["bar"] }, + }), + ); + await mkdir(join(ctx.package_dir, "bar")); + await writeFile( + join(ctx.package_dir, "bar", "package.json"), + JSON.stringify({ + name: "Bar", + version: "0.0.2", + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); + const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "Bar"]); - expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); - await access(join(package_dir, "bun.lockb")); -}); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "Bar"]); + expect(ctx.package_dir).toHaveWorkspaceLink(["Bar", "bar"]); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); -it("should handle inter-dependency between workspaces", async () => { - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - workspaces: ["bar", "packages/baz"], - }), - ); - await mkdir(join(package_dir, "bar")); - await writeFile( - join(package_dir, "bar", "package.json"), - JSON.stringify({ - name: "Bar", - version: "0.0.2", - dependencies: { - Baz: "0.0.3", - }, - }), - ); - await mkdir(join(package_dir, "packages", "baz"), { recursive: true }); - await writeFile( - join(package_dir, "packages", "baz", "package.json"), - JSON.stringify({ - name: "Baz", - version: "0.0.3", - dependencies: { - Bar: "0.0.2", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should handle inter-dependency between workspaces", async () => { + await withContext(defaultOpts, async ctx => { + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + workspaces: ["bar", "packages/baz"], + }), + ); + await mkdir(join(ctx.package_dir, "bar")); + await writeFile( + join(ctx.package_dir, "bar", "package.json"), + JSON.stringify({ + name: "Bar", + version: "0.0.2", + dependencies: { + Baz: "0.0.3", + }, + }), + ); + await mkdir(join(ctx.package_dir, "packages", "baz"), { recursive: true }); + await writeFile( + join(ctx.package_dir, "packages", "baz", "package.json"), + JSON.stringify({ + name: "Baz", + version: "0.0.3", + dependencies: { + Bar: "0.0.2", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "Bar", "Baz"]); + expect(ctx.package_dir).toHaveWorkspaceLink(["Bar", "bar"]); + expect(ctx.package_dir).toHaveWorkspaceLink(["Baz", "packages/baz"]); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "Bar", "Baz"]); - expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); - expect(package_dir).toHaveWorkspaceLink(["Baz", "packages/baz"]); - await access(join(package_dir, "bun.lockb")); -}); -it("should handle inter-dependency between workspaces (devDependencies)", async () => { - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - workspaces: ["bar", "packages/baz"], - }), - ); - await mkdir(join(package_dir, "bar")); - await writeFile( - join(package_dir, "bar", "package.json"), - JSON.stringify({ - name: "Bar", - version: "0.0.2", - devDependencies: { - Baz: "0.0.3", - }, - }), - ); - await mkdir(join(package_dir, "packages", "baz"), { recursive: true }); - await writeFile( - join(package_dir, "packages", "baz", "package.json"), - JSON.stringify({ - name: "Baz", - version: "0.0.3", - devDependencies: { - Bar: "0.0.2", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should handle inter-dependency between workspaces (devDependencies)", async () => { + await withContext(defaultOpts, async ctx => { + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + workspaces: ["bar", "packages/baz"], + }), + ); + await mkdir(join(ctx.package_dir, "bar")); + await writeFile( + join(ctx.package_dir, "bar", "package.json"), + JSON.stringify({ + name: "Bar", + version: "0.0.2", + devDependencies: { + Baz: "0.0.3", + }, + }), + ); + await mkdir(join(ctx.package_dir, "packages", "baz"), { recursive: true }); + await writeFile( + join(ctx.package_dir, "packages", "baz", "package.json"), + JSON.stringify({ + name: "Baz", + version: "0.0.3", + devDependencies: { + Bar: "0.0.2", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "Bar", "Baz"]); + expect(ctx.package_dir).toHaveWorkspaceLink(["Bar", "bar"]); + expect(ctx.package_dir).toHaveWorkspaceLink(["Baz", "packages/baz"]); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "Bar", "Baz"]); - expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); - expect(package_dir).toHaveWorkspaceLink(["Baz", "packages/baz"]); - await access(join(package_dir, "bun.lockb")); -}); -it("should handle inter-dependency between workspaces (optionalDependencies)", async () => { - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - workspaces: ["bar", "packages/baz"], - }), - ); - await mkdir(join(package_dir, "bar")); - await writeFile( - join(package_dir, "bar", "package.json"), - JSON.stringify({ - name: "Bar", - version: "0.0.2", - optionalDependencies: { - Baz: "0.0.3", - }, - }), - ); - await mkdir(join(package_dir, "packages", "baz"), { recursive: true }); - await writeFile( - join(package_dir, "packages", "baz", "package.json"), - JSON.stringify({ - name: "Baz", - version: "0.0.3", - optionalDependencies: { - Bar: "0.0.2", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should handle inter-dependency between workspaces (optionalDependencies)", async () => { + await withContext(defaultOpts, async ctx => { + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + workspaces: ["bar", "packages/baz"], + }), + ); + await mkdir(join(ctx.package_dir, "bar")); + await writeFile( + join(ctx.package_dir, "bar", "package.json"), + JSON.stringify({ + name: "Bar", + version: "0.0.2", + optionalDependencies: { + Baz: "0.0.3", + }, + }), + ); + await mkdir(join(ctx.package_dir, "packages", "baz"), { recursive: true }); + await writeFile( + join(ctx.package_dir, "packages", "baz", "package.json"), + JSON.stringify({ + name: "Baz", + version: "0.0.3", + optionalDependencies: { + Bar: "0.0.2", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "Bar", "Baz"]); + expect(ctx.package_dir).toHaveWorkspaceLink(["Bar", "bar"]); + expect(ctx.package_dir).toHaveWorkspaceLink(["Baz", "packages/baz"]); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "Bar", "Baz"]); - expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); - expect(package_dir).toHaveWorkspaceLink(["Baz", "packages/baz"]); - await access(join(package_dir, "bun.lockb")); -}); -it("should handle installing the same peerDependency with different versions", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - peerDependencies: { - peer: "0.0.2", - }, - dependencies: { - boba: "0.0.2", - }, - }), - ); + it("should handle installing the same peerDependency with different versions", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + peerDependencies: { + peer: "0.0.2", + }, + dependencies: { + boba: "0.0.2", + }, + }), + ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(ctx.requested).toBe(0); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ boba@0.0.2", + "+ peer@0.0.2", + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + }); }); - expect(requested).toBe(0); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ boba@0.0.2", - "+ peer@0.0.2", - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); -}); -it("should handle installing the same peerDependency with the same version", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - peerDependencies: { - peer: "0.0.1", - }, - dependencies: { - boba: "0.0.2", - }, - }), - ); + it("should handle installing the same peerDependency with the same version", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + peerDependencies: { + peer: "0.0.1", + }, + dependencies: { + boba: "0.0.2", + }, + }), + ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - expect(requested).toBe(0); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ boba@0.0.2", - "", - "1 package installed", - ]); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(ctx.requested).toBe(0); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ boba@0.0.2", + "", + "1 package installed", + ]); - expect(await exited).toBe(0); -}); + expect(await exited).toBe(0); + }); + }); -it("should handle life-cycle scripts within workspaces", async () => { - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - scripts: { - install: [bunExe(), "install.js"].join(" "), - }, - workspaces: ["bar"], - }), - ); - await writeFile(join(package_dir, "install.js"), 'await require("fs/promises").writeFile("foo.txt", "foo!");'); - await mkdir(join(package_dir, "bar")); - await writeFile( - join(package_dir, "bar", "package.json"), - JSON.stringify({ - name: "Bar", - version: "0.0.2", - scripts: { - preinstall: [bunExe(), "preinstall.js"].join(" "), - }, - }), - ); - await writeFile( - join(package_dir, "bar", "preinstall.js"), - 'await require("fs/promises").writeFile("bar.txt", "bar!");', - ); - const { stdout, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should handle life-cycle scripts within workspaces", async () => { + await withContext(defaultOpts, async ctx => { + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + scripts: { + install: [bunExe(), "install.js"].join(" "), + }, + workspaces: ["bar"], + }), + ); + await writeFile( + join(ctx.package_dir, "install.js"), + 'await require("fs/promises").writeFile("foo.txt", "foo!");', + ); + await mkdir(join(ctx.package_dir, "bar")); + await writeFile( + join(ctx.package_dir, "bar", "package.json"), + JSON.stringify({ + name: "Bar", + version: "0.0.2", + scripts: { + preinstall: [bunExe(), "preinstall.js"].join(" "), + }, + }), + ); + await writeFile( + join(ctx.package_dir, "bar", "preinstall.js"), + 'await require("fs/promises").writeFile("bar.txt", "bar!");', + ); + const { stdout, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "Bar"]); + expect(ctx.package_dir).toHaveWorkspaceLink(["Bar", "bar"]); + expect(await file(join(ctx.package_dir, "foo.txt")).text()).toBe("foo!"); + expect(await file(join(ctx.package_dir, "bar", "bar.txt")).text()).toBe("bar!"); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "Bar"]); - expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); - expect(await file(join(package_dir, "foo.txt")).text()).toBe("foo!"); - expect(await file(join(package_dir, "bar", "bar.txt")).text()).toBe("bar!"); - await access(join(package_dir, "bun.lockb")); -}); -it("should handle life-cycle scripts during re-installation", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - scripts: { - install: [bunExe(), "foo-install.js"].join(" "), - }, - dependencies: { - qux: "^0.0", - }, - trustedDependencies: ["qux"], - workspaces: ["bar"], - }), - ); - await writeFile(join(package_dir, "foo-install.js"), 'await require("fs/promises").writeFile("foo.txt", "foo!");'); - await mkdir(join(package_dir, "bar")); - await writeFile( - join(package_dir, "bar", "package.json"), - JSON.stringify({ - name: "Bar", - version: "0.0.2", - scripts: { - preinstall: [bunExe(), "bar-preinstall.js"].join(" "), - }, - }), - ); - await writeFile( - join(package_dir, "bar", "bar-preinstall.js"), - 'await require("fs/promises").writeFile("bar.txt", "bar!");', - ); - const { - stdout: stdout1, - stderr: stderr1, - exited: exited1, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should handle life-cycle scripts during re-installation", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + scripts: { + install: [bunExe(), "foo-install.js"].join(" "), + }, + dependencies: { + qux: "^0.0", + }, + trustedDependencies: ["qux"], + workspaces: ["bar"], + }), + ); + await writeFile( + join(ctx.package_dir, "foo-install.js"), + 'await require("fs/promises").writeFile("foo.txt", "foo!");', + ); + await mkdir(join(ctx.package_dir, "bar")); + await writeFile( + join(ctx.package_dir, "bar", "package.json"), + JSON.stringify({ + name: "Bar", + version: "0.0.2", + scripts: { + preinstall: [bunExe(), "bar-preinstall.js"].join(" "), + }, + }), + ); + await writeFile( + join(ctx.package_dir, "bar", "bar-preinstall.js"), + 'await require("fs/promises").writeFile("bar.txt", "bar!");', + ); + const { + stdout: stdout1, + stderr: stderr1, + exited: exited1, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err1 = await new Response(stderr1).text(); + expect(err1).toContain("Saved lockfile"); + const out1 = await new Response(stdout1).text(); + expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ qux@0.0.2", + "", + "2 packages installed", + ]); + expect(await exited1).toBe(0); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "Bar", "qux"]); + expect(ctx.package_dir).toHaveWorkspaceLink(["Bar", "bar"]); + expect(await file(join(ctx.package_dir, "foo.txt")).text()).toBe("foo!"); + expect(await file(join(ctx.package_dir, "bar", "bar.txt")).text()).toBe("bar!"); + await access(join(ctx.package_dir, "bun.lockb")); + // Perform `bun install` again but with lockfile from before + await rm(join(ctx.package_dir, "node_modules"), { force: true, recursive: true }); + const { + stdout: stdout2, + stderr: stderr2, + exited: exited2, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err2 = await new Response(stderr2).text(); + expect(err2).not.toContain("error:"); + expect(err2).not.toContain("Saved lockfile"); + const out2 = await new Response(stdout2).text(); + expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ qux@0.0.2", + "", + "2 packages installed", + ]); + expect(await exited2).toBe(0); + expect(ctx.requested).toBe(3); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "Bar", "qux"]); + expect(ctx.package_dir).toHaveWorkspaceLink(["Bar", "bar"]); + expect(await file(join(ctx.package_dir, "foo.txt")).text()).toBe("foo!"); + expect(await file(join(ctx.package_dir, "bar", "bar.txt")).text()).toBe("bar!"); + await access(join(ctx.package_dir, "bun.lockb")); + // Perform `bun install --production` with lockfile from before + await rm(join(ctx.package_dir, "node_modules"), { force: true, recursive: true }); + const { + stdout: stdout3, + stderr: stderr3, + exited: exited3, + } = spawn({ + cmd: [bunExe(), "install", "--production"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err3 = await new Response(stderr3).text(); + expect(err3).not.toContain("error:"); + expect(err3).not.toContain("Saved lockfile"); + const out3 = await new Response(stdout3).text(); + expect(out3.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ qux@0.0.2", + "", + "2 packages installed", + ]); + expect(await exited3).toBe(0); + expect(ctx.requested).toBe(4); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "Bar", "qux"]); + expect(ctx.package_dir).toHaveWorkspaceLink(["Bar", "bar"]); + expect(await file(join(ctx.package_dir, "foo.txt")).text()).toBe("foo!"); + expect(await file(join(ctx.package_dir, "bar", "bar.txt")).text()).toBe("bar!"); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - const err1 = await new Response(stderr1).text(); - expect(err1).toContain("Saved lockfile"); - const out1 = await new Response(stdout1).text(); - expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ qux@0.0.2", - "", - "2 packages installed", - ]); - expect(await exited1).toBe(0); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "Bar", "qux"]); - expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); - expect(await file(join(package_dir, "foo.txt")).text()).toBe("foo!"); - expect(await file(join(package_dir, "bar", "bar.txt")).text()).toBe("bar!"); - await access(join(package_dir, "bun.lockb")); - // Perform `bun install` again but with lockfile from before - await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); - const { - stdout: stdout2, - stderr: stderr2, - exited: exited2, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err2 = await new Response(stderr2).text(); - expect(err2).not.toContain("error:"); - expect(err2).not.toContain("Saved lockfile"); - const out2 = await new Response(stdout2).text(); - expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ qux@0.0.2", - "", - "2 packages installed", - ]); - expect(await exited2).toBe(0); - expect(requested).toBe(3); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "Bar", "qux"]); - expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); - expect(await file(join(package_dir, "foo.txt")).text()).toBe("foo!"); - expect(await file(join(package_dir, "bar", "bar.txt")).text()).toBe("bar!"); - await access(join(package_dir, "bun.lockb")); - // Perform `bun install --production` with lockfile from before - await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); - const { - stdout: stdout3, - stderr: stderr3, - exited: exited3, - } = spawn({ - cmd: [bunExe(), "install", "--production"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err3 = await new Response(stderr3).text(); - expect(err3).not.toContain("error:"); - expect(err3).not.toContain("Saved lockfile"); - const out3 = await new Response(stdout3).text(); - expect(out3.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ qux@0.0.2", - "", - "2 packages installed", - ]); - expect(await exited3).toBe(0); - expect(requested).toBe(4); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "Bar", "qux"]); - expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); - expect(await file(join(package_dir, "foo.txt")).text()).toBe("foo!"); - expect(await file(join(package_dir, "bar", "bar.txt")).text()).toBe("bar!"); - await access(join(package_dir, "bun.lockb")); -}); -it("should use updated life-cycle scripts in root during re-installation", async () => { - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - scripts: { - install: [bunExe(), "foo-install.js"].join(" "), - }, - workspaces: ["bar"], - }), - ); - await writeFile(join(package_dir, "foo-install.js"), 'await require("fs/promises").writeFile("foo.txt", "foo!");'); - await mkdir(join(package_dir, "bar")); - await writeFile( - join(package_dir, "bar", "package.json"), - JSON.stringify({ - name: "Bar", - scripts: { - preinstall: [bunExe(), "bar-preinstall.js"].join(" "), - }, - }), - ); - await writeFile( - join(package_dir, "bar", "bar-preinstall.js"), - 'await require("fs/promises").writeFile("bar.txt", "bar!");', - ); - const { - stdout: stdout1, - stderr: stderr1, - exited: exited1, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err1 = await new Response(stderr1).text(); - expect(err1).not.toContain("error:"); - expect(err1).toContain("Saved lockfile"); - const out1 = await new Response(stdout1).text(); - expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "1 package installed", - ]); - expect(await exited1).toBe(0); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "Bar"]); - expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); - expect(await file(join(package_dir, "foo.txt")).text()).toBe("foo!"); - expect(await file(join(package_dir, "bar", "bar.txt")).text()).toBe("bar!"); - await access(join(package_dir, "bun.lockb")); + it("should use updated life-cycle scripts in root during re-installation", async () => { + await withContext(defaultOpts, async ctx => { + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + scripts: { + install: [bunExe(), "foo-install.js"].join(" "), + }, + workspaces: ["bar"], + }), + ); + await writeFile( + join(ctx.package_dir, "foo-install.js"), + 'await require("fs/promises").writeFile("foo.txt", "foo!");', + ); + await mkdir(join(ctx.package_dir, "bar")); + await writeFile( + join(ctx.package_dir, "bar", "package.json"), + JSON.stringify({ + name: "Bar", + scripts: { + preinstall: [bunExe(), "bar-preinstall.js"].join(" "), + }, + }), + ); + await writeFile( + join(ctx.package_dir, "bar", "bar-preinstall.js"), + 'await require("fs/promises").writeFile("bar.txt", "bar!");', + ); + const { + stdout: stdout1, + stderr: stderr1, + exited: exited1, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err1 = await new Response(stderr1).text(); + expect(err1).not.toContain("error:"); + expect(err1).toContain("Saved lockfile"); + const out1 = await new Response(stdout1).text(); + expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "1 package installed", + ]); + expect(await exited1).toBe(0); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "Bar"]); + expect(ctx.package_dir).toHaveWorkspaceLink(["Bar", "bar"]); + expect(await file(join(ctx.package_dir, "foo.txt")).text()).toBe("foo!"); + expect(await file(join(ctx.package_dir, "bar", "bar.txt")).text()).toBe("bar!"); + await access(join(ctx.package_dir, "bun.lockb")); - // Perform `bun install` with outdated lockfile - await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - scripts: { - install: [bunExe(), "foo-install2.js"].join(" "), - postinstall: [bunExe(), "foo-postinstall.js"].join(" "), - }, - workspaces: ["bar"], - }), - ); - await writeFile(join(package_dir, "foo-install2.js"), 'await require("fs/promises").writeFile("foo2.txt", "foo2!");'); - await writeFile( - join(package_dir, "foo-postinstall.js"), - 'await require("fs/promises").writeFile("foo-postinstall.txt", "foo!");', - ); - const { - stdout: stdout2, - stderr: stderr2, - exited: exited2, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err2 = await new Response(stderr2).text(); - expect(err2).not.toContain("error:"); - expect(err2).toContain("Saved lockfile"); - const out2 = await new Response(stdout2).text(); - expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "1 package installed", - ]); - expect(await exited2).toBe(0); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "Bar"]); - expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); - expect(await file(join(package_dir, "foo2.txt")).text()).toBe("foo2!"); - expect(await file(join(package_dir, "bar", "bar.txt")).text()).toBe("bar!"); - expect(await file(join(package_dir, "foo-postinstall.txt")).text()).toBe("foo!"); + // Perform `bun install` with outdated lockfile + await rm(join(ctx.package_dir, "node_modules"), { force: true, recursive: true }); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + scripts: { + install: [bunExe(), "foo-install2.js"].join(" "), + postinstall: [bunExe(), "foo-postinstall.js"].join(" "), + }, + workspaces: ["bar"], + }), + ); + await writeFile( + join(ctx.package_dir, "foo-install2.js"), + 'await require("fs/promises").writeFile("foo2.txt", "foo2!");', + ); + await writeFile( + join(ctx.package_dir, "foo-postinstall.js"), + 'await require("fs/promises").writeFile("foo-postinstall.txt", "foo!");', + ); + const { + stdout: stdout2, + stderr: stderr2, + exited: exited2, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err2 = await new Response(stderr2).text(); + expect(err2).not.toContain("error:"); + expect(err2).toContain("Saved lockfile"); + const out2 = await new Response(stdout2).text(); + expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "1 package installed", + ]); + expect(await exited2).toBe(0); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "Bar"]); + expect(ctx.package_dir).toHaveWorkspaceLink(["Bar", "bar"]); + expect(await file(join(ctx.package_dir, "foo2.txt")).text()).toBe("foo2!"); + expect(await file(join(ctx.package_dir, "bar", "bar.txt")).text()).toBe("bar!"); + expect(await file(join(ctx.package_dir, "foo-postinstall.txt")).text()).toBe("foo!"); - await access(join(package_dir, "bun.lockb")); - // Perform `bun install --production` with lockfile from before - const bun_lockb = await file(join(package_dir, "bun.lockb")).arrayBuffer(); - await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); - const { - stdout: stdout3, - stderr: stderr3, - exited: exited3, - } = spawn({ - cmd: [bunExe(), "install", "--production"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err3 = await new Response(stderr3).text(); - expect(err3).not.toContain("error:"); - expect(err3).not.toContain("Saved lockfile"); + await access(join(ctx.package_dir, "bun.lockb")); + // Perform `bun install --production` with lockfile from before + const bun_lockb = await file(join(ctx.package_dir, "bun.lockb")).arrayBuffer(); + await rm(join(ctx.package_dir, "node_modules"), { force: true, recursive: true }); + const { + stdout: stdout3, + stderr: stderr3, + exited: exited3, + } = spawn({ + cmd: [bunExe(), "install", "--production"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err3 = await new Response(stderr3).text(); + expect(err3).not.toContain("error:"); + expect(err3).not.toContain("Saved lockfile"); - const out3 = await new Response(stdout3).text(); - expect(out3.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "1 package installed", - ]); - expect(await exited3).toBe(0); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "Bar"]); - expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); - expect(await file(join(package_dir, "bun.lockb")).arrayBuffer()).toEqual(bun_lockb); - expect(await file(join(package_dir, "foo2.txt")).text()).toBe("foo2!"); - expect(await file(join(package_dir, "bar", "bar.txt")).text()).toBe("bar!"); - expect(await file(join(package_dir, "foo-postinstall.txt")).text()).toBe("foo!"); -}); + const out3 = await new Response(stdout3).text(); + expect(out3.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "1 package installed", + ]); + expect(await exited3).toBe(0); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "Bar"]); + expect(ctx.package_dir).toHaveWorkspaceLink(["Bar", "bar"]); + expect(await file(join(ctx.package_dir, "bun.lockb")).arrayBuffer()).toEqual(bun_lockb); + expect(await file(join(ctx.package_dir, "foo2.txt")).text()).toBe("foo2!"); + expect(await file(join(ctx.package_dir, "bar", "bar.txt")).text()).toBe("bar!"); + expect(await file(join(ctx.package_dir, "foo-postinstall.txt")).text()).toBe("foo!"); + }); + }); -it("should use updated life-cycle scripts in dependency during re-installation", async () => { - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - scripts: { - install: [bunExe(), "foo-install.js"].join(" "), - }, - workspaces: ["bar"], - }), - ); - await writeFile(join(package_dir, "foo-install.js"), "await require('fs/promises').writeFile('foo.txt', 'foo!');"); - await mkdir(join(package_dir, "bar")); - await writeFile( - join(package_dir, "bar", "package.json"), - JSON.stringify({ - name: "Bar", - scripts: { - preinstall: [bunExe(), "bar-preinstall.js"].join(" "), - }, - }), - ); - await writeFile( - join(package_dir, "bar", "bar-preinstall.js"), - 'await require("fs/promises").writeFile("bar.txt", "bar!");', - ); - const { - stdout: stdout1, - stderr: stderr1, - exited: exited1, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err1 = await new Response(stderr1).text(); - expect(err1).not.toContain("error:"); - expect(err1).toContain("Saved lockfile"); - const out1 = await new Response(stdout1).text(); - expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "1 package installed", - ]); - expect(await exited1).toBe(0); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "Bar"]); - expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); - expect(await file(join(package_dir, "foo.txt")).text()).toBe("foo!"); - expect(await file(join(package_dir, "bar", "bar.txt")).text()).toBe("bar!"); - await access(join(package_dir, "bun.lockb")); - // Perform `bun install` with outdated lockfile - await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); - await rm(join(package_dir, "foo.txt")); - await rm(join(package_dir, "bar", "bar.txt")); - await writeFile( - join(package_dir, "bar", "package.json"), - JSON.stringify({ - name: "Bar", - scripts: { - preinstall: [bunExe(), "bar-preinstall.js"].join(" "), - postinstall: [bunExe(), "bar-postinstall.js"].join(" "), - }, - }), - ); - await writeFile( - join(package_dir, "bar", "bar-preinstall.js"), - 'await require("fs/promises").writeFile("bar-preinstall.txt", "bar preinstall!");', - ); - await writeFile( - join(package_dir, "bar", "bar-postinstall.js"), - 'await require("fs/promises").writeFile("bar-postinstall.txt", "bar postinstall!");', - ); - const { - stdout: stdout2, - stderr: stderr2, - exited: exited2, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err2 = await new Response(stderr2).text(); - expect(err2).not.toContain("error:"); - expect(err2).toContain("Saved lockfile"); - const out2 = await new Response(stdout2).text(); - expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "1 package installed", - ]); - expect(await exited2).toBe(0); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "Bar"]); - expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); - expect(await file(join(package_dir, "foo.txt")).text()).toBe("foo!"); - expect(await file(join(package_dir, "bar", "bar-preinstall.txt")).text()).toBe("bar preinstall!"); - expect(await file(join(package_dir, "bar", "bar-postinstall.txt")).text()).toBe("bar postinstall!"); - await access(join(package_dir, "bun.lockb")); + it("should use updated life-cycle scripts in dependency during re-installation", async () => { + await withContext(defaultOpts, async ctx => { + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + scripts: { + install: [bunExe(), "foo-install.js"].join(" "), + }, + workspaces: ["bar"], + }), + ); + await writeFile( + join(ctx.package_dir, "foo-install.js"), + "await require('fs/promises').writeFile('foo.txt', 'foo!');", + ); + await mkdir(join(ctx.package_dir, "bar")); + await writeFile( + join(ctx.package_dir, "bar", "package.json"), + JSON.stringify({ + name: "Bar", + scripts: { + preinstall: [bunExe(), "bar-preinstall.js"].join(" "), + }, + }), + ); + await writeFile( + join(ctx.package_dir, "bar", "bar-preinstall.js"), + 'await require("fs/promises").writeFile("bar.txt", "bar!");', + ); + const { + stdout: stdout1, + stderr: stderr1, + exited: exited1, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err1 = await new Response(stderr1).text(); + expect(err1).not.toContain("error:"); + expect(err1).toContain("Saved lockfile"); + const out1 = await new Response(stdout1).text(); + expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "1 package installed", + ]); + expect(await exited1).toBe(0); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "Bar"]); + expect(ctx.package_dir).toHaveWorkspaceLink(["Bar", "bar"]); + expect(await file(join(ctx.package_dir, "foo.txt")).text()).toBe("foo!"); + expect(await file(join(ctx.package_dir, "bar", "bar.txt")).text()).toBe("bar!"); + await access(join(ctx.package_dir, "bun.lockb")); + // Perform `bun install` with outdated lockfile + await rm(join(ctx.package_dir, "node_modules"), { force: true, recursive: true }); + await rm(join(ctx.package_dir, "foo.txt")); + await rm(join(ctx.package_dir, "bar", "bar.txt")); + await writeFile( + join(ctx.package_dir, "bar", "package.json"), + JSON.stringify({ + name: "Bar", + scripts: { + preinstall: [bunExe(), "bar-preinstall.js"].join(" "), + postinstall: [bunExe(), "bar-postinstall.js"].join(" "), + }, + }), + ); + await writeFile( + join(ctx.package_dir, "bar", "bar-preinstall.js"), + 'await require("fs/promises").writeFile("bar-preinstall.txt", "bar preinstall!");', + ); + await writeFile( + join(ctx.package_dir, "bar", "bar-postinstall.js"), + 'await require("fs/promises").writeFile("bar-postinstall.txt", "bar postinstall!");', + ); + const { + stdout: stdout2, + stderr: stderr2, + exited: exited2, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err2 = await new Response(stderr2).text(); + expect(err2).not.toContain("error:"); + expect(err2).toContain("Saved lockfile"); + const out2 = await new Response(stdout2).text(); + expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "1 package installed", + ]); + expect(await exited2).toBe(0); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "Bar"]); + expect(ctx.package_dir).toHaveWorkspaceLink(["Bar", "bar"]); + expect(await file(join(ctx.package_dir, "foo.txt")).text()).toBe("foo!"); + expect(await file(join(ctx.package_dir, "bar", "bar-preinstall.txt")).text()).toBe("bar preinstall!"); + expect(await file(join(ctx.package_dir, "bar", "bar-postinstall.txt")).text()).toBe("bar postinstall!"); + await access(join(ctx.package_dir, "bun.lockb")); - // Perform `bun install --production` with lockfile from before - const bun_lockb = await file(join(package_dir, "bun.lockb")).arrayBuffer(); - await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); - await rm(join(package_dir, "foo.txt")); - await rm(join(package_dir, "bar", "bar-preinstall.txt")); - await rm(join(package_dir, "bar", "bar-postinstall.txt")); - const { - stdout: stdout3, - stderr: stderr3, - exited: exited3, - } = spawn({ - cmd: [bunExe(), "install", "--production"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + // Perform `bun install --production` with lockfile from before + const bun_lockb = await file(join(ctx.package_dir, "bun.lockb")).arrayBuffer(); + await rm(join(ctx.package_dir, "node_modules"), { force: true, recursive: true }); + await rm(join(ctx.package_dir, "foo.txt")); + await rm(join(ctx.package_dir, "bar", "bar-preinstall.txt")); + await rm(join(ctx.package_dir, "bar", "bar-postinstall.txt")); + const { + stdout: stdout3, + stderr: stderr3, + exited: exited3, + } = spawn({ + cmd: [bunExe(), "install", "--production"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err3 = await new Response(stderr3).text(); + expect(err3).not.toContain("error:"); + expect(err3).not.toContain("Saved lockfile"); + const out3 = await new Response(stdout3).text(); + expect(out3.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "1 package installed", + ]); + expect(await exited3).toBe(0); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "Bar"]); + expect(ctx.package_dir).toHaveWorkspaceLink(["Bar", "bar"]); + expect(await file(join(ctx.package_dir, "bun.lockb")).arrayBuffer()).toEqual(bun_lockb); + expect(await file(join(ctx.package_dir, "foo.txt")).text()).toBe("foo!"); + expect(await file(join(ctx.package_dir, "bar", "bar-preinstall.txt")).text()).toBe("bar preinstall!"); + expect(await file(join(ctx.package_dir, "bar", "bar-postinstall.txt")).text()).toBe("bar postinstall!"); + }); }); - const err3 = await new Response(stderr3).text(); - expect(err3).not.toContain("error:"); - expect(err3).not.toContain("Saved lockfile"); - const out3 = await new Response(stdout3).text(); - expect(out3.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "1 package installed", - ]); - expect(await exited3).toBe(0); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "Bar"]); - expect(package_dir).toHaveWorkspaceLink(["Bar", "bar"]); - expect(await file(join(package_dir, "bun.lockb")).arrayBuffer()).toEqual(bun_lockb); - expect(await file(join(package_dir, "foo.txt")).text()).toBe("foo!"); - expect(await file(join(package_dir, "bar", "bar-preinstall.txt")).text()).toBe("bar preinstall!"); - expect(await file(join(package_dir, "bar", "bar-postinstall.txt")).text()).toBe("bar postinstall!"); -}); -it("should ignore workspaces within workspaces", async () => { - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - workspaces: ["bar"], - }), - ); - await mkdir(join(package_dir, "bar")); - await writeFile( - join(package_dir, "bar", "package.json"), - JSON.stringify({ - name: "bar", - version: "0.0.2", - workspaces: ["baz"], - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should ignore workspaces within workspaces", async () => { + await withContext(defaultOpts, async ctx => { + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + workspaces: ["bar"], + }), + ); + await mkdir(join(ctx.package_dir, "bar")); + await writeFile( + join(ctx.package_dir, "bar", "package.json"), + JSON.stringify({ + name: "bar", + version: "0.0.2", + workspaces: ["baz"], + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar"]); + expect(ctx.package_dir).toHaveWorkspaceLink(["bar", "bar"]); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); - expect(package_dir).toHaveWorkspaceLink(["bar", "bar"]); - await access(join(package_dir, "bun.lockb")); -}); -it("should handle ^0 in dependencies", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - bar: "^0", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should handle ^0 in dependencies", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + bar: "^0", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@0.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`, `${ctx.registry_url}bar-0.0.2.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@0.0.2", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - await access(join(package_dir, "bun.lockb")); -}); -it("should handle ^1 in dependencies", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - bar: "^1", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should handle ^1 in dependencies", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + bar: "^1", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain('error: No version matching "^1" found for specifier "bar" (but package exists)'); + expect(await stdout.text()).toEqual(expect.stringContaining("bun install v1.")); + expect(await exited).toBe(1); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`]); + expect(ctx.requested).toBe(1); + try { + await access(join(ctx.package_dir, "bun.lockb")); + expect.unreachable(); + } catch (err: any) { + expect(err.code).toBe("ENOENT"); + } + }); }); - const err = await stderr.text(); - expect(err).toContain('error: No version matching "^1" found for specifier "bar" (but package exists)'); - expect(await stdout.text()).toEqual(expect.stringContaining("bun install v1.")); - expect(await exited).toBe(1); - expect(urls.sort()).toEqual([`${root_url}/bar`]); - expect(requested).toBe(1); - try { - await access(join(package_dir, "bun.lockb")); - expect.unreachable(); - } catch (err: any) { - expect(err.code).toBe("ENOENT"); - } -}); -it("should handle ^0.0 in dependencies", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - bar: "^0.0", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should handle ^0.0 in dependencies", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + bar: "^0.0", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@0.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`, `${ctx.registry_url}bar-0.0.2.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@0.0.2", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - await access(join(package_dir, "bun.lockb")); -}); -it("should handle ^0.1 in dependencies", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - bar: "^0.1", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should handle ^0.1 in dependencies", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + bar: "^0.1", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain('error: No version matching "^0.1" found for specifier "bar" (but package exists)'); + expect(await stdout.text()).toEqual(expect.stringContaining("bun install v1.")); + expect(await exited).toBe(1); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`]); + expect(ctx.requested).toBe(1); + try { + await access(join(ctx.package_dir, "bun.lockb")); + expect.unreachable(); + } catch (err: any) { + expect(err.code).toBe("ENOENT"); + } + }); }); - const err = await stderr.text(); - expect(err).toContain('error: No version matching "^0.1" found for specifier "bar" (but package exists)'); - expect(await stdout.text()).toEqual(expect.stringContaining("bun install v1.")); - expect(await exited).toBe(1); - expect(urls.sort()).toEqual([`${root_url}/bar`]); - expect(requested).toBe(1); - try { - await access(join(package_dir, "bun.lockb")); - expect.unreachable(); - } catch (err: any) { - expect(err.code).toBe("ENOENT"); - } -}); -it("should handle ^0.0.0 in dependencies", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - bar: "^0.0.0", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should handle ^0.0.0 in dependencies", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + bar: "^0.0.0", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain('error: No version matching "^0.0.0" found for specifier "bar" (but package exists)'); + expect(await stdout.text()).toEqual(expect.stringContaining("bun install v1.")); + expect(await exited).toBe(1); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`]); + expect(ctx.requested).toBe(1); + try { + await access(join(ctx.package_dir, "bun.lockb")); + expect.unreachable(); + } catch (err: any) { + expect(err.code).toBe("ENOENT"); + } + }); }); - const err = await stderr.text(); - expect(err).toContain('error: No version matching "^0.0.0" found for specifier "bar" (but package exists)'); - expect(await stdout.text()).toEqual(expect.stringContaining("bun install v1.")); - expect(await exited).toBe(1); - expect(urls.sort()).toEqual([`${root_url}/bar`]); - expect(requested).toBe(1); - try { - await access(join(package_dir, "bun.lockb")); - expect.unreachable(); - } catch (err: any) { - expect(err.code).toBe("ENOENT"); - } -}); -it("should handle ^0.0.2 in dependencies", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - bar: "^0.0.2", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should handle ^0.0.2 in dependencies", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + bar: "^0.0.2", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@0.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`, `${ctx.registry_url}bar-0.0.2.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@0.0.2", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - await access(join(package_dir, "bun.lockb")); -}); -it("should handle matching workspaces from dependencies", async () => { - const urls: string[] = []; - setHandler( - dummyRegistry(urls, { - "0.2.0": { as: "0.2.0" }, - }), - ); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - workspaces: ["packages/*"], - }), - ); - await mkdir(join(package_dir, "packages", "pkg1"), { recursive: true }); - await mkdir(join(package_dir, "packages", "pkg2"), { recursive: true }); - await writeFile( - join(package_dir, "packages", "pkg1", "package.json"), - JSON.stringify({ - name: "pkg1", - version: "0.2.0", - }), - ); + it("should handle matching workspaces from dependencies", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { + "0.2.0": { as: "0.2.0" }, + }), + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + workspaces: ["packages/*"], + }), + ); + await mkdir(join(ctx.package_dir, "packages", "pkg1"), { recursive: true }); + await mkdir(join(ctx.package_dir, "packages", "pkg2"), { recursive: true }); + await writeFile( + join(ctx.package_dir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg1", + version: "0.2.0", + }), + ); - await writeFile( - join(package_dir, "packages", "pkg2", "package.json"), - JSON.stringify({ - name: "pkg2", - version: "0.2.0", - dependencies: { - // moo has a dependency on pkg1 that matches 0.2.0 - moo: "0.2.0", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + await writeFile( + join(ctx.package_dir, "packages", "pkg2", "package.json"), + JSON.stringify({ + name: "pkg2", + version: "0.2.0", + dependencies: { + // moo has a dependency on pkg1 that matches 0.2.0 + moo: "0.2.0", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).not.toContain("error:"); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "3 packages installed", + ]); + expect(await exited).toBe(0); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - const err = await stderr.text(); - expect(err).not.toContain("error:"); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "3 packages installed", - ]); - expect(await exited).toBe(0); - await access(join(package_dir, "bun.lockb")); -}); -it("should edit package json correctly with git dependencies", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - const package_json = JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: {}, + it("should edit package json correctly with git dependencies", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + const package_json = JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: {}, + }); + await writeFile(join(ctx.package_dir, "package.json"), package_json); + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i", "dylan-conway/install-test2"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + var err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + expect(await file(join(ctx.package_dir, "package.json")).json()).toEqual({ + name: "foo", + version: "0.0.1", + dependencies: { + "install-test2": "dylan-conway/install-test2", + }, + }); + await writeFile(join(ctx.package_dir, "package.json"), package_json); + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i", "dylan-conway/install-test2#HEAD"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + expect(await file(join(ctx.package_dir, "package.json")).json()).toEqual({ + name: "foo", + version: "0.0.1", + dependencies: { + "install-test2": "dylan-conway/install-test2#HEAD", + }, + }); + await writeFile(join(ctx.package_dir, "package.json"), package_json); + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i", "github:dylan-conway/install-test2"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + expect(await file(join(ctx.package_dir, "package.json")).json()).toEqual({ + name: "foo", + version: "0.0.1", + dependencies: { + "install-test2": "github:dylan-conway/install-test2", + }, + }); + await writeFile(join(ctx.package_dir, "package.json"), package_json); + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "i", "github:dylan-conway/install-test2#HEAD"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + expect(await exited).toBe(0); + expect(await file(join(ctx.package_dir, "package.json")).json()).toEqual({ + name: "foo", + version: "0.0.1", + dependencies: { + "install-test2": "github:dylan-conway/install-test2#HEAD", + }, + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - await writeFile(join(package_dir, "package.json"), package_json); - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i", "dylan-conway/install-test2"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - var err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - expect(await exited).toBe(0); - expect(await file(join(package_dir, "package.json")).json()).toEqual({ - name: "foo", - version: "0.0.1", - dependencies: { - "install-test2": "dylan-conway/install-test2", - }, - }); - await writeFile(join(package_dir, "package.json"), package_json); - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i", "dylan-conway/install-test2#HEAD"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - expect(await exited).toBe(0); - expect(await file(join(package_dir, "package.json")).json()).toEqual({ - name: "foo", - version: "0.0.1", - dependencies: { - "install-test2": "dylan-conway/install-test2#HEAD", - }, - }); - await writeFile(join(package_dir, "package.json"), package_json); - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i", "github:dylan-conway/install-test2"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - expect(await exited).toBe(0); - expect(await file(join(package_dir, "package.json")).json()).toEqual({ - name: "foo", - version: "0.0.1", - dependencies: { - "install-test2": "github:dylan-conway/install-test2", - }, - }); - await writeFile(join(package_dir, "package.json"), package_json); - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "i", "github:dylan-conway/install-test2#HEAD"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - expect(await exited).toBe(0); - expect(await file(join(package_dir, "package.json")).json()).toEqual({ - name: "foo", - version: "0.0.1", - dependencies: { - "install-test2": "github:dylan-conway/install-test2#HEAD", - }, - }); - await access(join(package_dir, "bun.lockb")); -}); -it("should handle ^0.0.2-rc in dependencies", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls, { "0.0.2-rc": { as: "0.0.2" } })); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - bar: "^0.0.2-rc", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should handle ^0.0.2-rc in dependencies", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls, { "0.0.2-rc": { as: "0.0.2" } })); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + bar: "^0.0.2-rc", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@0.0.2-rc", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`, `${ctx.registry_url}bar-0.0.2.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@0.0.2-rc", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - await access(join(package_dir, "bun.lockb")); -}); -it("should handle ^0.0.2-alpha.3+b4d in dependencies", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls, { "0.0.2-alpha.3": { as: "0.0.2" } })); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - bar: "^0.0.2-alpha.3+b4d", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should handle ^0.0.2-alpha.3+b4d in dependencies", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls, { "0.0.2-alpha.3": { as: "0.0.2" } })); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + bar: "^0.0.2-alpha.3+b4d", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@0.0.2-alpha.3", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`, `${ctx.registry_url}bar-0.0.2.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@0.0.2-alpha.3", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - await access(join(package_dir, "bun.lockb")); -}); -it("should choose the right version with prereleases", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls, { "0.0.2-alpha.3": { as: "0.0.2" } })); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - bar: "^0.0.2-alpha.3+b4d", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should choose the right version with prereleases", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls, { "0.0.2-alpha.3": { as: "0.0.2" } })); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + bar: "^0.0.2-alpha.3+b4d", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@0.0.2-alpha.3", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`, `${ctx.registry_url}bar-0.0.2.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@0.0.2-alpha.3", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - await access(join(package_dir, "bun.lockb")); -}); -it("should handle ^0.0.2rc1 in dependencies", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls, { "0.0.2rc1": { as: "0.0.2" } })); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - bar: "^0.0.2rc1", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should handle ^0.0.2rc1 in dependencies", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls, { "0.0.2rc1": { as: "0.0.2" } })); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + bar: "^0.0.2rc1", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@0.0.2-rc1", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`, `${ctx.registry_url}bar-0.0.2.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@0.0.2-rc1", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - await access(join(package_dir, "bun.lockb")); -}); -it("should handle caret range in dependencies when the registry has prereleased packages, issue#4398", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls, { "6.3.0": { as: "0.0.2" }, "7.0.0-rc2": { as: "0.0.3" } })); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - bar: "^6.3.0", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should handle caret range in dependencies when the registry has prereleased packages, issue#4398", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { "6.3.0": { as: "0.0.2" }, "7.0.0-rc2": { as: "0.0.3" } }), + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + bar: "^6.3.0", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ bar@6.3.0"), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`, `${ctx.registry_url}bar-0.0.2.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ bar@6.3.0"), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - await access(join(package_dir, "bun.lockb")); -}); -it("should prefer latest-tagged dependency", async () => { - const urls: string[] = []; - setHandler( - dummyRegistry(urls, { - "0.0.3": { + it("should prefer latest-tagged dependency", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { + "0.0.3": { + bin: { + "baz-run": "index.js", + }, + }, + "0.0.5": { + bin: { + "baz-exec": "index.js", + }, + }, + latest: "0.0.3", + }), + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + baz: "~0.0.2", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ baz@0.0.3", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}baz`, `${ctx.registry_url}baz-0.0.3.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "baz"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", bin: { "baz-run": "index.js", }, - }, - "0.0.5": { + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should install latest with prereleases", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { + "1.0.0-0": { as: "0.0.3" }, + "1.0.0-8": { as: "0.0.5" }, + latest: "1.0.0-0", + }), + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + }), + ); + + var { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "baz"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + var err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + var out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + "installed baz@1.0.0-0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(ctx.requested).toBe(2); + await rm(join(ctx.package_dir, "node_modules"), { recursive: true, force: true }); + await rm(join(ctx.package_dir, "bun.lockb"), { recursive: true, force: true }); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + baz: "latest", + }, + }), + ); + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ baz@1.0.0-0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + await rm(join(ctx.package_dir, "node_modules"), { recursive: true, force: true }); + await rm(join(ctx.package_dir, "bun.lockb"), { recursive: true, force: true }); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + baz: "^1.0.0-5", + }, + }), + ); + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ baz@1.0.0-8", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + + await rm(join(ctx.package_dir, "node_modules"), { recursive: true, force: true }); + await rm(join(ctx.package_dir, "bun.lockb"), { recursive: true, force: true }); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + baz: "^1.0.0-0", + }, + }), + ); + ({ stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + })); + err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ baz@1.0.0-0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle dependency aliasing", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { + "0.0.3": { + bin: { + "baz-run": "index.js", + }, + }, + }), + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + Bar: "npm:baz", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ Bar@0.0.3", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}baz`, `${ctx.registry_url}baz-0.0.3.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "Bar"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "Bar", "index.js")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "Bar"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "Bar", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle dependency aliasing (versioned)", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { + "0.0.3": { + bin: { + "baz-run": "index.js", + }, + }, + }), + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + Bar: "npm:baz@0.0.3", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ Bar@0.0.3", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}baz`, `${ctx.registry_url}baz-0.0.3.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "Bar"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "Bar", "index.js")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "Bar"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "Bar", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle dependency aliasing (dist-tagged)", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { + "0.0.3": { + bin: { + "baz-run": "index.js", + }, + }, + }), + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + Bar: "npm:baz@latest", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ Bar@0.0.3", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}baz`, `${ctx.registry_url}baz-0.0.3.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "Bar"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "Bar", "index.js")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "Bar"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "Bar", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should not reinstall aliased dependencies", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { + "0.0.3": { + bin: { + "baz-run": "index.js", + }, + }, + }), + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + Bar: "npm:baz", + }, + }), + ); + const { + stdout: stdout1, + stderr: stderr1, + exited: exited1, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err1 = await new Response(stderr1).text(); + expect(err1).toContain("Saved lockfile"); + const out1 = await new Response(stdout1).text(); + expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ Bar@0.0.3", + "", + "1 package installed", + ]); + expect(await exited1).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}baz`, `${ctx.registry_url}baz-0.0.3.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "Bar"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "Bar", "index.js")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "Bar"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "Bar", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + await access(join(ctx.package_dir, "bun.lockb")); + // Performs `bun install` again, expects no-op + urls.length = 0; + const { + stdout: stdout2, + stderr: stderr2, + exited: exited2, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err2 = await new Response(stderr2).text(); + expect(err2).not.toContain("Saved lockfile"); + const out2 = await new Response(stdout2).text(); + expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "Checked 1 install across 2 packages (no changes)", + ]); + expect(await exited2).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "Bar"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "Bar", "index.js")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "Bar"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "Bar", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle aliased & direct dependency references", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { + "0.0.3": { + bin: { + "baz-run": "index.js", + }, + }, + }), + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + baz: "~0.0.2", + }, + workspaces: ["bar"], + }), + ); + await mkdir(join(ctx.package_dir, "bar")); + await writeFile( + join(ctx.package_dir, "bar", "package.json"), + JSON.stringify({ + name: "bar", + version: "0.0.4", + dependencies: { + moo: "npm:baz", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ baz@0.0.3", + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}baz`, `${ctx.registry_url}baz-0.0.3.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([ + ".bin", + ".cache", + "bar", + "baz", + "moo", + ]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js")); + expect(await readlink(join(ctx.package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "bar")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + expect(await readdirSorted(join(ctx.package_dir, "bar"))).toEqual(["package.json"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "moo"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "moo", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should not hoist if name collides with alias", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { + "0.0.2": {}, + "0.0.3": { + bin: { + "baz-run": "index.js", + }, + }, + }), + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + bar: "npm:baz", + }, + workspaces: ["moo"], + }), + ); + await mkdir(join(ctx.package_dir, "moo")); + await writeFile( + join(ctx.package_dir, "moo", "package.json"), + JSON.stringify({ + name: "moo", + version: "0.0.4", + dependencies: { + bar: "0.0.2", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@0.0.3", + "", + "3 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([ + `${ctx.registry_url}bar`, + `${ctx.registry_url}bar-0.0.2.tgz`, + `${ctx.registry_url}baz`, + `${ctx.registry_url}baz-0.0.3.tgz`, + ]); + expect(ctx.requested).toBe(4); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "bar", "moo"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "bar", "index.js")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + expect(await readlink(join(ctx.package_dir, "node_modules", "moo"))).toBeWorkspaceLink(join("..", "moo")); + expect(await readdirSorted(join(ctx.package_dir, "moo"))).toEqual(["node_modules", "package.json"]); + expect(await readdirSorted(join(ctx.package_dir, "moo", "node_modules"))).toEqual(["bar"]); + expect(await readdirSorted(join(ctx.package_dir, "moo", "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "moo", "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should get npm alias with matching version", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { + "0.0.3": { as: "0.0.3" }, + "0.0.5": { as: "0.0.5" }, + }), + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + workspaces: ["moo"], + dependencies: { + "boba": "npm:baz@0.0.5", + }, + }), + ); + await mkdir(join(ctx.package_dir, "moo")); + await writeFile( + join(ctx.package_dir, "moo", "package.json"), + JSON.stringify({ + name: "moo", + version: "0.0.2", + dependencies: { + boba: ">=0.0.3", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ boba@0.0.5", + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}baz`, `${ctx.registry_url}baz-0.0.5.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "boba", "moo"]); + expect(await file(join(ctx.package_dir, "node_modules", "boba", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.5", bin: { "baz-exec": "index.js", }, - }, - latest: "0.0.3", - }), - ); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - baz: "~0.0.2", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ baz@0.0.3", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.3.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"))).toHaveBins(["baz-run"]); - expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(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", - }, - }); - await access(join(package_dir, "bun.lockb")); -}); -it("should install latest with prereleases", async () => { - const urls: string[] = []; - setHandler( - dummyRegistry(urls, { - "1.0.0-0": { as: "0.0.3" }, - "1.0.0-8": { as: "0.0.5" }, - latest: "1.0.0-0", - }), - ); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - }), - ); + it("should not apply overrides to package name of aliased package", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { + "0.0.3": { as: "0.0.3" }, + }), + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.2.0", + dependencies: { + bar: "npm:baz@0.0.3", + }, + overrides: { + "baz": "0.0.5", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); - var { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "baz"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - var err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - var out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - "installed baz@1.0.0-0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(requested).toBe(2); - await rm(join(package_dir, "node_modules"), { recursive: true, force: true }); - await rm(join(package_dir, "bun.lockb"), { recursive: true, force: true }); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - baz: "latest", - }, - }), - ); - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ baz@1.0.0-0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - await rm(join(package_dir, "node_modules"), { recursive: true, force: true }); - await rm(join(package_dir, "bun.lockb"), { recursive: true, force: true }); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - baz: "^1.0.0-5", - }, - }), - ); - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ baz@1.0.0-8", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - - await rm(join(package_dir, "node_modules"), { recursive: true, force: true }); - await rm(join(package_dir, "bun.lockb"), { recursive: true, force: true }); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - baz: "^1.0.0-0", - }, - }), - ); - ({ stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - })); - err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ baz@1.0.0-0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle dependency aliasing", async () => { - const urls: string[] = []; - setHandler( - dummyRegistry(urls, { - "0.0.3": { + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@0.0.3", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}baz`, `${ctx.registry_url}baz-0.0.3.tgz`]); + expect(ctx.requested).toBe(2); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", bin: { "baz-run": "index.js", }, - }, - }), - ); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - dependencies: { - Bar: "npm:baz", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ Bar@0.0.3", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.3.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "Bar"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); - expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "Bar", "index.js")); - expect(await readdirSorted(join(package_dir, "node_modules", "Bar"))).toEqual(["index.js", "package.json"]); - expect(await file(join(package_dir, "node_modules", "Bar", "package.json")).json()).toEqual({ - name: "baz", - version: "0.0.3", - bin: { - "baz-run": "index.js", - }, - }); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle dependency aliasing (versioned)", 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", - dependencies: { - Bar: "npm:baz@0.0.3", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ Bar@0.0.3", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.3.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "Bar"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); - expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "Bar", "index.js")); - expect(await readdirSorted(join(package_dir, "node_modules", "Bar"))).toEqual(["index.js", "package.json"]); - expect(await file(join(package_dir, "node_modules", "Bar", "package.json")).json()).toEqual({ - name: "baz", - version: "0.0.3", - bin: { - "baz-run": "index.js", - }, - }); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle dependency aliasing (dist-tagged)", 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", - dependencies: { - Bar: "npm:baz@latest", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ Bar@0.0.3", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.3.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "Bar"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); - expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "Bar", "index.js")); - expect(await readdirSorted(join(package_dir, "node_modules", "Bar"))).toEqual(["index.js", "package.json"]); - expect(await file(join(package_dir, "node_modules", "Bar", "package.json")).json()).toEqual({ - name: "baz", - version: "0.0.3", - bin: { - "baz-run": "index.js", - }, - }); - await access(join(package_dir, "bun.lockb")); -}); - -it("should not reinstall aliased dependencies", 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", - dependencies: { - Bar: "npm:baz", - }, - }), - ); - const { - stdout: stdout1, - stderr: stderr1, - exited: exited1, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err1 = await new Response(stderr1).text(); - expect(err1).toContain("Saved lockfile"); - const out1 = await new Response(stdout1).text(); - expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ Bar@0.0.3", - "", - "1 package installed", - ]); - expect(await exited1).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.3.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "Bar"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); - expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "Bar", "index.js")); - expect(await readdirSorted(join(package_dir, "node_modules", "Bar"))).toEqual(["index.js", "package.json"]); - expect(await file(join(package_dir, "node_modules", "Bar", "package.json")).json()).toEqual({ - name: "baz", - version: "0.0.3", - bin: { - "baz-run": "index.js", - }, - }); - await access(join(package_dir, "bun.lockb")); - // Performs `bun install` again, expects no-op - urls.length = 0; - const { - stdout: stdout2, - stderr: stderr2, - exited: exited2, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err2 = await new Response(stderr2).text(); - expect(err2).not.toContain("Saved lockfile"); - const out2 = await new Response(stdout2).text(); - expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "Checked 1 install across 2 packages (no changes)", - ]); - expect(await exited2).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "Bar"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); - expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "Bar", "index.js")); - expect(await readdirSorted(join(package_dir, "node_modules", "Bar"))).toEqual(["index.js", "package.json"]); - expect(await file(join(package_dir, "node_modules", "Bar", "package.json")).json()).toEqual({ - name: "baz", - version: "0.0.3", - bin: { - "baz-run": "index.js", - }, - }); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle aliased & direct dependency references", 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", - dependencies: { - baz: "~0.0.2", - }, - workspaces: ["bar"], - }), - ); - await mkdir(join(package_dir, "bar")); - await writeFile( - join(package_dir, "bar", "package.json"), - JSON.stringify({ - name: "bar", - version: "0.0.4", - dependencies: { - moo: "npm:baz", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ baz@0.0.3", - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.3.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "bar", "baz", "moo"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); - expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js")); - expect(await readlink(join(package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "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 readdirSorted(join(package_dir, "bar"))).toEqual(["package.json"]); - expect(await readdirSorted(join(package_dir, "node_modules", "moo"))).toEqual(["index.js", "package.json"]); - expect(await file(join(package_dir, "node_modules", "moo", "package.json")).json()).toEqual({ - name: "baz", - version: "0.0.3", - bin: { - "baz-run": "index.js", - }, - }); - await access(join(package_dir, "bun.lockb")); -}); - -it("should not hoist if name collides with alias", async () => { - const urls: string[] = []; - setHandler( - dummyRegistry(urls, { - "0.0.2": {}, - "0.0.3": { - bin: { - "baz-run": "index.js", - }, - }, - }), - ); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - bar: "npm:baz", - }, - workspaces: ["moo"], - }), - ); - await mkdir(join(package_dir, "moo")); - await writeFile( - join(package_dir, "moo", "package.json"), - JSON.stringify({ - name: "moo", - version: "0.0.4", - dependencies: { - bar: "0.0.2", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@0.0.3", - "", - "3 packages installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([ - `${root_url}/bar`, - `${root_url}/bar-0.0.2.tgz`, - `${root_url}/baz`, - `${root_url}/baz-0.0.3.tgz`, - ]); - expect(requested).toBe(4); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "bar", "moo"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); - expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "bar", "index.js")); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["index.js", "package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "baz", - version: "0.0.3", - bin: { - "baz-run": "index.js", - }, - }); - expect(await readlink(join(package_dir, "node_modules", "moo"))).toBeWorkspaceLink(join("..", "moo")); - expect(await readdirSorted(join(package_dir, "moo"))).toEqual(["node_modules", "package.json"]); - expect(await readdirSorted(join(package_dir, "moo", "node_modules"))).toEqual(["bar"]); - expect(await readdirSorted(join(package_dir, "moo", "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "moo", "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - await access(join(package_dir, "bun.lockb")); -}); - -it("should get npm alias with matching version", async () => { - const urls: string[] = []; - setHandler( - dummyRegistry(urls, { - "0.0.3": { as: "0.0.3" }, - "0.0.5": { as: "0.0.5" }, - }), - ); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - workspaces: ["moo"], - dependencies: { - "boba": "npm:baz@0.0.5", - }, - }), - ); - await mkdir(join(package_dir, "moo")); - await writeFile( - join(package_dir, "moo", "package.json"), - JSON.stringify({ - name: "moo", - version: "0.0.2", - dependencies: { - boba: ">=0.0.3", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ boba@0.0.5", - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.5.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "boba", "moo"]); - expect(await file(join(package_dir, "node_modules", "boba", "package.json")).json()).toEqual({ - name: "baz", - version: "0.0.5", - bin: { - "baz-exec": "index.js", - }, - }); - await access(join(package_dir, "bun.lockb")); -}); - -it("should not apply overrides to package name of aliased package", async () => { - const urls: string[] = []; - setHandler( - dummyRegistry(urls, { - "0.0.3": { as: "0.0.3" }, - }), - ); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.2.0", - dependencies: { - bar: "npm:baz@0.0.3", - }, - overrides: { - "baz": "0.0.5", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@0.0.3", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.3.tgz`]); - expect(requested).toBe(2); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "baz", - version: "0.0.3", - bin: { - "baz-run": "index.js", - }, - }); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle unscoped alias on scoped dependency", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls, { "0.1.0": {} })); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - "@barn/moo": "latest", - moo: "npm:@barn/moo", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ @barn/moo@0.1.0", - "+ moo@0.1.0", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/@barn%2fmoo`, `${root_url}/@barn/moo-0.1.0.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "@barn", "moo"]); - expect(await readdirSorted(join(package_dir, "node_modules", "@barn"))).toEqual(["moo"]); - expect(await readdirSorted(join(package_dir, "node_modules", "@barn", "moo"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "@barn", "moo", "package.json")).json()).toEqual({ - name: "@barn/moo", - version: "0.1.0", - // not installed as these are absent from manifest above - dependencies: { - bar: "0.0.2", - baz: "latest", - }, - }); - expect(await readdirSorted(join(package_dir, "node_modules", "moo"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "moo", "package.json")).json()).toEqual({ - name: "@barn/moo", - version: "0.1.0", - dependencies: { - bar: "0.0.2", - baz: "latest", - }, - }); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle scoped alias on unscoped dependency", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - "@baz/bar": "npm:bar", - bar: "latest", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ @baz/bar@0.0.2", - "+ bar@0.0.2", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "@baz", "bar"]); - expect(await readdirSorted(join(package_dir, "node_modules", "@baz"))).toEqual(["bar"]); - expect(await readdirSorted(join(package_dir, "node_modules", "@baz", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "@baz", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle aliased dependency with existing lockfile", async () => { - const urls: string[] = []; - setHandler( - dummyRegistry(urls, { - "0.0.2": {}, - "0.0.3": { - bin: { - "baz-run": "index.js", - }, - }, - "0.1.0": { + it("should handle unscoped alias on scoped dependency", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls, { "0.1.0": {} })); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + "@barn/moo": "latest", + moo: "npm:@barn/moo", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ @barn/moo@0.1.0", + "+ moo@0.1.0", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}@barn%2fmoo`, `${ctx.registry_url}@barn/moo-0.1.0.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "@barn", "moo"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@barn"))).toEqual(["moo"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@barn", "moo"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "@barn", "moo", "package.json")).json()).toEqual({ + name: "@barn/moo", + version: "0.1.0", + // not installed as these are absent from manifest above dependencies: { bar: "0.0.2", baz: "latest", }, - }, - latest: "0.0.3", - }), - ); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - "moz": "npm:@barn/moo@0.1.0", - }, - }), - ); - const { - stdout: stdout1, - stderr: stderr1, - exited: exited1, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err1 = await new Response(stderr1).text(); - expect(err1).toContain("Saved lockfile"); - const out1 = await new Response(stdout1).text(); - expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ moz@0.1.0", - "", - "3 packages installed", - ]); - expect(await exited1).toBe(0); - expect(urls.sort()).toEqual([ - `${root_url}/@barn%2fmoo`, - `${root_url}/@barn/moo-0.1.0.tgz`, - `${root_url}/bar`, - `${root_url}/bar-0.0.2.tgz`, - `${root_url}/baz`, - `${root_url}/baz-0.0.3.tgz`, - ]); - expect(requested).toBe(6); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "bar", "baz", "moz"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); - expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js")); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - 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 readdirSorted(join(package_dir, "node_modules", "moz"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "moz", "package.json")).json()).toEqual({ - name: "@barn/moo", - version: "0.1.0", - dependencies: { - bar: "0.0.2", - baz: "latest", - }, - }); - await access(join(package_dir, "bun.lockb")); - // Perform `bun install` again but with lockfile from before - await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); - urls.length = 0; - const { - stdout: stdout2, - stderr: stderr2, - exited: exited2, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err2 = await new Response(stderr2).text(); - expect(err2).not.toContain("Saved lockfile"); - const out2 = await new Response(stdout2).text(); - expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ moz@0.1.0", - "", - "3 packages installed", - ]); - expect(await exited2).toBe(0); - expect(urls.sort()).toEqual([ - `${root_url}/@barn/moo-0.1.0.tgz`, - `${root_url}/bar-0.0.2.tgz`, - `${root_url}/baz-0.0.3.tgz`, - ]); - expect(requested).toBe(9); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "bar", "baz", "moz"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); - expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js")); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - 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 readdirSorted(join(package_dir, "node_modules", "moz"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "moz", "package.json")).json()).toEqual({ - name: "@barn/moo", - version: "0.1.0", - dependencies: { - bar: "0.0.2", - baz: "latest", - }, - }); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle GitHub URL in dependencies (user/repo)", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - dependencies: { - uglify: "mishoo/UglifyJS", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - let out = await stdout.text(); - out = out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, ""); - out = out.replace(/(github:[^#]+)#[a-f0-9]+/, "$1"); - expect(out.split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ uglify@github:mishoo/UglifyJS", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "uglify"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["uglifyjs"]); - expect(await readdirSorted(join(package_dir, "node_modules", "uglify"))).toEqual([ - ".bun-tag", - ".gitattributes", - ".github", - ".gitignore", - "CONTRIBUTING.md", - "LICENSE", - "README.md", - "bin", - "lib", - "package.json", - "test", - "tools", - ]); - const package_json = await file(join(package_dir, "node_modules", "uglify", "package.json")).json(); - expect(package_json.name).toBe("uglify-js"); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle GitHub URL in dependencies (user/repo#commit-id)", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - dependencies: { - uglify: "mishoo/UglifyJS#e219a9a", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ uglify@github:mishoo/UglifyJS#e219a9a", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "uglify"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["uglifyjs"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".cache"))).toEqual([ - "@GH@mishoo-UglifyJS-e219a9a@@@1", - "uglify", - ]); - expect(await readdirSorted(join(package_dir, "node_modules", ".cache", "uglify"))).toEqual([ - "mishoo-UglifyJS-e219a9a@@@1", - ]); - expect( - resolve(await readlink(join(package_dir, "node_modules", ".cache", "uglify", "mishoo-UglifyJS-e219a9a@@@1"))), - ).toBe(join(package_dir, "node_modules", ".cache", "@GH@mishoo-UglifyJS-e219a9a@@@1")); - expect(await readdirSorted(join(package_dir, "node_modules", "uglify"))).toEqual([ - ".bun-tag", - ".gitattributes", - ".github", - ".gitignore", - "CONTRIBUTING.md", - "LICENSE", - "README.md", - "bin", - "lib", - "package.json", - "test", - "tools", - ]); - const package_json = await file(join(package_dir, "node_modules", "uglify", "package.json")).json(); - expect(package_json.name).toBe("uglify-js"); - expect(package_json.version).toBe("3.14.1"); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle GitHub URL in dependencies (user/repo#tag)", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - dependencies: { - uglify: "mishoo/UglifyJS#v3.14.1", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ uglify@github:mishoo/UglifyJS#e219a9a", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "uglify"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["uglifyjs"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".cache"))).toEqual([ - "@GH@mishoo-UglifyJS-e219a9a@@@1", - "uglify", - ]); - expect(await readdirSorted(join(package_dir, "node_modules", ".cache", "uglify"))).toEqual([ - "mishoo-UglifyJS-e219a9a@@@1", - ]); - expect( - resolve(await readlink(join(package_dir, "node_modules", ".cache", "uglify", "mishoo-UglifyJS-e219a9a@@@1"))), - ).toBe(join(package_dir, "node_modules", ".cache", "@GH@mishoo-UglifyJS-e219a9a@@@1")); - expect(await readdirSorted(join(package_dir, "node_modules", "uglify"))).toEqual([ - ".bun-tag", - ".gitattributes", - ".github", - ".gitignore", - "CONTRIBUTING.md", - "LICENSE", - "README.md", - "bin", - "lib", - "package.json", - "test", - "tools", - ]); - const package_json = await file(join(package_dir, "node_modules", "uglify", "package.json")).json(); - expect(package_json.name).toBe("uglify-js"); - expect(package_json.version).toBe("3.14.1"); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle bitbucket git dependencies", async () => { - const deps = [ - "bitbucket:dylan-conway/public-install-test", - "bitbucket.org:dylan-conway/public-install-test", - "bitbucket.com:dylan-conway/public-install-test", - "git@bitbucket.org:dylan-conway/public-install-test", - ]; - for (const dep of deps) { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", + }); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "moo"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "moo", "package.json")).json()).toEqual({ + name: "@barn/moo", + version: "0.1.0", dependencies: { - "public-install-test": dep, + bar: "0.0.2", + baz: "latest", }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + }); + await access(join(ctx.package_dir, "bun.lockb")); }); - - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ public-install-test@git+ssh://${dep}#79265e2d9754c60b60f97cc8d859fb6da073b5d2`, - "", - expect.stringContaining("installed"), - ]); - expect(await exited).toBe(0); - await access(join(package_dir, "bun.lockb")); - await dummyAfterEach(); - await dummyBeforeEach({ linker: "isolated" }); - } - - for (const dep of deps) { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "add", dep], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - `installed publicinstalltest@git+ssh://${dep}#79265e2d9754c60b60f97cc8d859fb6da073b5d2`, - "", - expect.stringContaining("installed"), - ]); - expect(await exited).toBe(0); - await access(join(package_dir, "bun.lockb")); - await dummyAfterEach(); - await dummyBeforeEach({ linker: "isolated" }); - } -}); - -it("should handle gitlab git dependencies", async () => { - const deps = ["gitlab:dylan-conway/public-install-test", "gitlab.com:dylan-conway/public-install-test"]; - for (const dep of deps) { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - "public-install-test": dep, - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ public-install-test@git+ssh://${dep}#93f3aa4ec9ca8a0bacc010776db48bfcd915c44c`, - "", - expect.stringContaining("installed"), - ]); - expect(await exited).toBe(0); - await access(join(package_dir, "bun.lockb")); - await dummyAfterEach(); - await dummyBeforeEach({ linker: "isolated" }); - } - - for (const dep of deps) { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "add", dep], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun add v1."), - "", - `installed public-install-test@git+ssh://${dep}#93f3aa4ec9ca8a0bacc010776db48bfcd915c44c`, - "", - expect.stringContaining("installed"), - ]); - expect(await exited).toBe(0); - await access(join(package_dir, "bun.lockb")); - await dummyAfterEach(); - await dummyBeforeEach({ linker: "isolated" }); - } -}); - -it("should handle GitHub URL in dependencies (github:user/repo#tag)", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - dependencies: { - uglify: "github:mishoo/UglifyJS#v3.14.1", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ uglify@github:mishoo/UglifyJS#e219a9a", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "uglify"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["uglifyjs"]); - expect(join(package_dir, "node_modules", ".bin", "uglifyjs")).toBeValidBin(join("..", "uglify", "bin", "uglifyjs")); - expect(await readdirSorted(join(package_dir, "node_modules", ".cache"))).toEqual([ - "@GH@mishoo-UglifyJS-e219a9a@@@1", - "uglify", - ]); - expect(await readdirSorted(join(package_dir, "node_modules", ".cache", "uglify"))).toEqual([ - "mishoo-UglifyJS-e219a9a@@@1", - ]); - expect( - resolve(await readlink(join(package_dir, "node_modules", ".cache", "uglify", "mishoo-UglifyJS-e219a9a@@@1"))), - ).toBe(join(package_dir, "node_modules", ".cache", "@GH@mishoo-UglifyJS-e219a9a@@@1")); - expect(await readdirSorted(join(package_dir, "node_modules", "uglify"))).toEqual([ - ".bun-tag", - ".gitattributes", - ".github", - ".gitignore", - "CONTRIBUTING.md", - "LICENSE", - "README.md", - "bin", - "lib", - "package.json", - "test", - "tools", - ]); - const package_json = await file(join(package_dir, "node_modules", "uglify", "package.json")).json(); - expect(package_json.name).toBe("uglify-js"); - expect(package_json.version).toBe("3.14.1"); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle GitHub URL in dependencies (https://github.com/user/repo.git)", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - dependencies: { - uglify: "https://github.com/mishoo/UglifyJS.git", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - let out = await stdout.text(); - out = out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, ""); - out = out.replace(/(github:[^#]+)#[a-f0-9]+/, "$1"); - expect(out.split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ uglify@github:mishoo/UglifyJS", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "uglify"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["uglifyjs"]); - expect(await readdirSorted(join(package_dir, "node_modules", "uglify"))).toEqual([ - ".bun-tag", - ".gitattributes", - ".github", - ".gitignore", - "CONTRIBUTING.md", - "LICENSE", - "README.md", - "bin", - "lib", - "package.json", - "test", - "tools", - ]); - const package_json = await file(join(package_dir, "node_modules", "uglify", "package.json")).json(); - expect(package_json.name).toBe("uglify-js"); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle GitHub URL in dependencies (git://github.com/user/repo.git#commit)", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - dependencies: { - uglify: "git://github.com/mishoo/UglifyJS.git#e219a9a", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ uglify@github:mishoo/UglifyJS#e219a9a", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "uglify"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["uglifyjs"]); - expect(join(package_dir, "node_modules", ".bin", "uglifyjs")).toBeValidBin(join("..", "uglify", "bin", "uglifyjs")); - expect(await readdirSorted(join(package_dir, "node_modules", ".cache"))).toEqual([ - "@GH@mishoo-UglifyJS-e219a9a@@@1", - "uglify", - ]); - expect(await readdirSorted(join(package_dir, "node_modules", ".cache", "uglify"))).toEqual([ - "mishoo-UglifyJS-e219a9a@@@1", - ]); - expect( - resolve(await readlink(join(package_dir, "node_modules", ".cache", "uglify", "mishoo-UglifyJS-e219a9a@@@1"))), - ).toBe(join(package_dir, "node_modules", ".cache", "@GH@mishoo-UglifyJS-e219a9a@@@1")); - expect(await readdirSorted(join(package_dir, "node_modules", "uglify"))).toEqual([ - ".bun-tag", - ".gitattributes", - ".github", - ".gitignore", - "CONTRIBUTING.md", - "LICENSE", - "README.md", - "bin", - "lib", - "package.json", - "test", - "tools", - ]); - const package_json = await file(join(package_dir, "node_modules", "uglify", "package.json")).json(); - expect(package_json.name).toBe("uglify-js"); - expect(package_json.version).toBe("3.14.1"); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle GitHub URL in dependencies (git+https://github.com/user/repo.git)", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - dependencies: { - uglify: "git+https://github.com/mishoo/UglifyJS.git", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - let out = await stdout.text(); - out = out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, ""); - out = out.replace(/(github:[^#]+)#[a-f0-9]+/, "$1"); - expect(out.split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ uglify@github:mishoo/UglifyJS", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "uglify"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["uglifyjs"]); - expect(await readdirSorted(join(package_dir, "node_modules", "uglify"))).toEqual([ - ".bun-tag", - ".gitattributes", - ".github", - ".gitignore", - "CONTRIBUTING.md", - "LICENSE", - "README.md", - "bin", - "lib", - "package.json", - "test", - "tools", - ]); - const package_json = await file(join(package_dir, "node_modules", "uglify", "package.json")).json(); - expect(package_json.name).toBe("uglify-js"); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle GitHub tarball URL in dependencies (https://github.com/user/repo/tarball/ref)", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - dependencies: { - when: "https://github.com/cujojs/when/tarball/1.0.2", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - let out = await stdout.text(); - out = out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, ""); - out = out.replace(/(github:[^#]+)#[a-f0-9]+/, "$1"); - expect(out.split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ when@https://github.com/cujojs/when/tarball/1.0.2", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "when"]); - expect(await readdirSorted(join(package_dir, "node_modules", "when"))).toEqual([ - ".gitignore", - ".gitmodules", - "LICENSE.txt", - "README.md", - "apply.js", - "cancelable.js", - "delay.js", - "package.json", - "test", - "timed.js", - "timeout.js", - "when.js", - ]); - const package_json = await file(join(package_dir, "node_modules", "when", "package.json")).json(); - expect(package_json.name).toBe("when"); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle GitHub tarball URL in dependencies (https://github.com/user/repo/tarball/ref) with custom GITHUB_API_URL", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - dependencies: { - when: "https://github.com/cujojs/when/tarball/1.0.2", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env: { - ...env, - GITHUB_API_URL: "https://example.com/github/api", - }, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - let out = await stdout.text(); - out = out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, ""); - out = out.replace(/(github:[^#]+)#[a-f0-9]+/, "$1"); - expect(out.split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ when@https://github.com/cujojs/when/tarball/1.0.2", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "when"]); - expect(await readdirSorted(join(package_dir, "node_modules", "when"))).toEqual([ - ".gitignore", - ".gitmodules", - "LICENSE.txt", - "README.md", - "apply.js", - "cancelable.js", - "delay.js", - "package.json", - "test", - "timed.js", - "timeout.js", - "when.js", - ]); - const package_json = await file(join(package_dir, "node_modules", "when", "package.json")).json(); - expect(package_json.name).toBe("when"); - await access(join(package_dir, "bun.lockb")); -}); - -it("should treat non-GitHub http(s) URLs as tarballs (https://some.url/path?stuff)", async () => { - const urls: string[] = []; - setHandler( - dummyRegistry(urls, { - "4.3.0": { as: "4.3.0" }, - }), - ); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - dependencies: { - "@vercel/turbopack-node": - "https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-node/js?turbopack-230922.2", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - let out = await stdout.text(); - out = out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, ""); - out = out.replace(/(github:[^#]+)#[a-f0-9]+/, "$1"); - expect(out.split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ @vercel/turbopack-node@https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-node/js?turbopack-230922.2", - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toHaveLength(2); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "@vercel", "loader-runner"]); - expect(await readdirSorted(join(package_dir, "node_modules", "@vercel"))).toEqual(["turbopack-node"]); - expect(await readdirSorted(join(package_dir, "node_modules", "@vercel", "turbopack-node"))).toEqual([ - "package.json", - "src", - "tsconfig.json", - ]); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle GitHub URL with existing lockfile", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "bunfig.toml"), - ` -[install] -cache = false -saveTextLockfile = false -`, - ); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - "html-minifier": "kangax/html-minifier#v4.0.0", - }, - }), - ); - const { - stdout: stdout1, - stderr: stderr1, - exited: exited1, - } = spawn({ - cmd: [bunExe(), "install", "--linker=hoisted"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err1 = await new Response(stderr1).text(); - expect(err1).toContain("Saved lockfile"); - const out1 = await new Response(stdout1).text(); - expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ html-minifier@github:kangax/html-minifier#4beb325", - "", - "12 packages installed", - ]); - expect(await exited1).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ - ".bin", - ".cache", - "camel-case", - "clean-css", - "commander", - "he", - "html-minifier", - "lower-case", - "no-case", - "param-case", - "relateurl", - "source-map", - "uglify-js", - "upper-case", - ]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins([ - "he", - "html-minifier", - "uglifyjs", - ]); - expect(join(package_dir, "node_modules", ".bin", "he")).toBeValidBin(join("..", "he", "bin", "he")); - expect(join(package_dir, "node_modules", ".bin", "html-minifier")).toBeValidBin( - join("..", "html-minifier", "cli.js"), - ); - expect(join(package_dir, "node_modules", ".bin", "uglifyjs")).toBeValidBin( - join("..", "uglify-js", "bin", "uglifyjs"), - ); - await access(join(package_dir, "bun.lockb")); - // Perform `bun install` again but with lockfile from before - await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); - urls.length = 0; - const { - stdout: stdout2, - stderr: stderr2, - exited: exited2, - } = spawn({ - cmd: [bunExe(), "install", "--linker=hoisted"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err2 = await new Response(stderr2).text(); - expect(err2).not.toContain("Saved lockfile"); - const out2 = await new Response(stdout2).text(); - expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ html-minifier@github:kangax/html-minifier#4beb325", - "", - "12 packages installed", - ]); - expect(await exited2).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ - ".bin", - ".cache", - "camel-case", - "clean-css", - "commander", - "he", - "html-minifier", - "lower-case", - "no-case", - "param-case", - "relateurl", - "source-map", - "uglify-js", - "upper-case", - ]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins([ - "he", - "html-minifier", - "uglifyjs", - ]); - expect(join(package_dir, "node_modules", ".bin", "he")).toBeValidBin(join("..", "he", "bin", "he")); - expect(join(package_dir, "node_modules", ".bin", "html-minifier")).toBeValidBin( - join("..", "html-minifier", "cli.js"), - ); - expect(join(package_dir, "node_modules", ".bin", "uglifyjs")).toBeValidBin( - join("..", "uglify-js", "bin", "uglifyjs"), - ); - await access(join(package_dir, "bun.lockb")); -}); - -it("should consider peerDependencies during hoisting", async () => { - const urls: string[] = []; - setHandler( - dummyRegistry(urls, { - "0.0.3": { - bin: { - "baz-run": "index.js", - }, - }, - "0.0.5": { - bin: { - "baz-exec": "index.js", - }, - }, - }), - ); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - peerDependencies: { - baz: ">0.0.3", - }, - workspaces: ["bar", "moo"], - }), - ); - await mkdir(join(package_dir, "bar")); - await writeFile( - join(package_dir, "bar", "package.json"), - JSON.stringify({ - name: "bar", - version: "0.0.2", - dependencies: { - baz: "0.0.3", - }, - }), - ); - await mkdir(join(package_dir, "moo")); - await writeFile( - join(package_dir, "moo", "package.json"), - JSON.stringify({ - name: "moo", - version: "0.0.4", - dependencies: { - baz: "0.0.5", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ baz@0.0.5", - "", - "4 packages installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.3.tgz`, `${root_url}/baz-0.0.5.tgz`]); - expect(requested).toBe(3); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "bar", "baz", "moo"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-exec"]); - expect(join(package_dir, "node_modules", ".bin", "baz-exec")).toBeValidBin(join("..", "baz", "index.js")); - expect(await readlink(join(package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "bar")); - expect(await readdirSorted(join(package_dir, "bar"))).toEqual(["node_modules", "package.json"]); - expect(await readdirSorted(join(package_dir, "bar", "node_modules"))).toEqual([".bin", "baz"]); - expect(join(package_dir, "bar", "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js")); - expect(await readdirSorted(join(package_dir, "bar", "node_modules", "baz"))).toEqual(["index.js", "package.json"]); - expect(await file(join(package_dir, "bar", "node_modules", "baz", "package.json")).json()).toEqual({ - name: "baz", - version: "0.0.3", - bin: { - "baz-run": "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.5", - bin: { - "baz-exec": "index.js", - }, - }); - expect(await readlink(join(package_dir, "node_modules", "moo"))).toBeWorkspaceLink(join("..", "moo")); - expect(await readdirSorted(join(package_dir, "moo"))).toEqual(["package.json"]); - await access(join(package_dir, "bun.lockb")); -}); - -it("should install peerDependencies when needed", async () => { - const urls: string[] = []; - setHandler( - dummyRegistry(urls, { - "0.0.3": { - bin: { - "baz-run": "index.js", - }, - }, - "0.0.5": { - bin: { - "baz-exec": "index.js", - }, - }, - }), - ); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - peerDependencies: { - baz: ">=0.0.3", - }, - workspaces: ["bar", "moo"], - }), - ); - await mkdir(join(package_dir, "bar")); - await writeFile( - join(package_dir, "bar", "package.json"), - JSON.stringify({ - name: "bar", - version: "0.0.2", - dependencies: { - baz: "0.0.3", - }, - }), - ); - await mkdir(join(package_dir, "moo")); - await writeFile( - join(package_dir, "moo", "package.json"), - JSON.stringify({ - name: "moo", - version: "0.0.4", - dependencies: { - baz: "0.0.5", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ baz@0.0.5", - "", - "4 packages installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.3.tgz`, `${root_url}/baz-0.0.5.tgz`]); - expect(requested).toBe(3); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "bar", "baz", "moo"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-exec"]); - expect(join(package_dir, "node_modules", ".bin", "baz-exec")).toBeValidBin(join("..", "baz", "index.js")); - expect(await readlink(join(package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "bar")); - expect(await readdirSorted(join(package_dir, "bar"))).toEqual(["node_modules", "package.json"]); - expect(join(package_dir, "bar", "node_modules", ".bin", "baz-run")).toBeValidBin(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.5", - bin: { - "baz-exec": "index.js", - }, - }); - expect(await readlink(join(package_dir, "node_modules", "moo"))).toBeWorkspaceLink(join("..", "moo")); - expect(await readdirSorted(join(package_dir, "moo"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "bar", "node_modules", "baz", "package.json")).json()).toEqual({ - name: "baz", - version: "0.0.3", - bin: { - "baz-run": "index.js", - }, - }); - await access(join(package_dir, "bun.lockb")); -}); - -it("should not regard peerDependencies declarations as duplicates", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - bar: "*", - }, - peerDependencies: { - bar: "^0.0.2", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@0.0.2", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - await access(join(package_dir, "bun.lockb")); -}); - -it("should report error on invalid format for package.json", async () => { - await writeFile(join(package_dir, "package.json"), "foo"); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err.replaceAll(joinP(package_dir + sep), "[dir]/").replaceAll(package_dir + sep, "[dir]/")).toMatchSnapshot(); - const out = await stdout.text(); - expect(out).toEqual(expect.stringContaining("bun install v1.")); - expect(await exited).toBe(1); -}); - -it("should report error on invalid format for dependencies", async () => { - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: [], - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err.replaceAll(joinP(package_dir + sep), "[dir]/")).toMatchSnapshot(); - const out = await stdout.text(); - expect(out).toEqual(expect.stringContaining("bun install v1.")); - expect(await exited).toBe(1); -}); - -it("should report error on invalid format for optionalDependencies", async () => { - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - optionalDependencies: "bar", - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - let err = await stderr.text(); - err = err.replaceAll(joinP(package_dir + sep), "[dir]/"); - err = err.substring(0, err.indexOf("\n", err.lastIndexOf("[dir]/package.json:"))).trim(); - expect(err.split("\n")).toEqual([ - `1 | {"name":"foo","version":"0.0.1","optionalDependencies":"bar"}`, - ` ^`, - `error: optionalDependencies expects a map of specifiers, e.g.`, - ` "optionalDependencies": {`, - ` "bun": "latest"`, - ` }`, - ` at [dir]/package.json:1:33`, - ]); - const out = await stdout.text(); - expect(out).toEqual(expect.stringContaining("bun install v1.")); - expect(await exited).toBe(1); -}); - -it("should report error on invalid format for workspaces", async () => { - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - workspaces: { - packages: { bar: true }, - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err.replaceAll(joinP(package_dir + sep), "[dir]/")).toMatchSnapshot(); - const out = await stdout.text(); - expect(out).toEqual(expect.stringContaining("bun install v1.")); - expect(await exited).toBe(1); -}); - -it("should report error on duplicated workspace packages", async () => { - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - workspaces: ["bar", "baz"], - }), - ); - await mkdir(join(package_dir, "bar")); - await writeFile( - join(package_dir, "bar", "package.json"), - JSON.stringify({ - name: "moo", - version: "0.0.2", - }), - ); - await mkdir(join(package_dir, "baz")); - await writeFile( - join(package_dir, "baz", "package.json"), - JSON.stringify({ - name: "moo", - version: "0.0.3", - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - let err = await stderr.text(); - err = err.replaceAll(package_dir, "[dir]"); - err = err.replaceAll(sep, "/"); - expect(err.trim().split("\n")).toEqual([ - `1 | {"name":"moo","version":"0.0.3"}`, - ` ^`, - `error: Workspace name "moo" already exists`, - ` at [dir]/baz/package.json:1:9`, - ``, - `1 | {"name":"moo","version":"0.0.2"}`, - ` ^`, - `note: Package name is also declared here`, - ` at [dir]/bar/package.json:1:9`, - ]); - const out = await stdout.text(); - expect(out).toEqual(expect.stringContaining("bun install v1.")); - expect(await exited).toBe(1); -}); - -it("should handle Git URL in dependencies", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - dependencies: { - "uglify-js": "git+https://git@github.com/mishoo/UglifyJS.git", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - let out = await stdout.text(); - out = out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, ""); - out = out.replace(/(\.git)#[a-f0-9]+/, "$1"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ uglify-js@git+https://git@github.com/mishoo/UglifyJS.git", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "uglify-js"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["uglifyjs"]); - expect(join(package_dir, "node_modules", ".bin", "uglifyjs")).toBeValidBin( - join("..", "uglify-js", "bin", "uglifyjs"), - ); - expect((await readdirSorted(join(package_dir, "node_modules", ".cache")))[0]).toBe("9694c5fe9c41ad51.git"); - expect(await readdirSorted(join(package_dir, "node_modules", "uglify-js"))).toEqual([ - ".bun-tag", - ".gitattributes", - ".github", - ".gitignore", - "CONTRIBUTING.md", - "LICENSE", - "README.md", - "bin", - "lib", - "package.json", - "test", - "tools", - ]); - const package_json = await file(join(package_dir, "node_modules", "uglify-js", "package.json")).json(); - expect(package_json.name).toBe("uglify-js"); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle Git URL in dependencies (SCP-style)", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - uglify: "github.com:mishoo/UglifyJS.git", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - let out = await stdout.text(); - out = out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, ""); - out = out.replace(/(\.git)#[a-f0-9]+/, "$1"); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ uglify@git+ssh://github.com:mishoo/UglifyJS.git", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "uglify"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["uglifyjs"]); - expect(join(package_dir, "node_modules", ".bin", "uglifyjs")).toBeValidBin(join("..", "uglify", "bin", "uglifyjs")); - expect((await readdirSorted(join(package_dir, "node_modules", ".cache")))[0]).toBe("87d55589eb4217d2.git"); - expect(await readdirSorted(join(package_dir, "node_modules", "uglify"))).toEqual([ - ".bun-tag", - ".gitattributes", - ".github", - ".gitignore", - "CONTRIBUTING.md", - "LICENSE", - "README.md", - "bin", - "lib", - "package.json", - "test", - "tools", - ]); - const package_json = await file(join(package_dir, "node_modules", "uglify", "package.json")).json(); - expect(package_json.name).toBe("uglify-js"); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle Git URL with committish in dependencies", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - dependencies: { - uglify: "git+https://git@github.com/mishoo/UglifyJS.git#v3.14.1", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ uglify@git+https://git@github.com/mishoo/UglifyJS.git#e219a9a78a0d2251e4dcbd4bb9034207eb484fe8", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "uglify"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["uglifyjs"]); - expect(join(package_dir, "node_modules", ".bin", "uglifyjs")).toBeValidBin(join("..", "uglify", "bin", "uglifyjs")); - expect(await readdirSorted(join(package_dir, "node_modules", ".cache"))).toEqual([ - "9694c5fe9c41ad51.git", - "@G@e219a9a78a0d2251e4dcbd4bb9034207eb484fe8", - ]); - expect(await readdirSorted(join(package_dir, "node_modules", "uglify"))).toEqual([ - ".bun-tag", - ".gitattributes", - ".github", - ".gitignore", - "CONTRIBUTING.md", - "LICENSE", - "README.md", - "bin", - "lib", - "package.json", - "test", - "tools", - ]); - const package_json = await file(join(package_dir, "node_modules", "uglify", "package.json")).json(); - expect(package_json.name).toBe("uglify-js"); - expect(package_json.version).toBe("3.14.1"); - await access(join(package_dir, "bun.lockb")); -}); - -it("should fail on invalid Git URL", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - dependencies: { - uglify: "git+http://bun.sh/no_such_repo", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err.split(/\r?\n/)).toContain("error: InstallFailed cloning repository for uglify"); - const out = await stdout.text(); - expect(out).toEqual(expect.stringContaining("bun install v1.")); - expect(await exited).toBe(1); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - try { - await access(join(package_dir, "bun.lockb")); - expect.unreachable(); - } catch (err: any) { - expect(err.code).toBe("ENOENT"); - } -}); - -it("should fail on ssh Git URL if invalid credentials", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - dependencies: { - "private-install": "git+ssh://git@bitbucket.org/kaizenmedia/private-install-test.git", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "ignore", - stderr: "pipe", - env: { ...env, "GIT_ASKPASS": "echo" }, - }); - const err = await stderr.text(); - expect(err.split(/\r?\n/)).toContain('error: "git clone" for "private-install" failed'); - const out = await stdout.text(); - expect(out).toEqual(expect.stringContaining("bun install v1.")); - expect(await exited).toBe(1); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - try { - await access(join(package_dir, "bun.lockb")); - expect.unreachable(); - } catch (err: any) { - expect(err.code).toBe("ENOENT"); - } -}); - -it("should fail on Git URL with invalid committish", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - dependencies: { - uglify: "git+https://git@github.com/mishoo/UglifyJS.git#404-no_such_tag", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err.split(/\r?\n/)).toContain( - 'error: no commit matching "404-no_such_tag" found for "uglify" (but repository exists)', - ); - const out = await stdout.text(); - expect(out).toEqual(expect.stringContaining("bun install v1.")); - expect(await exited).toBe(1); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - try { - await access(join(package_dir, "bun.lockb")); - expect.unreachable(); - } catch (err: any) { - expect(err.code).toBe("ENOENT"); - } -}); - -it("should de-duplicate committish in Git URLs", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "Foo", - version: "0.0.1", - dependencies: { - "uglify-ver": "git+https://git@github.com/mishoo/UglifyJS.git#v3.14.1", - "uglify-hash": "git+https://git@github.com/mishoo/UglifyJS.git#e219a9a", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ uglify-hash@git+https://git@github.com/mishoo/UglifyJS.git#e219a9a78a0d2251e4dcbd4bb9034207eb484fe8", - "+ uglify-ver@git+https://git@github.com/mishoo/UglifyJS.git#e219a9a78a0d2251e4dcbd4bb9034207eb484fe8", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ - ".bin", - ".cache", - "uglify-hash", - "uglify-ver", - ]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["uglifyjs"]); - expect(join(package_dir, "node_modules", ".bin", "uglifyjs")).toBeValidBin( - join("..", "uglify-hash", "bin", "uglifyjs"), - ); - expect(await readdirSorted(join(package_dir, "node_modules", ".cache"))).toEqual([ - "9694c5fe9c41ad51.git", - "@G@e219a9a78a0d2251e4dcbd4bb9034207eb484fe8", - ]); - expect(await readdirSorted(join(package_dir, "node_modules", "uglify-hash"))).toEqual([ - ".bun-tag", - ".gitattributes", - ".github", - ".gitignore", - "CONTRIBUTING.md", - "LICENSE", - "README.md", - "bin", - "lib", - "package.json", - "test", - "tools", - ]); - const hash_json = await file(join(package_dir, "node_modules", "uglify-hash", "package.json")).json(); - expect(hash_json.name).toBe("uglify-js"); - expect(hash_json.version).toBe("3.14.1"); - expect(await readdirSorted(join(package_dir, "node_modules", "uglify-ver"))).toEqual([ - ".bun-tag", - ".gitattributes", - ".github", - ".gitignore", - "CONTRIBUTING.md", - "LICENSE", - "README.md", - "bin", - "lib", - "package.json", - "test", - "tools", - ]); - const ver_json = await file(join(package_dir, "node_modules", "uglify-ver", "package.json")).json(); - expect(ver_json.name).toBe("uglify-js"); - expect(ver_json.version).toBe("3.14.1"); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle Git URL with existing lockfile", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "bunfig.toml"), - ` -[install] -cache = false -saveTextLockfile = false -`, - ); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - "html-minifier": "git+https://git@github.com/kangax/html-minifier#v4.0.0", - }, - }), - ); - const { - stdout: stdout1, - stderr: stderr1, - exited: exited1, - } = spawn({ - cmd: [bunExe(), "install", "--linker=hoisted"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err1 = await new Response(stderr1).text(); - expect(err1).toContain("Saved lockfile"); - const out1 = await new Response(stdout1).text(); - expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ html-minifier@git+https://git@github.com/kangax/html-minifier#4beb325eb01154a40c0cbebff2e5737bbd7071ab", - "", - "12 packages installed", - ]); - expect(await exited1).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ - ".bin", - ".cache", - "camel-case", - "clean-css", - "commander", - "he", - "html-minifier", - "lower-case", - "no-case", - "param-case", - "relateurl", - "source-map", - "uglify-js", - "upper-case", - ]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins([ - "he", - "html-minifier", - "uglifyjs", - ]); - expect(join(package_dir, "node_modules", ".bin", "he")).toBeValidBin(join("..", "he", "bin", "he")); - expect(join(package_dir, "node_modules", ".bin", "html-minifier")).toBeValidBin( - join("..", "html-minifier", "cli.js"), - ); - expect(join(package_dir, "node_modules", ".bin", "uglifyjs")).toBeValidBin( - join("..", "uglify-js", "bin", "uglifyjs"), - ); - await access(join(package_dir, "bun.lockb")); - // Perform `bun install` again but with lockfile from before - await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); - urls.length = 0; - const { - stdout: stdout2, - stderr: stderr2, - exited: exited2, - } = spawn({ - cmd: [bunExe(), "install", "--linker=hoisted"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err2 = await new Response(stderr2).text(); - expect(err2).not.toContain("Saved lockfile"); - const out2 = await new Response(stdout2).text(); - expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ html-minifier@git+https://git@github.com/kangax/html-minifier#4beb325eb01154a40c0cbebff2e5737bbd7071ab", - "", - "12 packages installed", - ]); - expect(await exited2).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ - ".bin", - ".cache", - "camel-case", - "clean-css", - "commander", - "he", - "html-minifier", - "lower-case", - "no-case", - "param-case", - "relateurl", - "source-map", - "uglify-js", - "upper-case", - ]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins([ - "he", - "html-minifier", - "uglifyjs", - ]); - expect(join(package_dir, "node_modules", ".bin", "he")).toBeValidBin(join("..", "he", "bin", "he")); - expect(join(package_dir, "node_modules", ".bin", "html-minifier")).toBeValidBin( - join("..", "html-minifier", "cli.js"), - ); - expect(join(package_dir, "node_modules", ".bin", "uglifyjs")).toBeValidBin( - join("..", "uglify-js", "bin", "uglifyjs"), - ); - await access(join(package_dir, "bun.lockb")); - // Perform `bun install` again but with cache & lockfile from before - await Promise.all( - [ - ".bin", - "camel-case", - "clean-css", - "commander", - "he", - "html-minifier", - "lower-case", - "no-case", - "param-case", - "relateurl", - "source-map", - "uglify-js", - "upper-case", - ].map(async dir => await rm(join(package_dir, "node_modules", dir), { force: true, recursive: true })), - ); - - urls.length = 0; - const { - stdout: stdout3, - stderr: stderr3, - exited: exited3, - } = spawn({ - cmd: [bunExe(), "install", "--linker=hoisted"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err3 = await new Response(stderr3).text(); - expect(err3).not.toContain("Saved lockfile"); - const out3 = await new Response(stdout3).text(); - expect(out3.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ html-minifier@git+https://git@github.com/kangax/html-minifier#4beb325eb01154a40c0cbebff2e5737bbd7071ab", - "", - "12 packages installed", - ]); - expect(await exited3).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ - ".bin", - ".cache", - "camel-case", - "clean-css", - "commander", - "he", - "html-minifier", - "lower-case", - "no-case", - "param-case", - "relateurl", - "source-map", - "uglify-js", - "upper-case", - ]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins([ - "he", - "html-minifier", - "uglifyjs", - ]); - expect(join(package_dir, "node_modules", ".bin", "he")).toBeValidBin(join("..", "he", "bin", "he")); - expect(join(package_dir, "node_modules", ".bin", "html-minifier")).toBeValidBin( - join("..", "html-minifier", "cli.js"), - ); - expect(join(package_dir, "node_modules", ".bin", "uglifyjs")).toBeValidBin( - join("..", "uglify-js", "bin", "uglifyjs"), - ); - await access(join(package_dir, "bun.lockb")); -}); - -it("should prefer optionalDependencies over dependencies of the same name", async () => { - const urls: string[] = []; - setHandler( - dummyRegistry(urls, { - "0.0.3": {}, - "0.0.5": {}, - }), - ); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - baz: "0.0.5", - }, - optionalDependencies: { - baz: "0.0.3", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ baz@0.0.3"), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.3.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "baz"]); - 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", - }, - }); -}); - -it("should prefer dependencies over peerDependencies of the same name", async () => { - const urls: string[] = []; - setHandler( - dummyRegistry(urls, { - "0.0.3": {}, - "0.0.5": {}, - }), - ); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - baz: "0.0.5", - }, - peerDependencies: { - baz: "0.0.3", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ baz@0.0.5", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.5.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "baz"]); - 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.5", - bin: { - "baz-exec": "index.js", - }, - }); -}); - -it("should handle tarball URL", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - baz: `${root_url}/baz-0.0.3.tgz`, - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ baz@${root_url}/baz-0.0.3.tgz`, - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/baz-0.0.3.tgz`]); - expect(requested).toBe(1); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "baz"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); - expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(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", - }, - }); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle tarball path", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - baz: join(import.meta.dir, "baz-0.0.3.tgz"), - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ baz@${join(import.meta.dir, "baz-0.0.3.tgz").replace(/\\/g, "/")}`, - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "baz"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); - expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(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", - }, - }); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle tarball URL with aliasing", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - bar: `${root_url}/baz-0.0.3.tgz`, - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ bar@${root_url}/baz-0.0.3.tgz`, - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/baz-0.0.3.tgz`]); - expect(requested).toBe(1); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "bar"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); - expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "bar", "index.js")); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["index.js", "package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "baz", - version: "0.0.3", - bin: { - "baz-run": "index.js", - }, - }); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle tarball path with aliasing", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - bar: join(import.meta.dir, "baz-0.0.3.tgz"), - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ bar@${join(import.meta.dir, "baz-0.0.3.tgz").replace(/\\/g, "/")}`, - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "bar"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); - expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "bar", "index.js")); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["index.js", "package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "baz", - version: "0.0.3", - bin: { - "baz-run": "index.js", - }, - }); - await access(join(package_dir, "bun.lockb")); -}); - -it("should de-duplicate dependencies alongside tarball URL", async () => { - const urls: string[] = []; - setHandler( - dummyRegistry(urls, { - "0.0.2": {}, - "0.0.3": { - bin: { - "baz-run": "index.js", - }, - }, - }), - ); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - "@barn/moo": `${root_url}/moo-0.1.0.tgz`, - bar: "<=0.0.2", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ @barn/moo@${root_url}/moo-0.1.0.tgz`, - expect.stringContaining("+ bar@0.0.2"), - "", - "3 packages installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([ - `${root_url}/bar`, - `${root_url}/bar-0.0.2.tgz`, - `${root_url}/baz`, - `${root_url}/baz-0.0.3.tgz`, - `${root_url}/moo-0.1.0.tgz`, - ]); - expect(requested).toBe(5); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "@barn", "bar", "baz"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); - expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js")); - expect(await readdirSorted(join(package_dir, "node_modules", "@barn"))).toEqual(["moo"]); - expect(await readdirSorted(join(package_dir, "node_modules", "@barn", "moo"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "@barn", "moo", "package.json")).json()).toEqual({ - name: "@barn/moo", - version: "0.1.0", - dependencies: { - bar: "0.0.2", - baz: "latest", - }, - }); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - 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", - }, - }); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle tarball URL with existing lockfile", async () => { - const urls: string[] = []; - setHandler( - dummyRegistry(urls, { - "0.0.2": {}, - "0.0.3": { - bin: { - "baz-run": "index.js", - }, - }, - }), - ); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - "@barn/moo": `${root_url}/moo-0.1.0.tgz`, - }, - }), - ); - const { - stdout: stdout1, - stderr: stderr1, - exited: exited1, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err1 = await new Response(stderr1).text(); - expect(err1).toContain("Saved lockfile"); - const out1 = await new Response(stdout1).text(); - expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ @barn/moo@${root_url}/moo-0.1.0.tgz`, - "", - "3 packages installed", - ]); - expect(await exited1).toBe(0); - expect(urls.sort()).toEqual([ - `${root_url}/bar`, - `${root_url}/bar-0.0.2.tgz`, - `${root_url}/baz`, - `${root_url}/baz-0.0.3.tgz`, - `${root_url}/moo-0.1.0.tgz`, - ]); - expect(requested).toBe(5); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "@barn", "bar", "baz"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); - expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js")); - expect(await readdirSorted(join(package_dir, "node_modules", "@barn"))).toEqual(["moo"]); - expect(await readdirSorted(join(package_dir, "node_modules", "@barn", "moo"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "@barn", "moo", "package.json")).json()).toEqual({ - name: "@barn/moo", - version: "0.1.0", - dependencies: { - bar: "0.0.2", - baz: "latest", - }, - }); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - 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", - }, - }); - await access(join(package_dir, "bun.lockb")); - // Perform `bun install` again but with lockfile from before - await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); - urls.length = 0; - const { - stdout: stdout2, - stderr: stderr2, - exited: exited2, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err2 = await new Response(stderr2).text(); - expect(err2).not.toContain("Saved lockfile"); - const out2 = await new Response(stdout2).text(); - expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ @barn/moo@${root_url}/moo-0.1.0.tgz`, - "", - "3 packages installed", - ]); - expect(await exited2).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar-0.0.2.tgz`, `${root_url}/baz-0.0.3.tgz`, `${root_url}/moo-0.1.0.tgz`]); - expect(requested).toBe(8); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "@barn", "bar", "baz"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); - expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js")); - expect(await readdirSorted(join(package_dir, "node_modules", "@barn"))).toEqual(["moo"]); - expect(await readdirSorted(join(package_dir, "node_modules", "@barn", "moo"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "@barn", "moo", "package.json")).json()).toEqual({ - name: "@barn/moo", - version: "0.1.0", - dependencies: { - bar: "0.0.2", - baz: "latest", - }, - }); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - 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", - }, - }); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle tarball path with existing lockfile", async () => { - const urls: string[] = []; - setHandler( - dummyRegistry(urls, { - "0.0.2": {}, - "0.0.3": { - bin: { - "baz-run": "index.js", - }, - }, - }), - ); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - "@barn/moo": join(import.meta.dir, "moo-0.1.0.tgz"), - }, - }), - ); - const { - stdout: stdout1, - stderr: stderr1, - exited: exited1, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err1 = await new Response(stderr1).text(); - expect(err1).toContain("Saved lockfile"); - const out1 = await new Response(stdout1).text(); - expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ @barn/moo@${join(import.meta.dir, "moo-0.1.0.tgz").replace(/\\/g, "/")}`, - "", - "3 packages installed", - ]); - expect(await exited1).toBe(0); - expect(urls.sort()).toEqual([ - `${root_url}/bar`, - `${root_url}/bar-0.0.2.tgz`, - `${root_url}/baz`, - `${root_url}/baz-0.0.3.tgz`, - ]); - expect(requested).toBe(4); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "@barn", "bar", "baz"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); - expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js")); - expect(await readdirSorted(join(package_dir, "node_modules", "@barn"))).toEqual(["moo"]); - expect(await readdirSorted(join(package_dir, "node_modules", "@barn", "moo"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "@barn", "moo", "package.json")).json()).toEqual({ - name: "@barn/moo", - version: "0.1.0", - dependencies: { - bar: "0.0.2", - baz: "latest", - }, - }); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - 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", - }, - }); - await access(join(package_dir, "bun.lockb")); - // Perform `bun install` again but with lockfile from before - await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); - urls.length = 0; - const { - stdout: stdout2, - stderr: stderr2, - exited: exited2, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err2 = await new Response(stderr2).text(); - expect(err2).not.toContain("Saved lockfile"); - const out2 = await new Response(stdout2).text(); - expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ @barn/moo@${join(import.meta.dir, "moo-0.1.0.tgz").replace(/\\/g, "/")}`, - "", - "3 packages installed", - ]); - expect(await exited2).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar-0.0.2.tgz`, `${root_url}/baz-0.0.3.tgz`]); - expect(requested).toBe(6); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".bin", ".cache", "@barn", "bar", "baz"]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); - expect(join(package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js")); - expect(await readdirSorted(join(package_dir, "node_modules", "@barn"))).toEqual(["moo"]); - expect(await readdirSorted(join(package_dir, "node_modules", "@barn", "moo"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "@barn", "moo", "package.json")).json()).toEqual({ - name: "@barn/moo", - version: "0.1.0", - dependencies: { - bar: "0.0.2", - baz: "latest", - }, - }); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - 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", - }, - }); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle devDependencies from folder", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.1.0", - dependencies: { - moo: "file:./moo", - }, - }), - ); - await mkdir(join(package_dir, "moo")); - const moo_package = JSON.stringify({ - name: "moo", - version: "0.2.0", - devDependencies: { - bar: "^0.0.2", - }, - }); - await writeFile(join(package_dir, "moo", "package.json"), moo_package); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ moo@moo", - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar", "moo"]); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - expect(await readdirSorted(join(package_dir, "node_modules", "moo"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "moo", "package.json")).text()).toEqual(moo_package); - await access(join(package_dir, "bun.lockb")); -}); - -it("should deduplicate devDependencies from folder", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.1.0", - devDependencies: { - bar: "^0.0.2", - moo: "file:./moo", - }, - }), - ); - await mkdir(join(package_dir, "moo")); - const moo_package = JSON.stringify({ - name: "moo", - version: "0.2.0", - devDependencies: { - bar: "^0.0.2", - }, - }); - await writeFile(join(package_dir, "moo", "package.json"), moo_package); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@0.0.2", - "+ moo@moo", - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar", "moo"]); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - expect(await readdirSorted(join(package_dir, "node_modules", "moo"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "moo", "package.json")).text()).toEqual(moo_package); - await access(join(package_dir, "bun.lockb")); -}); - -it("should install dependencies in root package of workspace", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.1.0", - workspaces: ["moo"], - }), - ); - await mkdir(join(package_dir, "moo")); - const moo_package = JSON.stringify({ - name: "moo", - version: "0.2.0", - dependencies: { - bar: "^0.0.2", - }, - }); - await writeFile(join(package_dir, "moo", "package.json"), moo_package); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(package_dir, "moo"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@0.0.2", - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar", "moo"]); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - expect(await readdirSorted(join(package_dir, "node_modules", "moo"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "moo", "package.json")).text()).toEqual(moo_package); - await access(join(package_dir, "bun.lockb")); -}); - -it("should install dependencies in root package of workspace (*)", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.1.0", - workspaces: ["*"], - }), - ); - await mkdir(join(package_dir, "moo")); - const moo_package = JSON.stringify({ - name: "moo", - version: "0.2.0", - dependencies: { - bar: "^0.0.2", - }, - }); - await writeFile(join(package_dir, "moo", "package.json"), moo_package); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(package_dir, "moo"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@0.0.2", - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar", "moo"]); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - expect(await readdirSorted(join(package_dir, "node_modules", "moo"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "moo", "package.json")).text()).toEqual(moo_package); - await access(join(package_dir, "bun.lockb")); -}); - -it("should ignore invalid workspaces from parent directory", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - const foo_package = JSON.stringify({ - name: "foo", - version: "0.1.0", - workspaces: ["moz"], - }); - await writeFile(join(package_dir, "package.json"), foo_package); - await mkdir(join(package_dir, "moo")); - await writeFile(join(package_dir, "moo", "bunfig.toml"), await file(join(package_dir, "bunfig.toml")).text()); - const moo_package = JSON.stringify({ - name: "moo", - version: "0.2.0", - dependencies: { - bar: "^0.0.2", - }, - }); - await writeFile(join(package_dir, "moo", "package.json"), moo_package); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(package_dir, "moo"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@0.0.2", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(package_dir)).toEqual(["bunfig.toml", "moo", "package.json"]); - expect(await file(join(package_dir, "package.json")).text()).toEqual(foo_package); - expect(await readdirSorted(join(package_dir, "moo"))).toEqual([ - "bun.lockb", - "bunfig.toml", - "node_modules", - "package.json", - ]); - expect(await file(join(package_dir, "moo", "package.json")).text()).toEqual(moo_package); - expect(await readdirSorted(join(package_dir, "moo", "node_modules"))).toEqual([".cache", "bar"]); - expect(await readdirSorted(join(package_dir, "moo", "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "moo", "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); -}); - -it("should handle --cwd", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - const foo_package = JSON.stringify({ - name: "foo", - version: "0.1.0", - }); - await writeFile(join(package_dir, "package.json"), foo_package); - await mkdir(join(package_dir, "moo")); - await writeFile(join(package_dir, "moo", "bunfig.toml"), await file(join(package_dir, "bunfig.toml")).text()); - const moo_package = JSON.stringify({ - name: "moo", - version: "0.2.0", - dependencies: { - bar: "^0.0.2", - }, - }); - await writeFile(join(package_dir, "moo", "package.json"), moo_package); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--cwd", "moo"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@0.0.2", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(package_dir)).toEqual(["bunfig.toml", "moo", "package.json"]); - expect(await file(join(package_dir, "package.json")).text()).toEqual(foo_package); - expect(await readdirSorted(join(package_dir, "moo"))).toEqual([ - "bun.lockb", - "bunfig.toml", - "node_modules", - "package.json", - ]); - expect(await file(join(package_dir, "moo", "package.json")).text()).toEqual(moo_package); - expect(await readdirSorted(join(package_dir, "moo", "node_modules"))).toEqual([".cache", "bar"]); - expect(await readdirSorted(join(package_dir, "moo", "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "moo", "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); -}); - -it("should handle --frozen-lockfile", async () => { - let urls: string[] = []; - setHandler(dummyRegistry(urls, { "0.0.3": { as: "0.0.3" }, "0.0.5": { as: "0.0.5" } })); - - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ name: "foo", version: "0.0.1", dependencies: { baz: "0.0.3" } }), - ); - - // save the lockfile once - expect( - await spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "ignore", - stdin: "ignore", - stderr: "ignore", - env, - }).exited, - ).toBe(0); - - // change version of baz in package.json - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { baz: "0.0.5" }, - }), - ); - - const { stderr, exited } = spawn({ - cmd: [bunExe(), "install", "--frozen-lockfile"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const err = await stderr.text(); - expect(err).toContain("error: lockfile had changes, but lockfile is frozen"); - expect(await exited).toBe(1); -}); - -it("should handle bun ci alias (to --frozen-lockfile)", async () => { - let urls: string[] = []; - setHandler(dummyRegistry(urls, { "0.0.3": { as: "0.0.3" }, "0.0.5": { as: "0.0.5" } })); - - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ name: "foo", version: "0.0.1", dependencies: { baz: "0.0.3" } }), - ); - - // save the lockfile once - expect( - await spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "ignore", - stdin: "ignore", - stderr: "ignore", - env, - }).exited, - ).toBe(0); - - // change version of baz in package.json - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { baz: "0.0.5" }, - }), - ); - - const { stderr: stderr1, exited: exited1 } = spawn({ - cmd: [bunExe(), "ci"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const err1 = await new Response(stderr1).text(); - expect(err1).toContain("error: lockfile had changes, but lockfile is frozen"); - expect(await exited1).toBe(1); - - // test that it works even if ci isn't first "arg" - const { stderr: stderr2, exited: exited2 } = spawn({ - cmd: [bunExe(), "--save", "ci"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const err2 = await new Response(stderr2).text(); - expect(err2).toContain("error: lockfile had changes, but lockfile is frozen"); - expect(await exited2).toBe(1); -}); - -it("should handle frozenLockfile in config file", async () => { - let urls: string[] = []; - setHandler(dummyRegistry(urls, { "0.0.3": { as: "0.0.3" }, "0.0.5": { as: "0.0.5" } })); - - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ name: "foo", version: "0.0.1", dependencies: { baz: "0.0.3" } }), - ); - - // save the lockfile once - expect( - await spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "ignore", - stdin: "ignore", - stderr: "ignore", - env, - }).exited, - ).toBe(0); - - await writeFile( - join(package_dir, "bunfig.toml"), - ` -[install] -frozenLockfile = true -registry = "${root_url}" -`, - ); - - // change version of baz in package.json - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { baz: "0.0.5" }, - }), - ); - - const { stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - - const err = await stderr.text(); - expect(err).toContain("error: lockfile had changes, but lockfile is frozen"); - expect(await exited).toBe(1); -}); - -it("should perform bin-linking across multiple dependencies", async () => { - const foo_package = JSON.stringify({ - name: "foo", - devDependencies: { - "conditional-type-checks": "1.0.6", - "prettier": "2.8.8", - "tsd": "0.22.0", - "typescript": "5.0.4", - }, - }); - await writeFile(join(package_dir, "package.json"), foo_package); - await cp(join(import.meta.dir, "bun.lockb.bin-linking"), join(package_dir, "bun.lockb")); - await writeFile( - join(package_dir, "bunfig.toml"), - ` -[install] -cache = false -`, - ); - const { - stdout: stdout1, - stderr: stderr1, - exited: exited1, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err1 = await new Response(stderr1).text(); - expect(err1).not.toContain("error:"); - const out1 = await new Response(stdout1).text(); - expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - `bun install ${Bun.version_with_sha}`, - "", - expect.stringContaining("+ conditional-type-checks@1.0.6"), - expect.stringContaining("+ prettier@2.8.8"), - expect.stringContaining("+ tsd@0.22.0"), - expect.stringContaining("+ typescript@5.0.4"), - "", - "112 packages installed", - ]); - expect(await exited1).toBe(0); - expect(await readdirSorted(package_dir)).toEqual(["bun.lockb", "bunfig.toml", "node_modules", "package.json"]); - expect(await file(join(package_dir, "package.json")).text()).toEqual(foo_package); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([ - ".bin", - ".cache", - "@babel", - "@nodelib", - "@tsd", - "@types", - "ansi-escapes", - "ansi-regex", - "ansi-styles", - "array-union", - "arrify", - "braces", - "camelcase", - "camelcase-keys", - "chalk", - "color-convert", - "color-name", - "conditional-type-checks", - "decamelize", - "decamelize-keys", - "dir-glob", - "emoji-regex", - "error-ex", - "eslint-formatter-pretty", - "eslint-rule-docs", - "fast-glob", - "fastq", - "fill-range", - "find-up", - "function-bind", - "glob-parent", - "globby", - "hard-rejection", - "has-flag", - "hasown", - "hosted-git-info", - "ignore", - "indent-string", - "irregular-plurals", - "is-arrayish", - "is-core-module", - "is-extglob", - "is-fullwidth-code-point", - "is-glob", - "is-number", - "is-plain-obj", - "is-unicode-supported", - "js-tokens", - "json-parse-even-better-errors", - "kind-of", - "lines-and-columns", - "locate-path", - "log-symbols", - "lru-cache", - "map-obj", - "meow", - "merge2", - "micromatch", - "min-indent", - "minimist-options", - "normalize-package-data", - "p-limit", - "p-locate", - "p-try", - "parse-json", - "path-exists", - "path-parse", - "path-type", - "picocolors", - "picomatch", - "plur", - "prettier", - "queue-microtask", - "quick-lru", - "read-pkg", - "read-pkg-up", - "redent", - "resolve", - "reusify", - "run-parallel", - "semver", - "slash", - "spdx-correct", - "spdx-exceptions", - "spdx-expression-parse", - "spdx-license-ids", - "string-width", - "strip-ansi", - "strip-indent", - "supports-color", - "supports-hyperlinks", - "supports-preserve-symlinks-flag", - "to-regex-range", - "trim-newlines", - "tsd", - "type-fest", - "typescript", - "validate-npm-package-license", - "yallist", - "yargs-parser", - ]); - expect(await readdirSorted(join(package_dir, "node_modules", ".bin"))).toHaveBins([ - "prettier", - "resolve", - "semver", - "tsc", - "tsd", - "tsserver", - ]); - // Perform `bun install --production` with lockfile from before - await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); - const { - stdout: stdout2, - stderr: stderr2, - exited: exited2, - } = spawn({ - cmd: [bunExe(), "install", "--production"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err2 = await new Response(stderr2).text(); - expect(err2).not.toContain("Saved lockfile"); - expect(err2).not.toContain("error:"); - const out2 = await new Response(stdout2).text(); - expect(out2.replace(/\[[0-9\.]+m?s\]/, "[]").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "[] done", - "", - ]); - expect(await exited2).toBe(0); - expect(await readdirSorted(package_dir)).toEqual(["bun.lockb", "bunfig.toml", "node_modules", "package.json"]); - expect(await file(join(package_dir, "package.json")).text()).toEqual(foo_package); - expect(await readdirSorted(join(package_dir, "node_modules"))).toBeEmpty(); -}); - -it("should handle trustedDependencies", async () => { - function getScripts(name: string) { - return { - preinstall: `echo preinstall ${name}`, - install: `echo install ${name}`, - postinstall: `echo postinstall ${name}`, - preprepare: `echo preprepare ${name}`, - prepare: `echo prepare ${name}`, - postprepare: `echo postprepare ${name}`, - }; - } - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.1.0", - dependencies: { - bar: "file:./bar", - moo: "file:./moo", - }, - trustedDependencies: ["moo"], - }), - ); - await mkdir(join(package_dir, "bar")); - const bar_package = JSON.stringify({ - name: "bar", - version: "0.2.0", - scripts: getScripts("bar"), - }); - await writeFile(join(package_dir, "bar", "package.json"), bar_package); - await mkdir(join(package_dir, "moo")); - const moo_package = JSON.stringify({ - name: "moo", - version: "0.3.0", - scripts: getScripts("moo"), - }); - await writeFile(join(package_dir, "moo", "package.json"), moo_package); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).not.toContain("error:"); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@bar", - "+ moo@moo", - "", - "2 packages installed", - "", - "Blocked 3 postinstalls. Run `bun pm untrusted` for details.", - "", - ]); - expect(await exited).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar", "moo"]); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); - expect(await readdirSorted(join(package_dir, "node_modules", "moo"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "moo", "package.json")).text()).toEqual(moo_package); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle `workspaces:*` and `workspace:*` gracefully", async () => { - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - workspaces: ["*"], - dependencies: { - bar: "workspace:*", - }, - }), - ); - await mkdir(join(package_dir, "bar")); - const bar_package = JSON.stringify({ - name: "bar", - version: "0.0.1", - }); - await writeFile(join(package_dir, "bar", "package.json"), bar_package); - const { - stdout: stdout1, - stderr: stderr1, - exited: exited1, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err1 = await new Response(stderr1).text(); - expect(err1).toContain("Saved lockfile"); - const out1 = await new Response(stdout1).text(); - expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@workspace:bar", - "", - "1 package installed", - ]); - expect(await exited1).toBe(0); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); - expect(await readlink(join(package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "bar")); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); - await access(join(package_dir, "bun.lockb")); - // Perform `bun install` again but with lockfile from before - await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); - const { - stdout: stdout2, - stderr: stderr2, - exited: exited2, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err2 = await new Response(stderr2).text(); - const out2 = await new Response(stdout2).text(); - expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@workspace:bar", - "", - "1 package installed", - ]); - expect(await exited2).toBe(0); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); - expect(await readlink(join(package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "bar")); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle `workspaces:bar` and `workspace:*` gracefully", async () => { - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - workspaces: ["bar"], - dependencies: { - bar: "workspace:*", - }, - }), - ); - await mkdir(join(package_dir, "bar")); - const bar_package = JSON.stringify({ - name: "bar", - version: "0.0.1", - }); - await writeFile(join(package_dir, "bar", "package.json"), bar_package); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@workspace:bar", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); - expect(await readlink(join(package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "bar")); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle `workspaces:*` and `workspace:bar` gracefully", async () => { - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - workspaces: ["*"], - dependencies: { - bar: "workspace:bar", - }, - }), - ); - await mkdir(join(package_dir, "bar")); - const bar_package = JSON.stringify({ - name: "bar", - version: "0.0.1", - }); - await writeFile(join(package_dir, "bar", "package.json"), bar_package); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@workspace:bar", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); - expect(await readlink(join(package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "bar")); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle `workspaces:bar` and `workspace:bar` gracefully", async () => { - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - workspaces: ["bar"], - dependencies: { - bar: "workspace:bar", - }, - }), - ); - await mkdir(join(package_dir, "bar")); - const bar_package = JSON.stringify({ - name: "bar", - version: "0.0.1", - }); - await writeFile(join(package_dir, "bar", "package.json"), bar_package); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@workspace:bar", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); - expect(await readlink(join(package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "bar")); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle installing packages from inside a workspace with `*`", async () => { - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "main", - workspaces: ["packages/*"], - private: true, - }), - ); - await mkdir(join(package_dir, "packages", "yolo"), { recursive: true }); - const yolo_package = JSON.stringify({ - name: "yolo", - version: "0.0.1", - dependencies: { - swag: "workspace:*", - }, - }); - await writeFile(join(package_dir, "packages", "yolo", "package.json"), yolo_package); - await mkdir(join(package_dir, "packages", "swag")); - const swag_package = JSON.stringify({ - name: "swag", - version: "0.0.1", - }); - await writeFile(join(package_dir, "packages", "swag", "package.json"), swag_package); - const { - stdout: stdout1, - stderr: stderr1, - exited: exited1, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(package_dir, "packages", "yolo"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err1 = await new Response(stderr1).text(); - expect(err1).toContain("Saved lockfile"); - const out1 = await new Response(stdout1).text(); - expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ swag@workspace:packages/swag`, - "", - "2 packages installed", - ]); - expect(await exited1).toBe(0); - expect(requested).toBe(0); - await access(join(package_dir, "bun.lockb")); - - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - - const { - stdout: stdout2, - stderr: stderr2, - exited: exited2, - } = spawn({ - cmd: [bunExe(), "install", "bar"], - cwd: join(package_dir, "packages", "yolo"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err2 = await new Response(stderr2).text(); - expect(err2).toContain("Saved lockfile"); - const out2 = await new Response(stdout2).text(); - expect(out2).toContain("installed bar"); - expect(await exited2).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); - await access(join(package_dir, "bun.lockb")); -}); - -it("should handle installing packages from inside a workspace without prefix", async () => { - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "main", - workspaces: ["packages/*"], - private: true, - }), - ); - await mkdir(join(package_dir, "packages", "p1"), { recursive: true }); - const p1_package = JSON.stringify({ - name: "p1", - version: "0.0.1", - dependencies: { - p2: "0.1.0", - }, - }); - await writeFile(join(package_dir, "packages", "p1", "package.json"), p1_package); - - await mkdir(join(package_dir, "packages", "p2")); - const p2_package = JSON.stringify({ - name: "p2", - version: "0.1.0", - }); - await writeFile(join(package_dir, "packages", "p2", "package.json"), p2_package); - - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - - const { - stdout: stdout1, - stderr: stderr1, - exited: exited1, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(package_dir, "packages", "p1"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err1 = await new Response(stderr1).text(); - expect(err1).toContain("Saved lockfile"); - const out1 = await new Response(stdout1).text(); - expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ p2@workspace:packages/p2`, - "", - "2 packages installed", - ]); - expect(await exited1).toBe(0); - expect(requested).toBe(0); - await access(join(package_dir, "bun.lockb")); - - const { - stdout: stdout2, - stderr: stderr2, - exited: exited2, - } = spawn({ - cmd: [bunExe(), "install", "bar"], - cwd: join(package_dir, "packages", "p1"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, }); - const err2 = await new Response(stderr2).text(); - expect(err2).toContain("Saved lockfile"); - const out2 = await new Response(stdout2).text(); - expect(out2).toContain("installed bar"); - expect(await exited2).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); - await access(join(package_dir, "bun.lockb")); -}); -it("should handle installing workspaces with more complicated globs", async () => { - const package_dir = tempDirWithFiles("complicated-glob", { - "package.json": JSON.stringify({ - name: "package3", - version: "0.0.1", - workspaces: ["packages/**/*"], - }), - "packages": { - "frontend": { - "package.json": JSON.stringify({ - name: "frontend", + it("should handle scoped alias on unscoped dependency", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", version: "0.0.1", dependencies: { - "types": "workspace:*", - "components": "workspace:*", + "@baz/bar": "npm:bar", + bar: "latest", }, }), - "components": { - "package.json": JSON.stringify({ - name: "components", - version: "0.0.1", - dependencies: { - "types": "workspace:*", - }, - }), - }, - }, - "backend": { - "package.json": JSON.stringify({ - name: "backend", - version: "0.0.1", - dependencies: { - "types": "workspace:*", - }, - }), - }, - "types": { - "package.json": JSON.stringify({ - name: "types", - version: "0.0.1", - dependencies: {}, - }), - }, - }, - }); - - const { stdout, stderr } = await Bun.$`${bunExe()} install`.env(env).cwd(package_dir).throws(true); - const err1 = stderr.toString(); - expect(err1).toContain("Saved lockfile"); - expect( - stdout - .toString() - .replace(/\s*\[[0-9\.]+m?s\]\s*$/, "") - .split(/\r?\n/), - ).toEqual([expect.stringContaining("bun install v1."), "", "Checked 7 installs across 5 packages (no changes)"]); -}); - -it("should handle installing workspaces with multiple glob patterns", async () => { - const package_dir = tempDirWithFiles("multi-glob", { - "package.json": JSON.stringify({ - name: "main", - version: "0.0.1", - workspaces: ["backend/**/*", "client/**/*", "types/**/*"], - }), - "backend": { - "server": { - "package.json": JSON.stringify({ - name: "server", - version: "0.0.1", - dependencies: { - "types": "workspace:*", - "db": "workspace:*", - }, - }), - }, - "db": { - "package.json": JSON.stringify({ - name: "db", - version: "0.0.1", - dependencies: { - "types": "workspace:*", - }, - }), - }, - }, - "client": { - "clientlib": { - "package.json": JSON.stringify({ - name: "clientlib", - version: "0.0.1", - dependencies: { - "types": "workspace:*", - }, - }), - }, - }, - "types": { - "types": { - "package.json": JSON.stringify({ - name: "types", - version: "0.0.1", - dependencies: {}, - }), - }, - }, - }); - - console.log("TEMPDIR", package_dir); - - const { stdout, stderr } = await Bun.$`${bunExe()} install`.env(env).cwd(package_dir).throws(true); - const err1 = stderr.toString(); - expect(err1).toContain("Saved lockfile"); - expect( - stdout - .toString() - .replace(/\s*\[[0-9\.]+m?s\]\s*$/, "") - .split(/\r?\n/), - ).toEqual([expect.stringContaining("bun install v1."), "", "Checked 7 installs across 5 packages (no changes)"]); -}); - -it.todo("should handle installing workspaces with absolute glob patterns", async () => { - const package_dir = tempDirWithFiles("absolute-glob", { - "package.json": base => - JSON.stringify({ - name: "package3", - version: "0.0.1", - workspaces: [join(base, "packages/**/*")], - }), - "packages": { - "frontend": { - "package.json": JSON.stringify({ - name: "frontend", - version: "0.0.1", - dependencies: { - "types": "workspace:*", - "components": "workspace:*", - }, - }), - "components": { - "package.json": JSON.stringify({ - name: "components", - version: "0.0.1", - dependencies: { - "types": "workspace:*", - }, - }), - }, - }, - "backend": { - "package.json": JSON.stringify({ - name: "backend", - version: "0.0.1", - dependencies: { - "types": "workspace:*", - }, - }), - }, - "types": { - "package.json": JSON.stringify({ - name: "types", - version: "0.0.1", - dependencies: {}, - }), - }, - }, - }); - console.log("TEMP DIR", package_dir); - - const { stdout, stderr } = await Bun.$`${bunExe()} install`.env(env).cwd(package_dir).throws(true); - const err1 = stderr.toString(); - expect(err1).toContain("Saved lockfile"); - expect( - stdout - .toString() - .replace(/\s*\[[0-9\.]+m?s\]\s*$/, "") - .split(/\r?\n/), - ).toEqual([expect.stringContaining("bun install v1."), "", "4 packages installed"]); -}); - -it("should handle installing packages inside workspaces with difference versions", async () => { - let package_jsons = [ - JSON.stringify({ - name: "main", - workspaces: ["packages/*"], - private: true, - }), - JSON.stringify({ - name: "main", - private: true, - workspaces: [ - "packages/package1", - "packages/package2", - "packages/package3", - "packages/package4", - "packages/package5", - ], - }), - ]; - await mkdir(join(package_dir, "packages", "package1"), { recursive: true }); - await mkdir(join(package_dir, "packages", "package2")); - await mkdir(join(package_dir, "packages", "package3")); - await mkdir(join(package_dir, "packages", "package4")); - await mkdir(join(package_dir, "packages", "package5")); - { - const package1 = JSON.stringify({ - name: "package1", - version: "0.0.2", - }); - await writeFile(join(package_dir, "packages", "package1", "package.json"), package1); - } - { - const package2 = JSON.stringify({ - name: "package2", - version: "0.0.1", - dependencies: { - package1: "workspace:*", - }, - }); - await writeFile(join(package_dir, "packages", "package2", "package.json"), package2); - } - { - const package3 = JSON.stringify({ - name: "package3", - version: "0.0.1", - dependencies: { - package1: "workspace:^", - }, - }); - await writeFile(join(package_dir, "packages", "package3", "package.json"), package3); - } - { - const package4 = JSON.stringify({ - name: "package4", - version: "0.0.1", - dependencies: { - package1: "workspace:../package1", - }, - }); - await writeFile(join(package_dir, "packages", "package4", "package.json"), package4); - } - { - const package5 = JSON.stringify({ - name: "package5", - version: "0.0.1", - dependencies: { - package1: "workspace:0.0.2", - }, - }); - await writeFile(join(package_dir, "packages", "package5", "package.json"), package5); - } - for (const package_json of package_jsons) { - await writeFile(join(package_dir, "package.json"), package_json); - - { - const package1 = JSON.stringify({ - name: "package1", + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ @baz/bar@0.0.2", + "+ bar@0.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`, `${ctx.registry_url}bar-0.0.2.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "@baz", "bar"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@baz"))).toEqual(["bar"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@baz", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "@baz", "bar", "package.json")).json()).toEqual({ + name: "bar", version: "0.0.2", }); - await writeFile(join(package_dir, "packages", "package1", "package.json"), package1); - } - { - const package2 = JSON.stringify({ - name: "package2", - version: "0.0.1", - dependencies: { - package1: "workspace:*", + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle aliased dependency with existing lockfile", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { + "0.0.2": {}, + "0.0.3": { + bin: { + "baz-run": "index.js", + }, + }, + "0.1.0": { + dependencies: { + bar: "0.0.2", + baz: "latest", + }, + }, + latest: "0.0.3", + }), + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + "moz": "npm:@barn/moo@0.1.0", + }, + }), + ); + const { + stdout: stdout1, + stderr: stderr1, + exited: exited1, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err1 = await new Response(stderr1).text(); + expect(err1).toContain("Saved lockfile"); + const out1 = await new Response(stdout1).text(); + expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ moz@0.1.0", + "", + "3 packages installed", + ]); + expect(await exited1).toBe(0); + expect(urls.sort()).toEqual([ + `${ctx.registry_url}@barn%2fmoo`, + `${ctx.registry_url}@barn/moo-0.1.0.tgz`, + `${ctx.registry_url}bar`, + `${ctx.registry_url}bar-0.0.2.tgz`, + `${ctx.registry_url}baz`, + `${ctx.registry_url}baz-0.0.3.tgz`, + ]); + expect(ctx.requested).toBe(6); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([ + ".bin", + ".cache", + "bar", + "baz", + "moz", + ]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", }, }); - await writeFile(join(package_dir, "packages", "package2", "package.json"), package2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "moz"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "moz", "package.json")).json()).toEqual({ + name: "@barn/moo", + version: "0.1.0", + dependencies: { + bar: "0.0.2", + baz: "latest", + }, + }); + await access(join(ctx.package_dir, "bun.lockb")); + // Perform `bun install` again but with lockfile from before + await rm(join(ctx.package_dir, "node_modules"), { force: true, recursive: true }); + urls.length = 0; + const { + stdout: stdout2, + stderr: stderr2, + exited: exited2, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err2 = await new Response(stderr2).text(); + expect(err2).not.toContain("Saved lockfile"); + const out2 = await new Response(stdout2).text(); + expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ moz@0.1.0", + "", + "3 packages installed", + ]); + expect(await exited2).toBe(0); + expect(urls.sort()).toEqual([ + `${ctx.registry_url}@barn/moo-0.1.0.tgz`, + `${ctx.registry_url}bar-0.0.2.tgz`, + `${ctx.registry_url}baz-0.0.3.tgz`, + ]); + expect(ctx.requested).toBe(9); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([ + ".bin", + ".cache", + "bar", + "baz", + "moz", + ]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "moz"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "moz", "package.json")).json()).toEqual({ + name: "@barn/moo", + version: "0.1.0", + dependencies: { + bar: "0.0.2", + baz: "latest", + }, + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle GitHub URL in dependencies (user/repo)", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + uglify: "mishoo/UglifyJS", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + let out = await stdout.text(); + out = out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, ""); + out = out.replace(/(github:[^#]+)#[a-f0-9]+/, "$1"); + expect(out.split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ uglify@github:mishoo/UglifyJS", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "uglify"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["uglifyjs"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "uglify"))).toEqual([ + ".bun-tag", + ".gitattributes", + ".github", + ".gitignore", + "CONTRIBUTING.md", + "LICENSE", + "README.md", + "bin", + "lib", + "package.json", + "test", + "tools", + ]); + const package_json = await file(join(ctx.package_dir, "node_modules", "uglify", "package.json")).json(); + expect(package_json.name).toBe("uglify-js"); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle GitHub URL in dependencies (user/repo#commit-id)", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + uglify: "mishoo/UglifyJS#e219a9a", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ uglify@github:mishoo/UglifyJS#e219a9a", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "uglify"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["uglifyjs"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".cache"))).toEqual([ + "@GH@mishoo-UglifyJS-e219a9a@@@1", + "uglify", + ]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".cache", "uglify"))).toEqual([ + "mishoo-UglifyJS-e219a9a@@@1", + ]); + expect( + resolve( + await readlink(join(ctx.package_dir, "node_modules", ".cache", "uglify", "mishoo-UglifyJS-e219a9a@@@1")), + ), + ).toBe(join(ctx.package_dir, "node_modules", ".cache", "@GH@mishoo-UglifyJS-e219a9a@@@1")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "uglify"))).toEqual([ + ".bun-tag", + ".gitattributes", + ".github", + ".gitignore", + "CONTRIBUTING.md", + "LICENSE", + "README.md", + "bin", + "lib", + "package.json", + "test", + "tools", + ]); + const package_json = await file(join(ctx.package_dir, "node_modules", "uglify", "package.json")).json(); + expect(package_json.name).toBe("uglify-js"); + expect(package_json.version).toBe("3.14.1"); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle GitHub URL in dependencies (user/repo#tag)", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + uglify: "mishoo/UglifyJS#v3.14.1", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ uglify@github:mishoo/UglifyJS#e219a9a", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "uglify"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["uglifyjs"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".cache"))).toEqual([ + "@GH@mishoo-UglifyJS-e219a9a@@@1", + "uglify", + ]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".cache", "uglify"))).toEqual([ + "mishoo-UglifyJS-e219a9a@@@1", + ]); + expect( + resolve( + await readlink(join(ctx.package_dir, "node_modules", ".cache", "uglify", "mishoo-UglifyJS-e219a9a@@@1")), + ), + ).toBe(join(ctx.package_dir, "node_modules", ".cache", "@GH@mishoo-UglifyJS-e219a9a@@@1")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "uglify"))).toEqual([ + ".bun-tag", + ".gitattributes", + ".github", + ".gitignore", + "CONTRIBUTING.md", + "LICENSE", + "README.md", + "bin", + "lib", + "package.json", + "test", + "tools", + ]); + const package_json = await file(join(ctx.package_dir, "node_modules", "uglify", "package.json")).json(); + expect(package_json.name).toBe("uglify-js"); + expect(package_json.version).toBe("3.14.1"); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + describe("should handle bitbucket git dependencies", () => { + const deps = [ + "bitbucket:dylan-conway/public-install-test", + "bitbucket.org:dylan-conway/public-install-test", + "bitbucket.com:dylan-conway/public-install-test", + "git@bitbucket.org:dylan-conway/public-install-test", + ]; + + for (const dep of deps) { + it(`install ${dep}`, async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + "public-install-test": dep, + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ public-install-test@git+ssh://${dep}#79265e2d9754c60b60f97cc8d859fb6da073b5d2`, + "", + expect.stringContaining("installed"), + ]); + expect(await exited).toBe(0); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it(`add ${dep}`, async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "add", dep], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + `installed publicinstalltest@git+ssh://${dep}#79265e2d9754c60b60f97cc8d859fb6da073b5d2`, + "", + expect.stringContaining("installed"), + ]); + expect(await exited).toBe(0); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); } - { - const package3 = JSON.stringify({ + }); + + describe("should handle gitlab git dependencies", () => { + const deps = ["gitlab:dylan-conway/public-install-test", "gitlab.com:dylan-conway/public-install-test"]; + + for (const dep of deps) { + it(`install ${dep}`, async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + "public-install-test": dep, + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ public-install-test@git+ssh://${dep}#93f3aa4ec9ca8a0bacc010776db48bfcd915c44c`, + "", + expect.stringContaining("installed"), + ]); + expect(await exited).toBe(0); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it(`add ${dep}`, async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + }), + ); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "add", dep], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun add v1."), + "", + `installed public-install-test@git+ssh://${dep}#93f3aa4ec9ca8a0bacc010776db48bfcd915c44c`, + "", + expect.stringContaining("installed"), + ]); + expect(await exited).toBe(0); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + } + }); + + it("should handle GitHub URL in dependencies (github:user/repo#tag)", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + uglify: "github:mishoo/UglifyJS#v3.14.1", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ uglify@github:mishoo/UglifyJS#e219a9a", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "uglify"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["uglifyjs"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "uglifyjs")).toBeValidBin( + join("..", "uglify", "bin", "uglifyjs"), + ); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".cache"))).toEqual([ + "@GH@mishoo-UglifyJS-e219a9a@@@1", + "uglify", + ]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".cache", "uglify"))).toEqual([ + "mishoo-UglifyJS-e219a9a@@@1", + ]); + expect( + resolve( + await readlink(join(ctx.package_dir, "node_modules", ".cache", "uglify", "mishoo-UglifyJS-e219a9a@@@1")), + ), + ).toBe(join(ctx.package_dir, "node_modules", ".cache", "@GH@mishoo-UglifyJS-e219a9a@@@1")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "uglify"))).toEqual([ + ".bun-tag", + ".gitattributes", + ".github", + ".gitignore", + "CONTRIBUTING.md", + "LICENSE", + "README.md", + "bin", + "lib", + "package.json", + "test", + "tools", + ]); + const package_json = await file(join(ctx.package_dir, "node_modules", "uglify", "package.json")).json(); + expect(package_json.name).toBe("uglify-js"); + expect(package_json.version).toBe("3.14.1"); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle GitHub URL in dependencies (https://github.com/user/repo.git)", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + uglify: "https://github.com/mishoo/UglifyJS.git", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + let out = await stdout.text(); + out = out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, ""); + out = out.replace(/(github:[^#]+)#[a-f0-9]+/, "$1"); + expect(out.split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ uglify@github:mishoo/UglifyJS", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "uglify"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["uglifyjs"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "uglify"))).toEqual([ + ".bun-tag", + ".gitattributes", + ".github", + ".gitignore", + "CONTRIBUTING.md", + "LICENSE", + "README.md", + "bin", + "lib", + "package.json", + "test", + "tools", + ]); + const package_json = await file(join(ctx.package_dir, "node_modules", "uglify", "package.json")).json(); + expect(package_json.name).toBe("uglify-js"); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle GitHub URL in dependencies (git://github.com/user/repo.git#commit)", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + uglify: "git://github.com/mishoo/UglifyJS.git#e219a9a", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ uglify@github:mishoo/UglifyJS#e219a9a", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "uglify"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["uglifyjs"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "uglifyjs")).toBeValidBin( + join("..", "uglify", "bin", "uglifyjs"), + ); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".cache"))).toEqual([ + "@GH@mishoo-UglifyJS-e219a9a@@@1", + "uglify", + ]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".cache", "uglify"))).toEqual([ + "mishoo-UglifyJS-e219a9a@@@1", + ]); + expect( + resolve( + await readlink(join(ctx.package_dir, "node_modules", ".cache", "uglify", "mishoo-UglifyJS-e219a9a@@@1")), + ), + ).toBe(join(ctx.package_dir, "node_modules", ".cache", "@GH@mishoo-UglifyJS-e219a9a@@@1")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "uglify"))).toEqual([ + ".bun-tag", + ".gitattributes", + ".github", + ".gitignore", + "CONTRIBUTING.md", + "LICENSE", + "README.md", + "bin", + "lib", + "package.json", + "test", + "tools", + ]); + const package_json = await file(join(ctx.package_dir, "node_modules", "uglify", "package.json")).json(); + expect(package_json.name).toBe("uglify-js"); + expect(package_json.version).toBe("3.14.1"); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle GitHub URL in dependencies (git+https://github.com/user/repo.git)", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + uglify: "git+https://github.com/mishoo/UglifyJS.git", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + let out = await stdout.text(); + out = out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, ""); + out = out.replace(/(github:[^#]+)#[a-f0-9]+/, "$1"); + expect(out.split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ uglify@github:mishoo/UglifyJS", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "uglify"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["uglifyjs"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "uglify"))).toEqual([ + ".bun-tag", + ".gitattributes", + ".github", + ".gitignore", + "CONTRIBUTING.md", + "LICENSE", + "README.md", + "bin", + "lib", + "package.json", + "test", + "tools", + ]); + const package_json = await file(join(ctx.package_dir, "node_modules", "uglify", "package.json")).json(); + expect(package_json.name).toBe("uglify-js"); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle GitHub tarball URL in dependencies (https://github.com/user/repo/tarball/ref)", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + when: "https://github.com/cujojs/when/tarball/1.0.2", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + let out = await stdout.text(); + out = out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, ""); + out = out.replace(/(github:[^#]+)#[a-f0-9]+/, "$1"); + expect(out.split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ when@https://github.com/cujojs/when/tarball/1.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "when"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "when"))).toEqual([ + ".gitignore", + ".gitmodules", + "LICENSE.txt", + "README.md", + "apply.js", + "cancelable.js", + "delay.js", + "package.json", + "test", + "timed.js", + "timeout.js", + "when.js", + ]); + const package_json = await file(join(ctx.package_dir, "node_modules", "when", "package.json")).json(); + expect(package_json.name).toBe("when"); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle GitHub tarball URL in dependencies (https://github.com/user/repo/tarball/ref) with custom GITHUB_API_URL", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + when: "https://github.com/cujojs/when/tarball/1.0.2", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env: { + ...env, + GITHUB_API_URL: "https://example.com/github/api", + }, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + let out = await stdout.text(); + out = out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, ""); + out = out.replace(/(github:[^#]+)#[a-f0-9]+/, "$1"); + expect(out.split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ when@https://github.com/cujojs/when/tarball/1.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "when"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "when"))).toEqual([ + ".gitignore", + ".gitmodules", + "LICENSE.txt", + "README.md", + "apply.js", + "cancelable.js", + "delay.js", + "package.json", + "test", + "timed.js", + "timeout.js", + "when.js", + ]); + const package_json = await file(join(ctx.package_dir, "node_modules", "when", "package.json")).json(); + expect(package_json.name).toBe("when"); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should treat non-GitHub http(s) URLs as tarballs (https://some.url/path?stuff)", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { + "4.3.0": { as: "4.3.0" }, + }), + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + "@vercel/turbopack-node": + "https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-node/js?turbopack-230922.2", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + let out = await stdout.text(); + out = out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, ""); + out = out.replace(/(github:[^#]+)#[a-f0-9]+/, "$1"); + expect(out.split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ @vercel/turbopack-node@https://gitpkg-fork.vercel.sh/vercel/turbo/crates/turbopack-node/js?turbopack-230922.2", + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toHaveLength(2); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([ + ".cache", + "@vercel", + "loader-runner", + ]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@vercel"))).toEqual(["turbopack-node"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@vercel", "turbopack-node"))).toEqual([ + "package.json", + "src", + "tsconfig.json", + ]); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle GitHub URL with existing lockfile", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "bunfig.toml"), + ` + [install] + cache = false + saveTextLockfile = false + `, + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + "html-minifier": "kangax/html-minifier#v4.0.0", + }, + }), + ); + const { + stdout: stdout1, + stderr: stderr1, + exited: exited1, + } = spawn({ + cmd: [bunExe(), "install", "--linker=hoisted"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err1 = await new Response(stderr1).text(); + expect(err1).toContain("Saved lockfile"); + const out1 = await new Response(stdout1).text(); + expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ html-minifier@github:kangax/html-minifier#4beb325", + "", + "12 packages installed", + ]); + expect(await exited1).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([ + ".bin", + ".cache", + "camel-case", + "clean-css", + "commander", + "he", + "html-minifier", + "lower-case", + "no-case", + "param-case", + "relateurl", + "source-map", + "uglify-js", + "upper-case", + ]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins([ + "he", + "html-minifier", + "uglifyjs", + ]); + expect(join(ctx.package_dir, "node_modules", ".bin", "he")).toBeValidBin(join("..", "he", "bin", "he")); + expect(join(ctx.package_dir, "node_modules", ".bin", "html-minifier")).toBeValidBin( + join("..", "html-minifier", "cli.js"), + ); + expect(join(ctx.package_dir, "node_modules", ".bin", "uglifyjs")).toBeValidBin( + join("..", "uglify-js", "bin", "uglifyjs"), + ); + await access(join(ctx.package_dir, "bun.lockb")); + // Perform `bun install` again but with lockfile from before + await rm(join(ctx.package_dir, "node_modules"), { force: true, recursive: true }); + urls.length = 0; + const { + stdout: stdout2, + stderr: stderr2, + exited: exited2, + } = spawn({ + cmd: [bunExe(), "install", "--linker=hoisted"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err2 = await new Response(stderr2).text(); + expect(err2).not.toContain("Saved lockfile"); + const out2 = await new Response(stdout2).text(); + expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ html-minifier@github:kangax/html-minifier#4beb325", + "", + "12 packages installed", + ]); + expect(await exited2).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([ + ".bin", + ".cache", + "camel-case", + "clean-css", + "commander", + "he", + "html-minifier", + "lower-case", + "no-case", + "param-case", + "relateurl", + "source-map", + "uglify-js", + "upper-case", + ]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins([ + "he", + "html-minifier", + "uglifyjs", + ]); + expect(join(ctx.package_dir, "node_modules", ".bin", "he")).toBeValidBin(join("..", "he", "bin", "he")); + expect(join(ctx.package_dir, "node_modules", ".bin", "html-minifier")).toBeValidBin( + join("..", "html-minifier", "cli.js"), + ); + expect(join(ctx.package_dir, "node_modules", ".bin", "uglifyjs")).toBeValidBin( + join("..", "uglify-js", "bin", "uglifyjs"), + ); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should consider peerDependencies during hoisting", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { + "0.0.3": { + bin: { + "baz-run": "index.js", + }, + }, + "0.0.5": { + bin: { + "baz-exec": "index.js", + }, + }, + }), + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + peerDependencies: { + baz: ">0.0.3", + }, + workspaces: ["bar", "moo"], + }), + ); + await mkdir(join(ctx.package_dir, "bar")); + await writeFile( + join(ctx.package_dir, "bar", "package.json"), + JSON.stringify({ + name: "bar", + version: "0.0.2", + dependencies: { + baz: "0.0.3", + }, + }), + ); + await mkdir(join(ctx.package_dir, "moo")); + await writeFile( + join(ctx.package_dir, "moo", "package.json"), + JSON.stringify({ + name: "moo", + version: "0.0.4", + dependencies: { + baz: "0.0.5", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ baz@0.0.5", + "", + "4 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([ + `${ctx.registry_url}baz`, + `${ctx.registry_url}baz-0.0.3.tgz`, + `${ctx.registry_url}baz-0.0.5.tgz`, + ]); + expect(ctx.requested).toBe(3); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([ + ".bin", + ".cache", + "bar", + "baz", + "moo", + ]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["baz-exec"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "baz-exec")).toBeValidBin(join("..", "baz", "index.js")); + expect(await readlink(join(ctx.package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "bar")); + expect(await readdirSorted(join(ctx.package_dir, "bar"))).toEqual(["node_modules", "package.json"]); + expect(await readdirSorted(join(ctx.package_dir, "bar", "node_modules"))).toEqual([".bin", "baz"]); + expect(join(ctx.package_dir, "bar", "node_modules", ".bin", "baz-run")).toBeValidBin( + join("..", "baz", "index.js"), + ); + expect(await readdirSorted(join(ctx.package_dir, "bar", "node_modules", "baz"))).toEqual([ + "index.js", + "package.json", + ]); + expect(await file(join(ctx.package_dir, "bar", "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.5", + bin: { + "baz-exec": "index.js", + }, + }); + expect(await readlink(join(ctx.package_dir, "node_modules", "moo"))).toBeWorkspaceLink(join("..", "moo")); + expect(await readdirSorted(join(ctx.package_dir, "moo"))).toEqual(["package.json"]); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should install peerDependencies when needed", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { + "0.0.3": { + bin: { + "baz-run": "index.js", + }, + }, + "0.0.5": { + bin: { + "baz-exec": "index.js", + }, + }, + }), + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + peerDependencies: { + baz: ">=0.0.3", + }, + workspaces: ["bar", "moo"], + }), + ); + await mkdir(join(ctx.package_dir, "bar")); + await writeFile( + join(ctx.package_dir, "bar", "package.json"), + JSON.stringify({ + name: "bar", + version: "0.0.2", + dependencies: { + baz: "0.0.3", + }, + }), + ); + await mkdir(join(ctx.package_dir, "moo")); + await writeFile( + join(ctx.package_dir, "moo", "package.json"), + JSON.stringify({ + name: "moo", + version: "0.0.4", + dependencies: { + baz: "0.0.5", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ baz@0.0.5", + "", + "4 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([ + `${ctx.registry_url}baz`, + `${ctx.registry_url}baz-0.0.3.tgz`, + `${ctx.registry_url}baz-0.0.5.tgz`, + ]); + expect(ctx.requested).toBe(3); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([ + ".bin", + ".cache", + "bar", + "baz", + "moo", + ]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["baz-exec"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "baz-exec")).toBeValidBin(join("..", "baz", "index.js")); + expect(await readlink(join(ctx.package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "bar")); + expect(await readdirSorted(join(ctx.package_dir, "bar"))).toEqual(["node_modules", "package.json"]); + expect(join(ctx.package_dir, "bar", "node_modules", ".bin", "baz-run")).toBeValidBin( + join("..", "baz", "index.js"), + ); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.5", + bin: { + "baz-exec": "index.js", + }, + }); + expect(await readlink(join(ctx.package_dir, "node_modules", "moo"))).toBeWorkspaceLink(join("..", "moo")); + expect(await readdirSorted(join(ctx.package_dir, "moo"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "bar", "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should not regard peerDependencies declarations as duplicates", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + bar: "*", + }, + peerDependencies: { + bar: "^0.0.2", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@0.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`, `${ctx.registry_url}bar-0.0.2.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + test.serial("should report error on invalid format for package.json", async () => { + await withContext(defaultOpts, async ctx => { + await writeFile(join(ctx.package_dir, "package.json"), "foo"); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect( + err.replaceAll(joinP(ctx.package_dir + sep), "[dir]/").replaceAll(ctx.package_dir + sep, "[dir]/"), + ).toMatchSnapshot(); + const out = await stdout.text(); + expect(out).toEqual(expect.stringContaining("bun install v1.")); + expect(await exited).toBe(1); + }); + }); + + test.serial("should report error on invalid format for dependencies", async () => { + await withContext(defaultOpts, async ctx => { + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: [], + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err.replaceAll(joinP(ctx.package_dir + sep), "[dir]/")).toMatchSnapshot(); + const out = await stdout.text(); + expect(out).toEqual(expect.stringContaining("bun install v1.")); + expect(await exited).toBe(1); + }); + }); + + it("should report error on invalid format for optionalDependencies", async () => { + await withContext(defaultOpts, async ctx => { + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + optionalDependencies: "bar", + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + let err = await stderr.text(); + err = err.replaceAll(joinP(ctx.package_dir + sep), "[dir]/"); + err = err.substring(0, err.indexOf("\n", err.lastIndexOf("[dir]/package.json:"))).trim(); + expect(err.split("\n")).toEqual([ + `1 | {"name":"foo","version":"0.0.1","optionalDependencies":"bar"}`, + ` ^`, + `error: optionalDependencies expects a map of specifiers, e.g.`, + ` "optionalDependencies": {`, + ` "bun": "latest"`, + ` }`, + ` at [dir]/package.json:1:33`, + ]); + const out = await stdout.text(); + expect(out).toEqual(expect.stringContaining("bun install v1.")); + expect(await exited).toBe(1); + }); + }); + + test.serial("should report error on invalid format for workspaces", async () => { + await withContext(defaultOpts, async ctx => { + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + workspaces: { + packages: { bar: true }, + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err.replaceAll(joinP(ctx.package_dir + sep), "[dir]/")).toMatchSnapshot(); + const out = await stdout.text(); + expect(out).toEqual(expect.stringContaining("bun install v1.")); + expect(await exited).toBe(1); + }); + }); + + it("should report error on duplicated workspace packages", async () => { + await withContext(defaultOpts, async ctx => { + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + workspaces: ["bar", "baz"], + }), + ); + await mkdir(join(ctx.package_dir, "bar")); + await writeFile( + join(ctx.package_dir, "bar", "package.json"), + JSON.stringify({ + name: "moo", + version: "0.0.2", + }), + ); + await mkdir(join(ctx.package_dir, "baz")); + await writeFile( + join(ctx.package_dir, "baz", "package.json"), + JSON.stringify({ + name: "moo", + version: "0.0.3", + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + let err = await stderr.text(); + err = err.replaceAll(ctx.package_dir, "[dir]"); + err = err.replaceAll(sep, "/"); + expect(err.trim().split("\n")).toEqual([ + `1 | {"name":"moo","version":"0.0.3"}`, + ` ^`, + `error: Workspace name "moo" already exists`, + ` at [dir]/baz/package.json:1:9`, + ``, + `1 | {"name":"moo","version":"0.0.2"}`, + ` ^`, + `note: Package name is also declared here`, + ` at [dir]/bar/package.json:1:9`, + ]); + const out = await stdout.text(); + expect(out).toEqual(expect.stringContaining("bun install v1.")); + expect(await exited).toBe(1); + }); + }); + + it("should handle Git URL in dependencies", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + "uglify-js": "git+https://git@github.com/mishoo/UglifyJS.git", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + let out = await stdout.text(); + out = out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, ""); + out = out.replace(/(\.git)#[a-f0-9]+/, "$1"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ uglify-js@git+https://git@github.com/mishoo/UglifyJS.git", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "uglify-js"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["uglifyjs"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "uglifyjs")).toBeValidBin( + join("..", "uglify-js", "bin", "uglifyjs"), + ); + expect((await readdirSorted(join(ctx.package_dir, "node_modules", ".cache")))[0]).toBe("9694c5fe9c41ad51.git"); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "uglify-js"))).toEqual([ + ".bun-tag", + ".gitattributes", + ".github", + ".gitignore", + "CONTRIBUTING.md", + "LICENSE", + "README.md", + "bin", + "lib", + "package.json", + "test", + "tools", + ]); + const package_json = await file(join(ctx.package_dir, "node_modules", "uglify-js", "package.json")).json(); + expect(package_json.name).toBe("uglify-js"); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle Git URL in dependencies (SCP-style)", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + uglify: "github.com:mishoo/UglifyJS.git", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + let out = await stdout.text(); + out = out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, ""); + out = out.replace(/(\.git)#[a-f0-9]+/, "$1"); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ uglify@git+ssh://github.com:mishoo/UglifyJS.git", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "uglify"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["uglifyjs"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "uglifyjs")).toBeValidBin( + join("..", "uglify", "bin", "uglifyjs"), + ); + expect((await readdirSorted(join(ctx.package_dir, "node_modules", ".cache")))[0]).toBe("87d55589eb4217d2.git"); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "uglify"))).toEqual([ + ".bun-tag", + ".gitattributes", + ".github", + ".gitignore", + "CONTRIBUTING.md", + "LICENSE", + "README.md", + "bin", + "lib", + "package.json", + "test", + "tools", + ]); + const package_json = await file(join(ctx.package_dir, "node_modules", "uglify", "package.json")).json(); + expect(package_json.name).toBe("uglify-js"); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle Git URL with committish in dependencies", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + uglify: "git+https://git@github.com/mishoo/UglifyJS.git#v3.14.1", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ uglify@git+https://git@github.com/mishoo/UglifyJS.git#e219a9a78a0d2251e4dcbd4bb9034207eb484fe8", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "uglify"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["uglifyjs"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "uglifyjs")).toBeValidBin( + join("..", "uglify", "bin", "uglifyjs"), + ); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".cache"))).toEqual([ + "9694c5fe9c41ad51.git", + "@G@e219a9a78a0d2251e4dcbd4bb9034207eb484fe8", + ]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "uglify"))).toEqual([ + ".bun-tag", + ".gitattributes", + ".github", + ".gitignore", + "CONTRIBUTING.md", + "LICENSE", + "README.md", + "bin", + "lib", + "package.json", + "test", + "tools", + ]); + const package_json = await file(join(ctx.package_dir, "node_modules", "uglify", "package.json")).json(); + expect(package_json.name).toBe("uglify-js"); + expect(package_json.version).toBe("3.14.1"); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should fail on invalid Git URL", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + uglify: "git+http://bun.sh/no_such_repo", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err.split(/\r?\n/)).toContain("error: InstallFailed cloning repository for uglify"); + const out = await stdout.text(); + expect(out).toEqual(expect.stringContaining("bun install v1.")); + expect(await exited).toBe(1); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + try { + await access(join(ctx.package_dir, "bun.lockb")); + expect.unreachable(); + } catch (err: any) { + expect(err.code).toBe("ENOENT"); + } + }); + }); + + it("should fail on ssh Git URL if invalid credentials", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + "private-install": "git+ssh://git@bitbucket.org/kaizenmedia/private-install-test.git", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "ignore", + stderr: "pipe", + env: { ...env, "GIT_ASKPASS": "echo" }, + }); + const err = await stderr.text(); + expect(err.split(/\r?\n/)).toContain('error: "git clone" for "private-install" failed'); + const out = await stdout.text(); + expect(out).toEqual(expect.stringContaining("bun install v1.")); + expect(await exited).toBe(1); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + try { + await access(join(ctx.package_dir, "bun.lockb")); + expect.unreachable(); + } catch (err: any) { + expect(err.code).toBe("ENOENT"); + } + }); + }); + + it("should fail on Git URL with invalid committish", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + uglify: "git+https://git@github.com/mishoo/UglifyJS.git#404-no_such_tag", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err.split(/\r?\n/)).toContain( + 'error: no commit matching "404-no_such_tag" found for "uglify" (but repository exists)', + ); + const out = await stdout.text(); + expect(out).toEqual(expect.stringContaining("bun install v1.")); + expect(await exited).toBe(1); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + try { + await access(join(ctx.package_dir, "bun.lockb")); + expect.unreachable(); + } catch (err: any) { + expect(err.code).toBe("ENOENT"); + } + }); + }); + + it("should de-duplicate committish in Git URLs", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "Foo", + version: "0.0.1", + dependencies: { + "uglify-ver": "git+https://git@github.com/mishoo/UglifyJS.git#v3.14.1", + "uglify-hash": "git+https://git@github.com/mishoo/UglifyJS.git#e219a9a", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ uglify-hash@git+https://git@github.com/mishoo/UglifyJS.git#e219a9a78a0d2251e4dcbd4bb9034207eb484fe8", + "+ uglify-ver@git+https://git@github.com/mishoo/UglifyJS.git#e219a9a78a0d2251e4dcbd4bb9034207eb484fe8", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([ + ".bin", + ".cache", + "uglify-hash", + "uglify-ver", + ]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["uglifyjs"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "uglifyjs")).toBeValidBin( + join("..", "uglify-hash", "bin", "uglifyjs"), + ); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".cache"))).toEqual([ + "9694c5fe9c41ad51.git", + "@G@e219a9a78a0d2251e4dcbd4bb9034207eb484fe8", + ]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "uglify-hash"))).toEqual([ + ".bun-tag", + ".gitattributes", + ".github", + ".gitignore", + "CONTRIBUTING.md", + "LICENSE", + "README.md", + "bin", + "lib", + "package.json", + "test", + "tools", + ]); + const hash_json = await file(join(ctx.package_dir, "node_modules", "uglify-hash", "package.json")).json(); + expect(hash_json.name).toBe("uglify-js"); + expect(hash_json.version).toBe("3.14.1"); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "uglify-ver"))).toEqual([ + ".bun-tag", + ".gitattributes", + ".github", + ".gitignore", + "CONTRIBUTING.md", + "LICENSE", + "README.md", + "bin", + "lib", + "package.json", + "test", + "tools", + ]); + const ver_json = await file(join(ctx.package_dir, "node_modules", "uglify-ver", "package.json")).json(); + expect(ver_json.name).toBe("uglify-js"); + expect(ver_json.version).toBe("3.14.1"); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle Git URL with existing lockfile", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "bunfig.toml"), + ` + [install] + cache = false + saveTextLockfile = false + `, + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + "html-minifier": "git+https://git@github.com/kangax/html-minifier#v4.0.0", + }, + }), + ); + const { + stdout: stdout1, + stderr: stderr1, + exited: exited1, + } = spawn({ + cmd: [bunExe(), "install", "--linker=hoisted"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err1 = await new Response(stderr1).text(); + expect(err1).toContain("Saved lockfile"); + const out1 = await new Response(stdout1).text(); + expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ html-minifier@git+https://git@github.com/kangax/html-minifier#4beb325eb01154a40c0cbebff2e5737bbd7071ab", + "", + "12 packages installed", + ]); + expect(await exited1).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([ + ".bin", + ".cache", + "camel-case", + "clean-css", + "commander", + "he", + "html-minifier", + "lower-case", + "no-case", + "param-case", + "relateurl", + "source-map", + "uglify-js", + "upper-case", + ]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins([ + "he", + "html-minifier", + "uglifyjs", + ]); + expect(join(ctx.package_dir, "node_modules", ".bin", "he")).toBeValidBin(join("..", "he", "bin", "he")); + expect(join(ctx.package_dir, "node_modules", ".bin", "html-minifier")).toBeValidBin( + join("..", "html-minifier", "cli.js"), + ); + expect(join(ctx.package_dir, "node_modules", ".bin", "uglifyjs")).toBeValidBin( + join("..", "uglify-js", "bin", "uglifyjs"), + ); + await access(join(ctx.package_dir, "bun.lockb")); + // Perform `bun install` again but with lockfile from before + await rm(join(ctx.package_dir, "node_modules"), { force: true, recursive: true }); + urls.length = 0; + const { + stdout: stdout2, + stderr: stderr2, + exited: exited2, + } = spawn({ + cmd: [bunExe(), "install", "--linker=hoisted"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err2 = await new Response(stderr2).text(); + expect(err2).not.toContain("Saved lockfile"); + const out2 = await new Response(stdout2).text(); + expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ html-minifier@git+https://git@github.com/kangax/html-minifier#4beb325eb01154a40c0cbebff2e5737bbd7071ab", + "", + "12 packages installed", + ]); + expect(await exited2).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([ + ".bin", + ".cache", + "camel-case", + "clean-css", + "commander", + "he", + "html-minifier", + "lower-case", + "no-case", + "param-case", + "relateurl", + "source-map", + "uglify-js", + "upper-case", + ]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins([ + "he", + "html-minifier", + "uglifyjs", + ]); + expect(join(ctx.package_dir, "node_modules", ".bin", "he")).toBeValidBin(join("..", "he", "bin", "he")); + expect(join(ctx.package_dir, "node_modules", ".bin", "html-minifier")).toBeValidBin( + join("..", "html-minifier", "cli.js"), + ); + expect(join(ctx.package_dir, "node_modules", ".bin", "uglifyjs")).toBeValidBin( + join("..", "uglify-js", "bin", "uglifyjs"), + ); + await access(join(ctx.package_dir, "bun.lockb")); + // Perform `bun install` again but with cache & lockfile from before + await Promise.all( + [ + ".bin", + "camel-case", + "clean-css", + "commander", + "he", + "html-minifier", + "lower-case", + "no-case", + "param-case", + "relateurl", + "source-map", + "uglify-js", + "upper-case", + ].map(async dir => await rm(join(ctx.package_dir, "node_modules", dir), { force: true, recursive: true })), + ); + + urls.length = 0; + const { + stdout: stdout3, + stderr: stderr3, + exited: exited3, + } = spawn({ + cmd: [bunExe(), "install", "--linker=hoisted"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err3 = await new Response(stderr3).text(); + expect(err3).not.toContain("Saved lockfile"); + const out3 = await new Response(stdout3).text(); + expect(out3.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ html-minifier@git+https://git@github.com/kangax/html-minifier#4beb325eb01154a40c0cbebff2e5737bbd7071ab", + "", + "12 packages installed", + ]); + expect(await exited3).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([ + ".bin", + ".cache", + "camel-case", + "clean-css", + "commander", + "he", + "html-minifier", + "lower-case", + "no-case", + "param-case", + "relateurl", + "source-map", + "uglify-js", + "upper-case", + ]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins([ + "he", + "html-minifier", + "uglifyjs", + ]); + expect(join(ctx.package_dir, "node_modules", ".bin", "he")).toBeValidBin(join("..", "he", "bin", "he")); + expect(join(ctx.package_dir, "node_modules", ".bin", "html-minifier")).toBeValidBin( + join("..", "html-minifier", "cli.js"), + ); + expect(join(ctx.package_dir, "node_modules", ".bin", "uglifyjs")).toBeValidBin( + join("..", "uglify-js", "bin", "uglifyjs"), + ); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should prefer optionalDependencies over dependencies of the same name", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { + "0.0.3": {}, + "0.0.5": {}, + }), + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + baz: "0.0.5", + }, + optionalDependencies: { + baz: "0.0.3", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ baz@0.0.3"), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}baz`, `${ctx.registry_url}baz-0.0.3.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "baz"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + }); + }); + + it("should prefer dependencies over peerDependencies of the same name", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { + "0.0.3": {}, + "0.0.5": {}, + }), + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + baz: "0.0.5", + }, + peerDependencies: { + baz: "0.0.3", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ baz@0.0.5", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}baz`, `${ctx.registry_url}baz-0.0.5.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "baz"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.5", + bin: { + "baz-exec": "index.js", + }, + }); + }); + }); + + it("should handle tarball URL", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + baz: `${ctx.registry_url}baz-0.0.3.tgz`, + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ baz@${ctx.registry_url}baz-0.0.3.tgz`, + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}baz-0.0.3.tgz`]); + expect(ctx.requested).toBe(1); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "baz"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle tarball path", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + baz: join(import.meta.dir, "baz-0.0.3.tgz"), + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ baz@${join(import.meta.dir, "baz-0.0.3.tgz").replace(/\\/g, "/")}`, + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "baz"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle tarball URL with aliasing", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + bar: `${ctx.registry_url}baz-0.0.3.tgz`, + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ bar@${ctx.registry_url}baz-0.0.3.tgz`, + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}baz-0.0.3.tgz`]); + expect(ctx.requested).toBe(1); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "bar"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "bar", "index.js")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle tarball path with aliasing", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + bar: join(import.meta.dir, "baz-0.0.3.tgz"), + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ bar@${join(import.meta.dir, "baz-0.0.3.tgz").replace(/\\/g, "/")}`, + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".bin", ".cache", "bar"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "bar", "index.js")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should de-duplicate dependencies alongside tarball URL", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { + "0.0.2": {}, + "0.0.3": { + bin: { + "baz-run": "index.js", + }, + }, + }), + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + "@barn/moo": `${ctx.registry_url}moo-0.1.0.tgz`, + bar: "<=0.0.2", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ @barn/moo@${ctx.registry_url}moo-0.1.0.tgz`, + expect.stringContaining("+ bar@0.0.2"), + "", + "3 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([ + `${ctx.registry_url}bar`, + `${ctx.registry_url}bar-0.0.2.tgz`, + `${ctx.registry_url}baz`, + `${ctx.registry_url}baz-0.0.3.tgz`, + `${ctx.registry_url}moo-0.1.0.tgz`, + ]); + expect(ctx.requested).toBe(5); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([ + ".bin", + ".cache", + "@barn", + "bar", + "baz", + ]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@barn"))).toEqual(["moo"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@barn", "moo"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "@barn", "moo", "package.json")).json()).toEqual({ + name: "@barn/moo", + version: "0.1.0", + dependencies: { + bar: "0.0.2", + baz: "latest", + }, + }); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle tarball URL with existing lockfile", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { + "0.0.2": {}, + "0.0.3": { + bin: { + "baz-run": "index.js", + }, + }, + }), + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + "@barn/moo": `${ctx.registry_url}moo-0.1.0.tgz`, + }, + }), + ); + const { + stdout: stdout1, + stderr: stderr1, + exited: exited1, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err1 = await new Response(stderr1).text(); + expect(err1).toContain("Saved lockfile"); + const out1 = await new Response(stdout1).text(); + expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ @barn/moo@${ctx.registry_url}moo-0.1.0.tgz`, + "", + "3 packages installed", + ]); + expect(await exited1).toBe(0); + expect(urls.sort()).toEqual([ + `${ctx.registry_url}bar`, + `${ctx.registry_url}bar-0.0.2.tgz`, + `${ctx.registry_url}baz`, + `${ctx.registry_url}baz-0.0.3.tgz`, + `${ctx.registry_url}moo-0.1.0.tgz`, + ]); + expect(ctx.requested).toBe(5); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([ + ".bin", + ".cache", + "@barn", + "bar", + "baz", + ]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@barn"))).toEqual(["moo"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@barn", "moo"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "@barn", "moo", "package.json")).json()).toEqual({ + name: "@barn/moo", + version: "0.1.0", + dependencies: { + bar: "0.0.2", + baz: "latest", + }, + }); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + await access(join(ctx.package_dir, "bun.lockb")); + // Perform `bun install` again but with lockfile from before + await rm(join(ctx.package_dir, "node_modules"), { force: true, recursive: true }); + urls.length = 0; + const { + stdout: stdout2, + stderr: stderr2, + exited: exited2, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err2 = await new Response(stderr2).text(); + expect(err2).not.toContain("Saved lockfile"); + const out2 = await new Response(stdout2).text(); + expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ @barn/moo@${ctx.registry_url}moo-0.1.0.tgz`, + "", + "3 packages installed", + ]); + expect(await exited2).toBe(0); + expect(urls.sort()).toEqual([ + `${ctx.registry_url}bar-0.0.2.tgz`, + `${ctx.registry_url}baz-0.0.3.tgz`, + `${ctx.registry_url}moo-0.1.0.tgz`, + ]); + expect(ctx.requested).toBe(8); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([ + ".bin", + ".cache", + "@barn", + "bar", + "baz", + ]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@barn"))).toEqual(["moo"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@barn", "moo"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "@barn", "moo", "package.json")).json()).toEqual({ + name: "@barn/moo", + version: "0.1.0", + dependencies: { + bar: "0.0.2", + baz: "latest", + }, + }); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle tarball path with existing lockfile", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { + "0.0.2": {}, + "0.0.3": { + bin: { + "baz-run": "index.js", + }, + }, + }), + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + "@barn/moo": join(import.meta.dir, "moo-0.1.0.tgz"), + }, + }), + ); + const { + stdout: stdout1, + stderr: stderr1, + exited: exited1, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err1 = await new Response(stderr1).text(); + expect(err1).toContain("Saved lockfile"); + const out1 = await new Response(stdout1).text(); + expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ @barn/moo@${join(import.meta.dir, "moo-0.1.0.tgz").replace(/\\/g, "/")}`, + "", + "3 packages installed", + ]); + expect(await exited1).toBe(0); + expect(urls.sort()).toEqual([ + `${ctx.registry_url}bar`, + `${ctx.registry_url}bar-0.0.2.tgz`, + `${ctx.registry_url}baz`, + `${ctx.registry_url}baz-0.0.3.tgz`, + ]); + expect(ctx.requested).toBe(4); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([ + ".bin", + ".cache", + "@barn", + "bar", + "baz", + ]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@barn"))).toEqual(["moo"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@barn", "moo"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "@barn", "moo", "package.json")).json()).toEqual({ + name: "@barn/moo", + version: "0.1.0", + dependencies: { + bar: "0.0.2", + baz: "latest", + }, + }); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + await access(join(ctx.package_dir, "bun.lockb")); + // Perform `bun install` again but with lockfile from before + await rm(join(ctx.package_dir, "node_modules"), { force: true, recursive: true }); + urls.length = 0; + const { + stdout: stdout2, + stderr: stderr2, + exited: exited2, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err2 = await new Response(stderr2).text(); + expect(err2).not.toContain("Saved lockfile"); + const out2 = await new Response(stdout2).text(); + expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ @barn/moo@${join(import.meta.dir, "moo-0.1.0.tgz").replace(/\\/g, "/")}`, + "", + "3 packages installed", + ]); + expect(await exited2).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar-0.0.2.tgz`, `${ctx.registry_url}baz-0.0.3.tgz`]); + expect(ctx.requested).toBe(6); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([ + ".bin", + ".cache", + "@barn", + "bar", + "baz", + ]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins(["baz-run"]); + expect(join(ctx.package_dir, "node_modules", ".bin", "baz-run")).toBeValidBin(join("..", "baz", "index.js")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@barn"))).toEqual(["moo"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@barn", "moo"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "@barn", "moo", "package.json")).json()).toEqual({ + name: "@barn/moo", + version: "0.1.0", + dependencies: { + bar: "0.0.2", + baz: "latest", + }, + }); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz"))).toEqual(["index.js", "package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "baz", "package.json")).json()).toEqual({ + name: "baz", + version: "0.0.3", + bin: { + "baz-run": "index.js", + }, + }); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle devDependencies from folder", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.1.0", + dependencies: { + moo: "file:./moo", + }, + }), + ); + await mkdir(join(ctx.package_dir, "moo")); + const moo_package = JSON.stringify({ + name: "moo", + version: "0.2.0", + devDependencies: { + bar: "^0.0.2", + }, + }); + await writeFile(join(ctx.package_dir, "moo", "package.json"), moo_package); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ moo@moo", + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`, `${ctx.registry_url}bar-0.0.2.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar", "moo"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "moo"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "moo", "package.json")).text()).toEqual(moo_package); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should deduplicate devDependencies from folder", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.1.0", + devDependencies: { + bar: "^0.0.2", + moo: "file:./moo", + }, + }), + ); + await mkdir(join(ctx.package_dir, "moo")); + const moo_package = JSON.stringify({ + name: "moo", + version: "0.2.0", + devDependencies: { + bar: "^0.0.2", + }, + }); + await writeFile(join(ctx.package_dir, "moo", "package.json"), moo_package); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@0.0.2", + "+ moo@moo", + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`, `${ctx.registry_url}bar-0.0.2.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar", "moo"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "moo"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "moo", "package.json")).text()).toEqual(moo_package); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should install dependencies in root package of workspace", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.1.0", + workspaces: ["moo"], + }), + ); + await mkdir(join(ctx.package_dir, "moo")); + const moo_package = JSON.stringify({ + name: "moo", + version: "0.2.0", + dependencies: { + bar: "^0.0.2", + }, + }); + await writeFile(join(ctx.package_dir, "moo", "package.json"), moo_package); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(ctx.package_dir, "moo"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@0.0.2", + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`, `${ctx.registry_url}bar-0.0.2.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar", "moo"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "moo"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "moo", "package.json")).text()).toEqual(moo_package); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should install dependencies in root package of workspace (*)", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.1.0", + workspaces: ["*"], + }), + ); + await mkdir(join(ctx.package_dir, "moo")); + const moo_package = JSON.stringify({ + name: "moo", + version: "0.2.0", + dependencies: { + bar: "^0.0.2", + }, + }); + await writeFile(join(ctx.package_dir, "moo", "package.json"), moo_package); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(ctx.package_dir, "moo"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@0.0.2", + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`, `${ctx.registry_url}bar-0.0.2.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar", "moo"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "moo"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "moo", "package.json")).text()).toEqual(moo_package); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should ignore invalid workspaces from parent directory", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + const foo_package = JSON.stringify({ + name: "foo", + version: "0.1.0", + workspaces: ["moz"], + }); + await writeFile(join(ctx.package_dir, "package.json"), foo_package); + await mkdir(join(ctx.package_dir, "moo")); + await writeFile( + join(ctx.package_dir, "moo", "bunfig.toml"), + await file(join(ctx.package_dir, "bunfig.toml")).text(), + ); + const moo_package = JSON.stringify({ + name: "moo", + version: "0.2.0", + dependencies: { + bar: "^0.0.2", + }, + }); + await writeFile(join(ctx.package_dir, "moo", "package.json"), moo_package); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(ctx.package_dir, "moo"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@0.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`, `${ctx.registry_url}bar-0.0.2.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(ctx.package_dir)).toEqual(["bunfig.toml", "moo", "package.json"]); + expect(await file(join(ctx.package_dir, "package.json")).text()).toEqual(foo_package); + expect(await readdirSorted(join(ctx.package_dir, "moo"))).toEqual([ + "bun.lockb", + "bunfig.toml", + "node_modules", + "package.json", + ]); + expect(await file(join(ctx.package_dir, "moo", "package.json")).text()).toEqual(moo_package); + expect(await readdirSorted(join(ctx.package_dir, "moo", "node_modules"))).toEqual([".cache", "bar"]); + expect(await readdirSorted(join(ctx.package_dir, "moo", "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "moo", "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + }); + }); + + it("should handle --cwd", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + const foo_package = JSON.stringify({ + name: "foo", + version: "0.1.0", + }); + await writeFile(join(ctx.package_dir, "package.json"), foo_package); + await mkdir(join(ctx.package_dir, "moo")); + await writeFile( + join(ctx.package_dir, "moo", "bunfig.toml"), + await file(join(ctx.package_dir, "bunfig.toml")).text(), + ); + const moo_package = JSON.stringify({ + name: "moo", + version: "0.2.0", + dependencies: { + bar: "^0.0.2", + }, + }); + await writeFile(join(ctx.package_dir, "moo", "package.json"), moo_package); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--cwd", "moo"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@0.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`, `${ctx.registry_url}bar-0.0.2.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(ctx.package_dir)).toEqual(["bunfig.toml", "moo", "package.json"]); + expect(await file(join(ctx.package_dir, "package.json")).text()).toEqual(foo_package); + expect(await readdirSorted(join(ctx.package_dir, "moo"))).toEqual([ + "bun.lockb", + "bunfig.toml", + "node_modules", + "package.json", + ]); + expect(await file(join(ctx.package_dir, "moo", "package.json")).text()).toEqual(moo_package); + expect(await readdirSorted(join(ctx.package_dir, "moo", "node_modules"))).toEqual([".cache", "bar"]); + expect(await readdirSorted(join(ctx.package_dir, "moo", "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "moo", "node_modules", "bar", "package.json")).json()).toEqual({ + name: "bar", + version: "0.0.2", + }); + }); + }); + + it("should handle --frozen-lockfile", async () => { + await withContext(defaultOpts, async ctx => { + let urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { "0.0.3": { as: "0.0.3" }, "0.0.5": { as: "0.0.5" } }), + ); + + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ name: "foo", version: "0.0.1", dependencies: { baz: "0.0.3" } }), + ); + + // save the lockfile once + expect( + await spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "ignore", + stdin: "ignore", + stderr: "ignore", + env, + }).exited, + ).toBe(0); + + // change version of baz in package.json + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { baz: "0.0.5" }, + }), + ); + + const { stderr, exited } = spawn({ + cmd: [bunExe(), "install", "--frozen-lockfile"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const err = await stderr.text(); + expect(err).toContain("error: lockfile had changes, but lockfile is frozen"); + expect(await exited).toBe(1); + }); + }); + + it("should handle bun ci alias (to --frozen-lockfile)", async () => { + await withContext(defaultOpts, async ctx => { + let urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { "0.0.3": { as: "0.0.3" }, "0.0.5": { as: "0.0.5" } }), + ); + + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ name: "foo", version: "0.0.1", dependencies: { baz: "0.0.3" } }), + ); + + // save the lockfile once + expect( + await spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "ignore", + stdin: "ignore", + stderr: "ignore", + env, + }).exited, + ).toBe(0); + + // change version of baz in package.json + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { baz: "0.0.5" }, + }), + ); + + const { stderr: stderr1, exited: exited1 } = spawn({ + cmd: [bunExe(), "ci"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const err1 = await new Response(stderr1).text(); + expect(err1).toContain("error: lockfile had changes, but lockfile is frozen"); + expect(await exited1).toBe(1); + + // test that it works even if ci isn't first "arg" + const { stderr: stderr2, exited: exited2 } = spawn({ + cmd: [bunExe(), "--save", "ci"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const err2 = await new Response(stderr2).text(); + expect(err2).toContain("error: lockfile had changes, but lockfile is frozen"); + expect(await exited2).toBe(1); + }); + }); + + it("should handle frozenLockfile in config file", async () => { + await withContext(defaultOpts, async ctx => { + let urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { "0.0.3": { as: "0.0.3" }, "0.0.5": { as: "0.0.5" } }), + ); + + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ name: "foo", version: "0.0.1", dependencies: { baz: "0.0.3" } }), + ); + + // save the lockfile once + expect( + await spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "ignore", + stdin: "ignore", + stderr: "ignore", + env, + }).exited, + ).toBe(0); + + await writeFile( + join(ctx.package_dir, "bunfig.toml"), + ` + [install] + frozenLockfile = true + registry = "${ctx.registry_url}" + `, + ); + + // change version of baz in package.json + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { baz: "0.0.5" }, + }), + ); + + const { stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + + const err = await stderr.text(); + expect(err).toContain("error: lockfile had changes, but lockfile is frozen"); + expect(await exited).toBe(1); + }); + }); + + it("should perform bin-linking across multiple dependencies", async () => { + await withContext(defaultOpts, async ctx => { + const foo_package = JSON.stringify({ + name: "foo", + devDependencies: { + "conditional-type-checks": "1.0.6", + "prettier": "2.8.8", + "tsd": "0.22.0", + "typescript": "5.0.4", + }, + }); + await writeFile(join(ctx.package_dir, "package.json"), foo_package); + await cp(join(import.meta.dir, "bun.lockb.bin-linking"), join(ctx.package_dir, "bun.lockb")); + await writeFile( + join(ctx.package_dir, "bunfig.toml"), + ` + [install] + cache = false + `, + ); + const { + stdout: stdout1, + stderr: stderr1, + exited: exited1, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err1 = await new Response(stderr1).text(); + expect(err1).not.toContain("error:"); + const out1 = await new Response(stdout1).text(); + expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + `bun install ${Bun.version_with_sha}`, + "", + expect.stringContaining("+ conditional-type-checks@1.0.6"), + expect.stringContaining("+ prettier@2.8.8"), + expect.stringContaining("+ tsd@0.22.0"), + expect.stringContaining("+ typescript@5.0.4"), + "", + "112 packages installed", + ]); + expect(await exited1).toBe(0); + expect(await readdirSorted(ctx.package_dir)).toEqual([ + "bun.lockb", + "bunfig.toml", + "node_modules", + "package.json", + ]); + expect(await file(join(ctx.package_dir, "package.json")).text()).toEqual(foo_package); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([ + ".bin", + ".cache", + "@babel", + "@nodelib", + "@tsd", + "@types", + "ansi-escapes", + "ansi-regex", + "ansi-styles", + "array-union", + "arrify", + "braces", + "camelcase", + "camelcase-keys", + "chalk", + "color-convert", + "color-name", + "conditional-type-checks", + "decamelize", + "decamelize-keys", + "dir-glob", + "emoji-regex", + "error-ex", + "eslint-formatter-pretty", + "eslint-rule-docs", + "fast-glob", + "fastq", + "fill-range", + "find-up", + "function-bind", + "glob-parent", + "globby", + "hard-rejection", + "has-flag", + "hasown", + "hosted-git-info", + "ignore", + "indent-string", + "irregular-plurals", + "is-arrayish", + "is-core-module", + "is-extglob", + "is-fullwidth-code-point", + "is-glob", + "is-number", + "is-plain-obj", + "is-unicode-supported", + "js-tokens", + "json-parse-even-better-errors", + "kind-of", + "lines-and-columns", + "locate-path", + "log-symbols", + "lru-cache", + "map-obj", + "meow", + "merge2", + "micromatch", + "min-indent", + "minimist-options", + "normalize-package-data", + "p-limit", + "p-locate", + "p-try", + "parse-json", + "path-exists", + "path-parse", + "path-type", + "picocolors", + "picomatch", + "plur", + "prettier", + "queue-microtask", + "quick-lru", + "read-pkg", + "read-pkg-up", + "redent", + "resolve", + "reusify", + "run-parallel", + "semver", + "slash", + "spdx-correct", + "spdx-exceptions", + "spdx-expression-parse", + "spdx-license-ids", + "string-width", + "strip-ansi", + "strip-indent", + "supports-color", + "supports-hyperlinks", + "supports-preserve-symlinks-flag", + "to-regex-range", + "trim-newlines", + "tsd", + "type-fest", + "typescript", + "validate-npm-package-license", + "yallist", + "yargs-parser", + ]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", ".bin"))).toHaveBins([ + "prettier", + "resolve", + "semver", + "tsc", + "tsd", + "tsserver", + ]); + // Perform `bun install --production` with lockfile from before + await rm(join(ctx.package_dir, "node_modules"), { force: true, recursive: true }); + const { + stdout: stdout2, + stderr: stderr2, + exited: exited2, + } = spawn({ + cmd: [bunExe(), "install", "--production"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err2 = await new Response(stderr2).text(); + expect(err2).not.toContain("Saved lockfile"); + expect(err2).not.toContain("error:"); + const out2 = await new Response(stdout2).text(); + expect(out2.replace(/\[[0-9\.]+m?s\]/, "[]").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "[] done", + "", + ]); + expect(await exited2).toBe(0); + expect(await readdirSorted(ctx.package_dir)).toEqual([ + "bun.lockb", + "bunfig.toml", + "node_modules", + "package.json", + ]); + expect(await file(join(ctx.package_dir, "package.json")).text()).toEqual(foo_package); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toBeEmpty(); + }); + }); + + it("should handle trustedDependencies", async () => { + await withContext(defaultOpts, async ctx => { + function getScripts(name: string) { + return { + preinstall: `echo preinstall ${name}`, + install: `echo install ${name}`, + postinstall: `echo postinstall ${name}`, + preprepare: `echo preprepare ${name}`, + prepare: `echo prepare ${name}`, + postprepare: `echo postprepare ${name}`, + }; + } + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.1.0", + dependencies: { + bar: "file:./bar", + moo: "file:./moo", + }, + trustedDependencies: ["moo"], + }), + ); + await mkdir(join(ctx.package_dir, "bar")); + const bar_package = JSON.stringify({ + name: "bar", + version: "0.2.0", + scripts: getScripts("bar"), + }); + await writeFile(join(ctx.package_dir, "bar", "package.json"), bar_package); + await mkdir(join(ctx.package_dir, "moo")); + const moo_package = JSON.stringify({ + name: "moo", + version: "0.3.0", + scripts: getScripts("moo"), + }); + await writeFile(join(ctx.package_dir, "moo", "package.json"), moo_package); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).not.toContain("error:"); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@bar", + "+ moo@moo", + "", + "2 packages installed", + "", + "Blocked 3 postinstalls. Run `bun pm untrusted` for details.", + "", + ]); + expect(await exited).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar", "moo"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "moo"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "moo", "package.json")).text()).toEqual(moo_package); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle `workspaces:*` and `workspace:*` gracefully", async () => { + await withContext(defaultOpts, async ctx => { + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + workspaces: ["*"], + dependencies: { + bar: "workspace:*", + }, + }), + ); + await mkdir(join(ctx.package_dir, "bar")); + const bar_package = JSON.stringify({ + name: "bar", + version: "0.0.1", + }); + await writeFile(join(ctx.package_dir, "bar", "package.json"), bar_package); + const { + stdout: stdout1, + stderr: stderr1, + exited: exited1, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err1 = await new Response(stderr1).text(); + expect(err1).toContain("Saved lockfile"); + const out1 = await new Response(stdout1).text(); + expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@workspace:bar", + "", + "1 package installed", + ]); + expect(await exited1).toBe(0); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar"]); + expect(await readlink(join(ctx.package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "bar")); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); + await access(join(ctx.package_dir, "bun.lockb")); + // Perform `bun install` again but with lockfile from before + await rm(join(ctx.package_dir, "node_modules"), { force: true, recursive: true }); + const { + stdout: stdout2, + stderr: stderr2, + exited: exited2, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err2 = await new Response(stderr2).text(); + const out2 = await new Response(stdout2).text(); + expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@workspace:bar", + "", + "1 package installed", + ]); + expect(await exited2).toBe(0); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar"]); + expect(await readlink(join(ctx.package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "bar")); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle `workspaces:bar` and `workspace:*` gracefully", async () => { + await withContext(defaultOpts, async ctx => { + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + workspaces: ["bar"], + dependencies: { + bar: "workspace:*", + }, + }), + ); + await mkdir(join(ctx.package_dir, "bar")); + const bar_package = JSON.stringify({ + name: "bar", + version: "0.0.1", + }); + await writeFile(join(ctx.package_dir, "bar", "package.json"), bar_package); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@workspace:bar", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar"]); + expect(await readlink(join(ctx.package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "bar")); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle `workspaces:*` and `workspace:bar` gracefully", async () => { + await withContext(defaultOpts, async ctx => { + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + workspaces: ["*"], + dependencies: { + bar: "workspace:bar", + }, + }), + ); + await mkdir(join(ctx.package_dir, "bar")); + const bar_package = JSON.stringify({ + name: "bar", + version: "0.0.1", + }); + await writeFile(join(ctx.package_dir, "bar", "package.json"), bar_package); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@workspace:bar", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar"]); + expect(await readlink(join(ctx.package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "bar")); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle `workspaces:bar` and `workspace:bar` gracefully", async () => { + await withContext(defaultOpts, async ctx => { + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + workspaces: ["bar"], + dependencies: { + bar: "workspace:bar", + }, + }), + ); + await mkdir(join(ctx.package_dir, "bar")); + const bar_package = JSON.stringify({ + name: "bar", + version: "0.0.1", + }); + await writeFile(join(ctx.package_dir, "bar", "package.json"), bar_package); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@workspace:bar", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar"]); + expect(await readlink(join(ctx.package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "bar")); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle installing packages from inside a workspace with `*`", async () => { + await withContext(defaultOpts, async ctx => { + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "main", + workspaces: ["packages/*"], + private: true, + }), + ); + await mkdir(join(ctx.package_dir, "packages", "yolo"), { recursive: true }); + const yolo_package = JSON.stringify({ + name: "yolo", + version: "0.0.1", + dependencies: { + swag: "workspace:*", + }, + }); + await writeFile(join(ctx.package_dir, "packages", "yolo", "package.json"), yolo_package); + await mkdir(join(ctx.package_dir, "packages", "swag")); + const swag_package = JSON.stringify({ + name: "swag", + version: "0.0.1", + }); + await writeFile(join(ctx.package_dir, "packages", "swag", "package.json"), swag_package); + const { + stdout: stdout1, + stderr: stderr1, + exited: exited1, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(ctx.package_dir, "packages", "yolo"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err1 = await new Response(stderr1).text(); + expect(err1).toContain("Saved lockfile"); + const out1 = await new Response(stdout1).text(); + expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ swag@workspace:packages/swag`, + "", + "2 packages installed", + ]); + expect(await exited1).toBe(0); + expect(ctx.requested).toBe(0); + await access(join(ctx.package_dir, "bun.lockb")); + + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + + const { + stdout: stdout2, + stderr: stderr2, + exited: exited2, + } = spawn({ + cmd: [bunExe(), "install", "bar"], + cwd: join(ctx.package_dir, "packages", "yolo"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err2 = await new Response(stderr2).text(); + expect(err2).toContain("Saved lockfile"); + const out2 = await new Response(stdout2).text(); + expect(out2).toContain("installed bar"); + expect(await exited2).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`, `${ctx.registry_url}bar-0.0.2.tgz`]); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle installing packages from inside a workspace without prefix", async () => { + await withContext(defaultOpts, async ctx => { + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "main", + workspaces: ["packages/*"], + private: true, + }), + ); + await mkdir(join(ctx.package_dir, "packages", "p1"), { recursive: true }); + const p1_package = JSON.stringify({ + name: "p1", + version: "0.0.1", + dependencies: { + p2: "0.1.0", + }, + }); + await writeFile(join(ctx.package_dir, "packages", "p1", "package.json"), p1_package); + + await mkdir(join(ctx.package_dir, "packages", "p2")); + const p2_package = JSON.stringify({ + name: "p2", + version: "0.1.0", + }); + await writeFile(join(ctx.package_dir, "packages", "p2", "package.json"), p2_package); + + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + + const { + stdout: stdout1, + stderr: stderr1, + exited: exited1, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(ctx.package_dir, "packages", "p1"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err1 = await new Response(stderr1).text(); + expect(err1).toContain("Saved lockfile"); + const out1 = await new Response(stdout1).text(); + expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ p2@workspace:packages/p2`, + "", + "2 packages installed", + ]); + expect(await exited1).toBe(0); + expect(ctx.requested).toBe(0); + await access(join(ctx.package_dir, "bun.lockb")); + + const { + stdout: stdout2, + stderr: stderr2, + exited: exited2, + } = spawn({ + cmd: [bunExe(), "install", "bar"], + cwd: join(ctx.package_dir, "packages", "p1"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err2 = await new Response(stderr2).text(); + expect(err2).toContain("Saved lockfile"); + const out2 = await new Response(stdout2).text(); + expect(out2).toContain("installed bar"); + expect(await exited2).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`, `${ctx.registry_url}bar-0.0.2.tgz`]); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); + + it("should handle installing workspaces with more complicated globs", async () => { + const package_dir = tempDirWithFiles("complicated-glob", { + "package.json": JSON.stringify({ name: "package3", version: "0.0.1", - dependencies: { - package1: "workspace:^", + workspaces: ["packages/**/*"], + }), + "packages": { + "frontend": { + "package.json": JSON.stringify({ + name: "frontend", + version: "0.0.1", + dependencies: { + "types": "workspace:*", + "components": "workspace:*", + }, + }), + "components": { + "package.json": JSON.stringify({ + name: "components", + version: "0.0.1", + dependencies: { + "types": "workspace:*", + }, + }), + }, }, - }); - await writeFile(join(package_dir, "packages", "package3", "package.json"), package3); - } - { - const package4 = JSON.stringify({ - name: "package4", - version: "0.0.1", - dependencies: { - package1: "workspace:../package1", + "backend": { + "package.json": JSON.stringify({ + name: "backend", + version: "0.0.1", + dependencies: { + "types": "workspace:*", + }, + }), }, - }); - await writeFile(join(package_dir, "packages", "package4", "package.json"), package4); - } - { - const package5 = JSON.stringify({ - name: "package5", - version: "0.0.1", - dependencies: { - package1: "workspace:0.0.2", + "types": { + "package.json": JSON.stringify({ + name: "types", + version: "0.0.1", + dependencies: {}, + }), }, - }); - await writeFile(join(package_dir, "packages", "package5", "package.json"), package5); - } - - const { - stdout: stdout1, - stderr: stderr1, - exited: exited1, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(package_dir, "packages", "package2"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + }, }); - const err1 = await new Response(stderr1).text(); + + const { stdout, stderr } = await Bun.$`${bunExe()} install`.env(env).cwd(package_dir).throws(true); + const err1 = stderr.toString(); expect(err1).toContain("Saved lockfile"); - const out1 = await new Response(stdout1).text(); - expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ package1@workspace:packages/package1`, - "", - "5 packages installed", - ]); - expect(await exited1).toBe(0); - await access(join(package_dir, "bun.lockb")); + expect( + stdout + .toString() + .replace(/\s*\[[0-9\.]+m?s\]\s*$/, "") + .split(/\r?\n/), + ).toEqual([expect.stringContaining("bun install v1."), "", "Checked 7 installs across 5 packages (no changes)"]); + }); - var urls: string[] = []; - setHandler(dummyRegistry(urls)); - - const { - stdout: stdout1_2, - stderr: stderr1_2, - exited: exited1_2, - } = spawn({ - cmd: [bunExe(), "install", "bar"], - cwd: join(package_dir, "packages", "package2"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should handle installing workspaces with multiple glob patterns", async () => { + const package_dir = tempDirWithFiles("multi-glob", { + "package.json": JSON.stringify({ + name: "main", + version: "0.0.1", + workspaces: ["backend/**/*", "client/**/*", "types/**/*"], + }), + "backend": { + "server": { + "package.json": JSON.stringify({ + name: "server", + version: "0.0.1", + dependencies: { + "types": "workspace:*", + "db": "workspace:*", + }, + }), + }, + "db": { + "package.json": JSON.stringify({ + name: "db", + version: "0.0.1", + dependencies: { + "types": "workspace:*", + }, + }), + }, + }, + "client": { + "clientlib": { + "package.json": JSON.stringify({ + name: "clientlib", + version: "0.0.1", + dependencies: { + "types": "workspace:*", + }, + }), + }, + }, + "types": { + "types": { + "package.json": JSON.stringify({ + name: "types", + version: "0.0.1", + dependencies: {}, + }), + }, + }, }); - const err1_2 = await new Response(stderr1_2).text(); - expect(err1_2).toContain("Saved lockfile"); - const out1_2 = await new Response(stdout1_2).text(); - expect(out1_2).toContain("installed bar"); - expect(await exited1_2).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); - await access(join(package_dir, "bun.lockb")); - await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); - await rm(join(package_dir, "bun.lockb"), { force: true, recursive: true }); + console.log("TEMPDIR", package_dir); - const { - stdout: stdout2, - stderr: stderr2, - exited: exited2, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(package_dir, "packages", "package3"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + const { stdout, stderr } = await Bun.$`${bunExe()} install`.env(env).cwd(package_dir).throws(true); + const err1 = stderr.toString(); + expect(err1).toContain("Saved lockfile"); + expect( + stdout + .toString() + .replace(/\s*\[[0-9\.]+m?s\]\s*$/, "") + .split(/\r?\n/), + ).toEqual([expect.stringContaining("bun install v1."), "", "Checked 7 installs across 5 packages (no changes)"]); + }); + + it.todo("should handle installing workspaces with absolute glob patterns", async () => { + const package_dir = tempDirWithFiles("absolute-glob", { + "package.json": base => + JSON.stringify({ + name: "package3", + version: "0.0.1", + workspaces: [join(base, "packages/**/*")], + }), + "packages": { + "frontend": { + "package.json": JSON.stringify({ + name: "frontend", + version: "0.0.1", + dependencies: { + "types": "workspace:*", + "components": "workspace:*", + }, + }), + "components": { + "package.json": JSON.stringify({ + name: "components", + version: "0.0.1", + dependencies: { + "types": "workspace:*", + }, + }), + }, + }, + "backend": { + "package.json": JSON.stringify({ + name: "backend", + version: "0.0.1", + dependencies: { + "types": "workspace:*", + }, + }), + }, + "types": { + "package.json": JSON.stringify({ + name: "types", + version: "0.0.1", + dependencies: {}, + }), + }, + }, }); - const err2 = await new Response(stderr2).text(); - expect(err2).toContain("Saved lockfile"); - const out2 = await new Response(stdout2).text(); - expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ package1@workspace:packages/package1`, - "", - "6 packages installed", - ]); - expect(await exited2).toBe(0); + console.log("TEMP DIR", package_dir); - const { - stdout: stdout2_2, - stderr: stderr2_2, - exited: exited2_2, - } = spawn({ - cmd: [bunExe(), "install", "bar"], - cwd: join(package_dir, "packages", "package3"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + const { stdout, stderr } = await Bun.$`${bunExe()} install`.env(env).cwd(package_dir).throws(true); + const err1 = stderr.toString(); + expect(err1).toContain("Saved lockfile"); + expect( + stdout + .toString() + .replace(/\s*\[[0-9\.]+m?s\]\s*$/, "") + .split(/\r?\n/), + ).toEqual([expect.stringContaining("bun install v1."), "", "4 packages installed"]); + }); + + it("should handle installing packages inside workspaces with difference versions", async () => { + await withContext(defaultOpts, async ctx => { + let package_jsons = [ + JSON.stringify({ + name: "main", + workspaces: ["packages/*"], + private: true, + }), + JSON.stringify({ + name: "main", + private: true, + workspaces: [ + "packages/package1", + "packages/package2", + "packages/package3", + "packages/package4", + "packages/package5", + ], + }), + ]; + await mkdir(join(ctx.package_dir, "packages", "package1"), { recursive: true }); + await mkdir(join(ctx.package_dir, "packages", "package2")); + await mkdir(join(ctx.package_dir, "packages", "package3")); + await mkdir(join(ctx.package_dir, "packages", "package4")); + await mkdir(join(ctx.package_dir, "packages", "package5")); + { + const package1 = JSON.stringify({ + name: "package1", + version: "0.0.2", + }); + await writeFile(join(ctx.package_dir, "packages", "package1", "package.json"), package1); + } + { + const package2 = JSON.stringify({ + name: "package2", + version: "0.0.1", + dependencies: { + package1: "workspace:*", + }, + }); + await writeFile(join(ctx.package_dir, "packages", "package2", "package.json"), package2); + } + { + const package3 = JSON.stringify({ + name: "package3", + version: "0.0.1", + dependencies: { + package1: "workspace:^", + }, + }); + await writeFile(join(ctx.package_dir, "packages", "package3", "package.json"), package3); + } + { + const package4 = JSON.stringify({ + name: "package4", + version: "0.0.1", + dependencies: { + package1: "workspace:../package1", + }, + }); + await writeFile(join(ctx.package_dir, "packages", "package4", "package.json"), package4); + } + { + const package5 = JSON.stringify({ + name: "package5", + version: "0.0.1", + dependencies: { + package1: "workspace:0.0.2", + }, + }); + await writeFile(join(ctx.package_dir, "packages", "package5", "package.json"), package5); + } + for (const package_json of package_jsons) { + await writeFile(join(ctx.package_dir, "package.json"), package_json); + + { + const package1 = JSON.stringify({ + name: "package1", + version: "0.0.2", + }); + await writeFile(join(ctx.package_dir, "packages", "package1", "package.json"), package1); + } + { + const package2 = JSON.stringify({ + name: "package2", + version: "0.0.1", + dependencies: { + package1: "workspace:*", + }, + }); + await writeFile(join(ctx.package_dir, "packages", "package2", "package.json"), package2); + } + { + const package3 = JSON.stringify({ + name: "package3", + version: "0.0.1", + dependencies: { + package1: "workspace:^", + }, + }); + await writeFile(join(ctx.package_dir, "packages", "package3", "package.json"), package3); + } + { + const package4 = JSON.stringify({ + name: "package4", + version: "0.0.1", + dependencies: { + package1: "workspace:../package1", + }, + }); + await writeFile(join(ctx.package_dir, "packages", "package4", "package.json"), package4); + } + { + const package5 = JSON.stringify({ + name: "package5", + version: "0.0.1", + dependencies: { + package1: "workspace:0.0.2", + }, + }); + await writeFile(join(ctx.package_dir, "packages", "package5", "package.json"), package5); + } + + const { + stdout: stdout1, + stderr: stderr1, + exited: exited1, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(ctx.package_dir, "packages", "package2"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err1 = await new Response(stderr1).text(); + expect(err1).toContain("Saved lockfile"); + const out1 = await new Response(stdout1).text(); + expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ package1@workspace:packages/package1`, + "", + "5 packages installed", + ]); + expect(await exited1).toBe(0); + await access(join(ctx.package_dir, "bun.lockb")); + + var urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + + const { + stdout: stdout1_2, + stderr: stderr1_2, + exited: exited1_2, + } = spawn({ + cmd: [bunExe(), "install", "bar"], + cwd: join(ctx.package_dir, "packages", "package2"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err1_2 = await new Response(stderr1_2).text(); + expect(err1_2).toContain("Saved lockfile"); + const out1_2 = await new Response(stdout1_2).text(); + expect(out1_2).toContain("installed bar"); + expect(await exited1_2).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`, `${ctx.registry_url}bar-0.0.2.tgz`]); + await access(join(ctx.package_dir, "bun.lockb")); + + await rm(join(ctx.package_dir, "node_modules"), { force: true, recursive: true }); + await rm(join(ctx.package_dir, "bun.lockb"), { force: true, recursive: true }); + + const { + stdout: stdout2, + stderr: stderr2, + exited: exited2, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(ctx.package_dir, "packages", "package3"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err2 = await new Response(stderr2).text(); + expect(err2).toContain("Saved lockfile"); + const out2 = await new Response(stdout2).text(); + expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ package1@workspace:packages/package1`, + "", + "6 packages installed", + ]); + expect(await exited2).toBe(0); + + const { + stdout: stdout2_2, + stderr: stderr2_2, + exited: exited2_2, + } = spawn({ + cmd: [bunExe(), "install", "bar"], + cwd: join(ctx.package_dir, "packages", "package3"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err2_2 = await new Response(stderr2_2).text(); + expect(err2_2).toContain("Saved lockfile"); + const out2_2 = await new Response(stdout2_2).text(); + expect(out2_2).toContain("installed bar"); + expect(await exited2_2).toBe(0); + await access(join(ctx.package_dir, "bun.lockb")); + + await rm(join(ctx.package_dir, "node_modules"), { force: true, recursive: true }); + await rm(join(ctx.package_dir, "bun.lockb"), { force: true, recursive: true }); + + const { + stdout: stdout3, + stderr: stderr3, + exited: exited3, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(ctx.package_dir, "packages", "package4"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err3 = await new Response(stderr3).text(); + expect(err3).toContain("Saved lockfile"); + const out3 = await new Response(stdout3).text(); + expect(out3.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ package1@workspace:packages/package1`, + "", + "6 packages installed", + ]); + expect(await exited3).toBe(0); + + const { + stdout: stdout3_2, + stderr: stderr3_2, + exited: exited3_2, + } = spawn({ + cmd: [bunExe(), "install", "bar"], + cwd: join(ctx.package_dir, "packages", "package4"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err3_2 = await new Response(stderr3_2).text(); + expect(err3_2).toContain("Saved lockfile"); + const out3_2 = await new Response(stdout3_2).text(); + expect(out3_2).toContain("installed bar"); + expect(await exited3_2).toBe(0); + await access(join(ctx.package_dir, "bun.lockb")); + + await rm(join(ctx.package_dir, "node_modules"), { force: true, recursive: true }); + await rm(join(ctx.package_dir, "bun.lockb"), { force: true, recursive: true }); + + const { + stdout: stdout4, + stderr: stderr4, + exited: exited4, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(ctx.package_dir, "packages", "package5"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err4 = await new Response(stderr4).text(); + expect(err4).toContain("Saved lockfile"); + const out4 = await new Response(stdout4).text(); + expect(out4.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ package1@workspace:packages/package1`, + "", + "6 packages installed", + ]); + expect(await exited4).toBe(0); + + const { + stdout: stdout4_2, + stderr: stderr4_2, + exited: exited4_2, + } = spawn({ + cmd: [bunExe(), "install", "bar"], + cwd: join(ctx.package_dir, "packages", "package5"), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err4_2 = await new Response(stderr4_2).text(); + expect(err4_2).toContain("Saved lockfile"); + const out4_2 = await new Response(stdout4_2).text(); + expect(out4_2).toContain("installed bar"); + expect(await exited4_2).toBe(0); + await access(join(ctx.package_dir, "bun.lockb")); + + // from the root + await rm(join(ctx.package_dir, "node_modules"), { force: true, recursive: true }); + await rm(join(ctx.package_dir, "bun.lockb"), { force: true, recursive: true }); + + const { + stdout: stdout5, + stderr: stderr5, + exited: exited5, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: join(ctx.package_dir), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err5 = await new Response(stderr5).text(); + expect(err5).toContain("Saved lockfile"); + const out5 = await new Response(stdout5).text(); + expect(out5.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "6 packages installed", + ]); + expect(await exited5).toBe(0); + + const { + stdout: stdout5_2, + stderr: stderr5_2, + exited: exited5_2, + } = spawn({ + cmd: [bunExe(), "install", "bar"], + cwd: join(ctx.package_dir), + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err5_2 = await new Response(stderr5_2).text(); + expect(err5_2).toContain("Saved lockfile"); + const out5_2 = await new Response(stdout5_2).text(); + expect(out5_2).toContain("installed bar"); + expect(await exited5_2).toBe(0); + await access(join(ctx.package_dir, "bun.lockb")); + + await rm(join(ctx.package_dir, "node_modules"), { force: true, recursive: true }); + await rm(join(ctx.package_dir, "bun.lockb"), { force: true, recursive: true }); + await rm(join(ctx.package_dir, "package.json")); + } }); - const err2_2 = await new Response(stderr2_2).text(); - expect(err2_2).toContain("Saved lockfile"); - const out2_2 = await new Response(stdout2_2).text(); - expect(out2_2).toContain("installed bar"); - expect(await exited2_2).toBe(0); - await access(join(package_dir, "bun.lockb")); + }); - await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); - await rm(join(package_dir, "bun.lockb"), { force: true, recursive: true }); - - const { - stdout: stdout3, - stderr: stderr3, - exited: exited3, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(package_dir, "packages", "package4"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should override npm dependency by matching workspace", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + workspaces: ["*"], + dependencies: { + bar: "*", + }, + }), + ); + await mkdir(join(ctx.package_dir, "bar")); + const bar_package = JSON.stringify({ + name: "bar", + version: "0.0.1", + }); + await writeFile(join(ctx.package_dir, "bar", "package.json"), bar_package); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@workspace:bar", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar"]); + expect(await readlink(join(ctx.package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "bar")); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); + await access(join(ctx.package_dir, "bun.lockb")); }); - const err3 = await new Response(stderr3).text(); - expect(err3).toContain("Saved lockfile"); - const out3 = await new Response(stdout3).text(); - expect(out3.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ package1@workspace:packages/package1`, - "", - "6 packages installed", - ]); - expect(await exited3).toBe(0); + }); - const { - stdout: stdout3_2, - stderr: stderr3_2, - exited: exited3_2, - } = spawn({ - cmd: [bunExe(), "install", "bar"], - cwd: join(package_dir, "packages", "package4"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should not override npm dependency by workspace with mismatched version", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + workspaces: ["*"], + dependencies: { + bar: "^0.0.2", + }, + }), + ); + await mkdir(join(ctx.package_dir, "bar")); + const bar_package = JSON.stringify({ + name: "bar", + version: "0.0.1", + }); + await writeFile(join(ctx.package_dir, "bar", "package.json"), bar_package); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@0.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`, `${ctx.registry_url}bar-0.0.2.tgz`]); + expect(ctx.requested).toBe(2); }); - const err3_2 = await new Response(stderr3_2).text(); - expect(err3_2).toContain("Saved lockfile"); - const out3_2 = await new Response(stdout3_2).text(); - expect(out3_2).toContain("installed bar"); - expect(await exited3_2).toBe(0); - await access(join(package_dir, "bun.lockb")); + }); - await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); - await rm(join(package_dir, "bun.lockb"), { force: true, recursive: true }); - - const { - stdout: stdout4, - stderr: stderr4, - exited: exited4, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(package_dir, "packages", "package5"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should override @scoped npm dependency by matching workspace", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + "@bar/baz": "^0.1", + }, + }), + ); + await mkdir(join(ctx.package_dir, "packages", "bar-baz"), { recursive: true }); + const baz_package = JSON.stringify({ + name: "@bar/baz", + version: "0.1.2", + }); + await writeFile(join(ctx.package_dir, "packages", "bar-baz", "package.json"), baz_package); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ @bar/baz@workspace:packages/bar-baz`, + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "@bar"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@bar"))).toEqual(["baz"]); + expect(await readlink(join(ctx.package_dir, "node_modules", "@bar", "baz"))).toBeWorkspaceLink( + join("..", "..", "packages", "bar-baz"), + ); + expect(await file(join(ctx.package_dir, "node_modules", "@bar", "baz", "package.json")).text()).toEqual( + baz_package, + ); + await access(join(ctx.package_dir, "bun.lockb")); }); - const err4 = await new Response(stderr4).text(); - expect(err4).toContain("Saved lockfile"); - const out4 = await new Response(stdout4).text(); - expect(out4.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ package1@workspace:packages/package1`, - "", - "6 packages installed", - ]); - expect(await exited4).toBe(0); + }); - const { - stdout: stdout4_2, - stderr: stderr4_2, - exited: exited4_2, - } = spawn({ - cmd: [bunExe(), "install", "bar"], - cwd: join(package_dir, "packages", "package5"), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should override aliased npm dependency by matching workspace", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + workspaces: ["*"], + dependencies: { + bar: "npm:baz@<0.0.2", + }, + }), + ); + await mkdir(join(ctx.package_dir, "baz")); + const baz_package = JSON.stringify({ + name: "baz", + version: "0.0.1", + }); + await writeFile(join(ctx.package_dir, "baz", "package.json"), baz_package); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@workspace:baz", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar", "baz"]); + expect(await readlink(join(ctx.package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "baz")); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).text()).toEqual(baz_package); + await access(join(ctx.package_dir, "bun.lockb")); }); - const err4_2 = await new Response(stderr4_2).text(); - expect(err4_2).toContain("Saved lockfile"); - const out4_2 = await new Response(stdout4_2).text(); - expect(out4_2).toContain("installed bar"); - expect(await exited4_2).toBe(0); - await access(join(package_dir, "bun.lockb")); + }); - // from the root - await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); - await rm(join(package_dir, "bun.lockb"), { force: true, recursive: true }); - - const { - stdout: stdout5, - stderr: stderr5, - exited: exited5, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: join(package_dir), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should override child npm dependency by matching workspace", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + workspaces: ["*"], + }), + ); + await mkdir(join(ctx.package_dir, "bar")); + const bar_package = JSON.stringify({ + name: "bar", + version: "0.0.1", + }); + await writeFile(join(ctx.package_dir, "bar", "package.json"), bar_package); + await mkdir(join(ctx.package_dir, "baz")); + await writeFile( + join(ctx.package_dir, "baz", "package.json"), + JSON.stringify({ + name: "baz", + version: "0.1.0", + dependencies: { + bar: "*", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar", "baz"]); + expect(await readlink(join(ctx.package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "bar")); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); + expect(await readlink(join(ctx.package_dir, "node_modules", "baz"))).toBeWorkspaceLink(join("..", "baz")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz"))).toEqual(["package.json"]); + await access(join(ctx.package_dir, "bun.lockb")); }); - const err5 = await new Response(stderr5).text(); - expect(err5).toContain("Saved lockfile"); - const out5 = await new Response(stdout5).text(); - expect(out5.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "6 packages installed", - ]); - expect(await exited5).toBe(0); + }); - const { - stdout: stdout5_2, - stderr: stderr5_2, - exited: exited5_2, - } = spawn({ - cmd: [bunExe(), "install", "bar"], - cwd: join(package_dir), - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + it("should not override child npm dependency by workspace with mismatched version", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + workspaces: ["*"], + }), + ); + await mkdir(join(ctx.package_dir, "bar")); + const bar_package = JSON.stringify({ + name: "bar", + version: "0.0.1", + }); + await writeFile(join(ctx.package_dir, "bar", "package.json"), bar_package); + await mkdir(join(ctx.package_dir, "baz")); + await writeFile( + join(ctx.package_dir, "baz", "package.json"), + JSON.stringify({ + name: "baz", + version: "0.1.0", + dependencies: { + bar: "^0.0.2", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "3 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`, `${ctx.registry_url}bar-0.0.2.tgz`]); + expect(ctx.requested).toBe(2); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar", "baz"]); + expect(await readlink(join(ctx.package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "bar")); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); + expect(await readlink(join(ctx.package_dir, "node_modules", "baz"))).toBeWorkspaceLink(join("..", "baz")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz", "node_modules"))).toEqual(["bar"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz", "node_modules", "bar"))).toEqual([ + "package.json", + ]); + expect( + await file(join(ctx.package_dir, "node_modules", "baz", "node_modules", "bar", "package.json")).json(), + ).toEqual({ + name: "bar", + version: "0.0.2", + }); + await access(join(ctx.package_dir, "bun.lockb")); }); - const err5_2 = await new Response(stderr5_2).text(); - expect(err5_2).toContain("Saved lockfile"); - const out5_2 = await new Response(stdout5_2).text(); - expect(out5_2).toContain("installed bar"); - expect(await exited5_2).toBe(0); - await access(join(package_dir, "bun.lockb")); + }); - await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); - await rm(join(package_dir, "bun.lockb"), { force: true, recursive: true }); - await rm(join(package_dir, "package.json")); - } -}); + it("should override @scoped child npm dependency by matching workspace", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + }), + ); + await mkdir(join(ctx.package_dir, "packages", "moo-bar"), { recursive: true }); + const bar_package = JSON.stringify({ + name: "@moo/bar", + version: "1.2.3", + }); + await writeFile(join(ctx.package_dir, "packages", "moo-bar", "package.json"), bar_package); + await mkdir(join(ctx.package_dir, "packages", "moo-baz"), { recursive: true }); + await writeFile( + join(ctx.package_dir, "packages", "moo-baz", "package.json"), + JSON.stringify({ + name: "@moo/baz", + dependencies: { + "@moo/bar": "1.x", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "@moo"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@moo"))).toEqual(["bar", "baz"]); + expect(await readlink(join(ctx.package_dir, "node_modules", "@moo", "bar"))).toBeWorkspaceLink( + join("..", "..", "packages", "moo-bar"), + ); + expect(await file(join(ctx.package_dir, "node_modules", "@moo", "bar", "package.json")).text()).toEqual( + bar_package, + ); + expect(await readlink(join(ctx.package_dir, "node_modules", "@moo", "baz"))).toBeWorkspaceLink( + join("..", "..", "packages", "moo-baz"), + ); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@moo", "baz"))).toEqual(["package.json"]); + await access(join(ctx.package_dir, "bun.lockb")); + }); + }); -it("should override npm dependency by matching workspace", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - workspaces: ["*"], - dependencies: { - bar: "*", - }, - }), - ); - await mkdir(join(package_dir, "bar")); - const bar_package = JSON.stringify({ - name: "bar", - version: "0.0.1", + it("should override aliased child npm dependency by matching workspace", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + }), + ); + await mkdir(join(ctx.package_dir, "packages", "bar"), { recursive: true }); + const bar_package = JSON.stringify({ + name: "@moo/bar", + version: "0.0.1", + }); + await writeFile(join(ctx.package_dir, "packages", "bar", "package.json"), bar_package); + await mkdir(join(ctx.package_dir, "packages", "baz"), { recursive: true }); + await writeFile( + join(ctx.package_dir, "packages", "baz", "package.json"), + JSON.stringify({ + name: "baz", + version: "0.1.0", + dependencies: { + bar: "npm:@moo/bar@*", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "@moo", "bar", "baz"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@moo"))).toEqual(["bar"]); + expect(await readlink(join(ctx.package_dir, "node_modules", "@moo", "bar"))).toBeWorkspaceLink( + join("..", "..", "packages", "bar"), + ); + expect(await file(join(ctx.package_dir, "node_modules", "@moo", "bar", "package.json")).text()).toEqual( + bar_package, + ); + expect(await readlink(join(ctx.package_dir, "node_modules", "baz"))).toBeWorkspaceLink( + join("..", "packages", "baz"), + ); + expect(await readdirSorted(join(ctx.package_dir, "packages", "baz"))).toEqual(["package.json"]); + expect(await readlink(join(ctx.package_dir, "node_modules", "bar"))).toBeWorkspaceLink( + join("..", "packages", "bar"), + ); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - await writeFile(join(package_dir, "bar", "package.json"), bar_package); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@workspace:bar", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar"]); - expect(await readlink(join(package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "bar")); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); - await access(join(package_dir, "bun.lockb")); -}); -it("should not override npm dependency by workspace with mismatched version", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - workspaces: ["*"], - dependencies: { - bar: "^0.0.2", - }, - }), - ); - await mkdir(join(package_dir, "bar")); - const bar_package = JSON.stringify({ - name: "bar", - version: "0.0.1", + it("should handle `workspace:` with semver range", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + workspaces: ["bar", "baz"], + }), + ); + await mkdir(join(ctx.package_dir, "bar")); + const bar_package = JSON.stringify({ + name: "bar", + version: "0.0.1", + }); + await writeFile(join(ctx.package_dir, "bar", "package.json"), bar_package); + await mkdir(join(ctx.package_dir, "baz")); + await writeFile( + join(ctx.package_dir, "baz", "package.json"), + JSON.stringify({ + name: "baz", + version: "0.1.0", + dependencies: { + bar: "workspace:~0.0.1", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar", "baz"]); + expect(await readlink(join(ctx.package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "bar")); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); + expect(await readlink(join(ctx.package_dir, "node_modules", "baz"))).toBeWorkspaceLink(join("..", "baz")); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz"))).toEqual(["package.json"]); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - await writeFile(join(package_dir, "bar", "package.json"), bar_package); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@0.0.2", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); - expect(requested).toBe(2); -}); -it("should override @scoped npm dependency by matching workspace", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - workspaces: ["packages/*"], - dependencies: { - "@bar/baz": "^0.1", - }, - }), - ); - await mkdir(join(package_dir, "packages", "bar-baz"), { recursive: true }); - const baz_package = JSON.stringify({ - name: "@bar/baz", - version: "0.1.2", + it("should handle `workspace:` with alias & @scope", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + }), + ); + await mkdir(join(ctx.package_dir, "packages", "bar"), { recursive: true }); + const bar_package = JSON.stringify({ + name: "@moo/bar", + version: "0.1.2", + }); + await writeFile(join(ctx.package_dir, "packages", "bar", "package.json"), bar_package); + await mkdir(join(ctx.package_dir, "packages", "baz"), { recursive: true }); + await writeFile( + join(ctx.package_dir, "packages", "baz", "package.json"), + JSON.stringify({ + name: "@moz/baz", + dependencies: { + "@moz/bar": "workspace:@moo/bar@>=0.1", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "2 packages installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "@moo", "@moz"]); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@moo"))).toEqual(["bar"]); + expect(await readlink(join(ctx.package_dir, "node_modules", "@moo", "bar"))).toBeWorkspaceLink( + join("..", "..", "packages", "bar"), + ); + expect(await file(join(ctx.package_dir, "node_modules", "@moo", "bar", "package.json")).text()).toEqual( + bar_package, + ); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "@moz"))).toEqual(["bar", "baz"]); + expect(await readlink(join(ctx.package_dir, "node_modules", "@moz", "baz"))).toBeWorkspaceLink( + join("..", "..", "packages", "baz"), + ); + expect(await readlink(join(ctx.package_dir, "node_modules", "@moz", "bar"))).toBeWorkspaceLink( + join("..", "..", "packages", "bar"), + ); + expect(await readdirSorted(join(ctx.package_dir, "packages", "baz"))).toEqual(["package.json"]); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - await writeFile(join(package_dir, "packages", "bar-baz", "package.json"), baz_package); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ @bar/baz@workspace:packages/bar-baz`, - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "@bar"]); - expect(await readdirSorted(join(package_dir, "node_modules", "@bar"))).toEqual(["baz"]); - expect(await readlink(join(package_dir, "node_modules", "@bar", "baz"))).toBeWorkspaceLink( - join("..", "..", "packages", "bar-baz"), - ); - expect(await file(join(package_dir, "node_modules", "@bar", "baz", "package.json")).text()).toEqual(baz_package); - await access(join(package_dir, "bun.lockb")); -}); -it("should override aliased npm dependency by matching workspace", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - workspaces: ["*"], - dependencies: { - bar: "npm:baz@<0.0.2", - }, - }), - ); - await mkdir(join(package_dir, "baz")); - const baz_package = JSON.stringify({ - name: "baz", - version: "0.0.1", + it("should handle `workspace:*` on both root & child", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + bar: "workspace:*", + }, + }), + ); + await mkdir(join(ctx.package_dir, "packages", "bar"), { recursive: true }); + const bar_package = JSON.stringify({ + name: "bar", + version: "0.1.2", + }); + await writeFile(join(ctx.package_dir, "packages", "bar", "package.json"), bar_package); + await mkdir(join(ctx.package_dir, "packages", "baz"), { recursive: true }); + const baz_package = JSON.stringify({ + name: "baz", + version: "1.2.3", + devDependencies: { + bar: "workspace:*", + }, + }); + await writeFile(join(ctx.package_dir, "packages", "baz", "package.json"), baz_package); + const { + stdout: stdout1, + stderr: stderr1, + exited: exited1, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err1 = await new Response(stderr1).text(); + expect(err1).not.toContain("error:"); + expect(err1).toContain("Saved lockfile"); + const out1 = await new Response(stdout1).text(); + expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ bar@workspace:packages/bar`, + "", + "2 packages installed", + ]); + expect(await exited1).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar", "baz"]); + expect(await readlink(join(ctx.package_dir, "node_modules", "bar"))).toBeWorkspaceLink( + join("..", "packages", "bar"), + ); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); + expect(await readlink(join(ctx.package_dir, "node_modules", "baz"))).toBeWorkspaceLink( + join("..", "packages", "baz"), + ); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "baz", "package.json")).text()).toEqual(baz_package); + await access(join(ctx.package_dir, "bun.lockb")); + // Perform `bun install` again but with lockfile from before + await rm(join(ctx.package_dir, "node_modules"), { force: true, recursive: true }); + const { + stdout: stdout2, + stderr: stderr2, + exited: exited2, + } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + const err2 = await new Response(stderr2).text(); + expect(err2).not.toContain("error:"); + const out2 = await new Response(stdout2).text(); + expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + `+ bar@workspace:packages/bar`, + "", + "2 packages installed", + ]); + expect(await exited2).toBe(0); + expect(urls.sort()).toBeEmpty(); + expect(ctx.requested).toBe(0); + expect(await readdirSorted(join(ctx.package_dir, "node_modules"))).toEqual([".cache", "bar", "baz"]); + expect(await readlink(join(ctx.package_dir, "node_modules", "bar"))).toBeWorkspaceLink( + join("..", "packages", "bar"), + ); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "bar"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); + expect(await readlink(join(ctx.package_dir, "node_modules", "baz"))).toBeWorkspaceLink( + join("..", "packages", "baz"), + ); + expect(await readdirSorted(join(ctx.package_dir, "node_modules", "baz"))).toEqual(["package.json"]); + expect(await file(join(ctx.package_dir, "node_modules", "baz", "package.json")).text()).toEqual(baz_package); + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - await writeFile(join(package_dir, "baz", "package.json"), baz_package); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@workspace:baz", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar", "baz"]); - expect(await readlink(join(package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "baz")); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).text()).toEqual(baz_package); - await access(join(package_dir, "bun.lockb")); -}); -it("should override child npm dependency by matching workspace", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - workspaces: ["*"], - }), - ); - await mkdir(join(package_dir, "bar")); - const bar_package = JSON.stringify({ - name: "bar", - version: "0.0.1", - }); - await writeFile(join(package_dir, "bar", "package.json"), bar_package); - await mkdir(join(package_dir, "baz")); - await writeFile( - join(package_dir, "baz", "package.json"), - JSON.stringify({ - name: "baz", - version: "0.1.0", - dependencies: { - bar: "*", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar", "baz"]); - expect(await readlink(join(package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "bar")); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); - expect(await readlink(join(package_dir, "node_modules", "baz"))).toBeWorkspaceLink(join("..", "baz")); - expect(await readdirSorted(join(package_dir, "node_modules", "baz"))).toEqual(["package.json"]); - await access(join(package_dir, "bun.lockb")); -}); + it("should install peer dependencies from root package", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler(ctx, dummyRegistryForContext(ctx, urls)); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + peerDependencies: { + bar: "0.0.2", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + env, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + "+ bar@0.0.2", + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}bar`, `${ctx.registry_url}bar-0.0.2.tgz`]); + expect(ctx.requested).toBe(2); -it("should not override child npm dependency by workspace with mismatched version", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - workspaces: ["*"], - }), - ); - await mkdir(join(package_dir, "bar")); - const bar_package = JSON.stringify({ - name: "bar", - version: "0.0.1", + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - await writeFile(join(package_dir, "bar", "package.json"), bar_package); - await mkdir(join(package_dir, "baz")); - await writeFile( - join(package_dir, "baz", "package.json"), - JSON.stringify({ - name: "baz", - version: "0.1.0", - dependencies: { - bar: "^0.0.2", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "3 packages installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); - expect(requested).toBe(2); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar", "baz"]); - expect(await readlink(join(package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "bar")); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); - expect(await readlink(join(package_dir, "node_modules", "baz"))).toBeWorkspaceLink(join("..", "baz")); - expect(await readdirSorted(join(package_dir, "node_modules", "baz", "node_modules"))).toEqual(["bar"]); - expect(await readdirSorted(join(package_dir, "node_modules", "baz", "node_modules", "bar"))).toEqual([ - "package.json", - ]); - expect(await file(join(package_dir, "node_modules", "baz", "node_modules", "bar", "package.json")).json()).toEqual({ - name: "bar", - version: "0.0.2", - }); - await access(join(package_dir, "bun.lockb")); -}); -it("should override @scoped child npm dependency by matching workspace", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - workspaces: ["packages/*"], - }), - ); - await mkdir(join(package_dir, "packages", "moo-bar"), { recursive: true }); - const bar_package = JSON.stringify({ - name: "@moo/bar", - version: "1.2.3", - }); - await writeFile(join(package_dir, "packages", "moo-bar", "package.json"), bar_package); - await mkdir(join(package_dir, "packages", "moo-baz"), { recursive: true }); - await writeFile( - join(package_dir, "packages", "moo-baz", "package.json"), - JSON.stringify({ - name: "@moo/baz", - dependencies: { - "@moo/bar": "1.x", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "@moo"]); - expect(await readdirSorted(join(package_dir, "node_modules", "@moo"))).toEqual(["bar", "baz"]); - expect(await readlink(join(package_dir, "node_modules", "@moo", "bar"))).toBeWorkspaceLink( - join("..", "..", "packages", "moo-bar"), - ); - expect(await file(join(package_dir, "node_modules", "@moo", "bar", "package.json")).text()).toEqual(bar_package); - expect(await readlink(join(package_dir, "node_modules", "@moo", "baz"))).toBeWorkspaceLink( - join("..", "..", "packages", "moo-baz"), - ); - expect(await readdirSorted(join(package_dir, "node_modules", "@moo", "baz"))).toEqual(["package.json"]); - await access(join(package_dir, "bun.lockb")); -}); + it("should install correct version of peer dependency from root package", async () => { + await withContext(defaultOpts, async ctx => { + const urls: string[] = []; + setContextHandler( + ctx, + dummyRegistryForContext(ctx, urls, { + "0.0.3": { as: "0.0.3" }, + "0.0.5": { as: "0.0.5" }, + }), + ); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + dependencies: { + baz: "0.0.3", + }, + peerDependencies: { + baz: "0.0.5", + }, + }), + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + env, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + }); + const err = await stderr.text(); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ + expect.stringContaining("bun install v1."), + "", + expect.stringContaining("+ baz@0.0.3"), + "", + "1 package installed", + ]); + expect(await exited).toBe(0); + expect(urls.sort()).toEqual([`${ctx.registry_url}baz`, `${ctx.registry_url}baz-0.0.3.tgz`]); + expect(ctx.requested).toBe(2); -it("should override aliased child npm dependency by matching workspace", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - workspaces: ["packages/*"], - }), - ); - await mkdir(join(package_dir, "packages", "bar"), { recursive: true }); - const bar_package = JSON.stringify({ - name: "@moo/bar", - version: "0.0.1", + await access(join(ctx.package_dir, "bun.lockb")); + }); }); - await writeFile(join(package_dir, "packages", "bar", "package.json"), bar_package); - await mkdir(join(package_dir, "packages", "baz"), { recursive: true }); - await writeFile( - join(package_dir, "packages", "baz", "package.json"), - JSON.stringify({ - name: "baz", - version: "0.1.0", - dependencies: { - bar: "npm:@moo/bar@*", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "@moo", "bar", "baz"]); - expect(await readdirSorted(join(package_dir, "node_modules", "@moo"))).toEqual(["bar"]); - expect(await readlink(join(package_dir, "node_modules", "@moo", "bar"))).toBeWorkspaceLink( - join("..", "..", "packages", "bar"), - ); - expect(await file(join(package_dir, "node_modules", "@moo", "bar", "package.json")).text()).toEqual(bar_package); - expect(await readlink(join(package_dir, "node_modules", "baz"))).toBeWorkspaceLink(join("..", "packages", "baz")); - expect(await readdirSorted(join(package_dir, "packages", "baz"))).toEqual(["package.json"]); - expect(await readlink(join(package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "packages", "bar")); - await access(join(package_dir, "bun.lockb")); -}); -it("should handle `workspace:` with semver range", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - workspaces: ["bar", "baz"], - }), - ); - await mkdir(join(package_dir, "bar")); - const bar_package = JSON.stringify({ - name: "bar", - version: "0.0.1", - }); - await writeFile(join(package_dir, "bar", "package.json"), bar_package); - await mkdir(join(package_dir, "baz")); - await writeFile( - join(package_dir, "baz", "package.json"), - JSON.stringify({ - name: "baz", - version: "0.1.0", - dependencies: { - bar: "workspace:~0.0.1", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar", "baz"]); - expect(await readlink(join(package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "bar")); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); - expect(await readlink(join(package_dir, "node_modules", "baz"))).toBeWorkspaceLink(join("..", "baz")); - expect(await readdirSorted(join(package_dir, "node_modules", "baz"))).toEqual(["package.json"]); - await access(join(package_dir, "bun.lockb")); -}); + describe("Registry URLs", () => { + // Some of the non failing URLs are invalid, but bun's URL parser ignores + // the validation error and returns a valid serialized URL anyway. + const registryURLs: [url: string, fails: boolean | -1][] = [ + ["asdfghjklqwertyuiop", true], + [" ", true], + ["::::::::::::::::", true], + ["https://ex ample.org/", true], + ["example", true], + ["https://example.com:demo", true], + ["http://[www.example.com]/", true], + ["c:a", true], + ["https://registry.npmjs.org/", false], + ["http://artifactory.xxx.yyy/artifactory/api/npm/my-npm/", false], // https://github.com/oven-sh/bun/issues/3899 + ["http://artifactory.xxx.yyy/artifactory/api/npm/my-npm", false], // https://github.com/oven-sh/bun/issues/5368 + // ["", true], + ["https:example.org", false], + ["https://////example.com///", false], + ["https://example.com/https:example.org", false], + ["https://example.com/[]?[]#[]", false], + ["http://example/%?%#%", false], + ["c:", true], + ["c:/", -1], + ["http://點看", false], // gets converted to punycode + ["http://xn--c1yn36f/", false], + ]; -it("should handle `workspace:` with alias & @scope", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - workspaces: ["packages/*"], - }), - ); - await mkdir(join(package_dir, "packages", "bar"), { recursive: true }); - const bar_package = JSON.stringify({ - name: "@moo/bar", - version: "0.1.2", - }); - await writeFile(join(package_dir, "packages", "bar", "package.json"), bar_package); - await mkdir(join(package_dir, "packages", "baz"), { recursive: true }); - await writeFile( - join(package_dir, "packages", "baz", "package.json"), - JSON.stringify({ - name: "@moz/baz", - dependencies: { - "@moz/bar": "workspace:@moo/bar@>=0.1", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "2 packages installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "@moo", "@moz"]); - expect(await readdirSorted(join(package_dir, "node_modules", "@moo"))).toEqual(["bar"]); - expect(await readlink(join(package_dir, "node_modules", "@moo", "bar"))).toBeWorkspaceLink( - join("..", "..", "packages", "bar"), - ); - expect(await file(join(package_dir, "node_modules", "@moo", "bar", "package.json")).text()).toEqual(bar_package); - expect(await readdirSorted(join(package_dir, "node_modules", "@moz"))).toEqual(["bar", "baz"]); - expect(await readlink(join(package_dir, "node_modules", "@moz", "baz"))).toBeWorkspaceLink( - join("..", "..", "packages", "baz"), - ); - expect(await readlink(join(package_dir, "node_modules", "@moz", "bar"))).toBeWorkspaceLink( - join("..", "..", "packages", "bar"), - ); - expect(await readdirSorted(join(package_dir, "packages", "baz"))).toEqual(["package.json"]); - await access(join(package_dir, "bun.lockb")); -}); + for (const entry of registryURLs) { + const regURL = entry[0]; + const fails = entry[1]; -it("should handle `workspace:*` on both root & child", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - workspaces: ["packages/*"], - dependencies: { - bar: "workspace:*", - }, - }), - ); - await mkdir(join(package_dir, "packages", "bar"), { recursive: true }); - const bar_package = JSON.stringify({ - name: "bar", - version: "0.1.2", - }); - await writeFile(join(package_dir, "packages", "bar", "package.json"), bar_package); - await mkdir(join(package_dir, "packages", "baz"), { recursive: true }); - const baz_package = JSON.stringify({ - name: "baz", - version: "1.2.3", - devDependencies: { - bar: "workspace:*", - }, - }); - await writeFile(join(package_dir, "packages", "baz", "package.json"), baz_package); - const { - stdout: stdout1, - stderr: stderr1, - exited: exited1, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err1 = await new Response(stderr1).text(); - expect(err1).not.toContain("error:"); - expect(err1).toContain("Saved lockfile"); - const out1 = await new Response(stdout1).text(); - expect(out1.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ bar@workspace:packages/bar`, - "", - "2 packages installed", - ]); - expect(await exited1).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar", "baz"]); - expect(await readlink(join(package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "packages", "bar")); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); - expect(await readlink(join(package_dir, "node_modules", "baz"))).toBeWorkspaceLink(join("..", "packages", "baz")); - expect(await readdirSorted(join(package_dir, "node_modules", "baz"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "baz", "package.json")).text()).toEqual(baz_package); - await access(join(package_dir, "bun.lockb")); - // Perform `bun install` again but with lockfile from before - await rm(join(package_dir, "node_modules"), { force: true, recursive: true }); - const { - stdout: stdout2, - stderr: stderr2, - exited: exited2, - } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, - }); - const err2 = await new Response(stderr2).text(); - expect(err2).not.toContain("error:"); - const out2 = await new Response(stdout2).text(); - expect(out2.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - `+ bar@workspace:packages/bar`, - "", - "2 packages installed", - ]); - expect(await exited2).toBe(0); - expect(urls.sort()).toBeEmpty(); - expect(requested).toBe(0); - expect(await readdirSorted(join(package_dir, "node_modules"))).toEqual([".cache", "bar", "baz"]); - expect(await readlink(join(package_dir, "node_modules", "bar"))).toBeWorkspaceLink(join("..", "packages", "bar")); - expect(await readdirSorted(join(package_dir, "node_modules", "bar"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "bar", "package.json")).text()).toEqual(bar_package); - expect(await readlink(join(package_dir, "node_modules", "baz"))).toBeWorkspaceLink(join("..", "packages", "baz")); - expect(await readdirSorted(join(package_dir, "node_modules", "baz"))).toEqual(["package.json"]); - expect(await file(join(package_dir, "node_modules", "baz", "package.json")).text()).toEqual(baz_package); - await access(join(package_dir, "bun.lockb")); -}); + it( + `should ${fails ? "fail" : "handle"} joining registry and package URLs (${regURL})`, + async () => { + await withContext(defaultOpts, async ctx => { + await writeFile(join(ctx.package_dir, "bunfig.toml"), `[install]\ncache = false\nregistry = "${regURL}"`); -it("should install peer dependencies from root package", async () => { - const urls: string[] = []; - setHandler(dummyRegistry(urls)); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - peerDependencies: { - bar: "0.0.2", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - env, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - "+ bar@0.0.2", - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/bar`, `${root_url}/bar-0.0.2.tgz`]); - expect(requested).toBe(2); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + notapackage: "0.0.2", + }, + }), + ); - await access(join(package_dir, "bun.lockb")); -}); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(await stdout.text()).toEqual(expect.stringContaining("bun install v1.")); -it("should install correct version of peer dependency from root package", async () => { - const urls: string[] = []; - setHandler( - dummyRegistry(urls, { - "0.0.3": { as: "0.0.3" }, - "0.0.5": { as: "0.0.5" }, - }), - ); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - dependencies: { - baz: "0.0.3", - }, - peerDependencies: { - baz: "0.0.5", - }, - }), - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - env, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - }); - const err = await stderr.text(); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ - expect.stringContaining("bun install v1."), - "", - expect.stringContaining("+ baz@0.0.3"), - "", - "1 package installed", - ]); - expect(await exited).toBe(0); - expect(urls.sort()).toEqual([`${root_url}/baz`, `${root_url}/baz-0.0.3.tgz`]); - expect(requested).toBe(2); + const err = await stderr.text(); - await access(join(package_dir, "bun.lockb")); -}); + if (fails === -1) { + expect(err).toContain(`Registry URL must be http:// or https://`); + } else if (fails) { + expect(err).toContain(`Failed to join registry "${regURL}" and package "notapackage" URLs`); + } else { + expect(err).toContain("error: notapackage@0.0.2 failed to resolve"); + } + // fails either way, since notapackage is, well, not a real package. + expect(await exited).not.toBe(0); + }); + }, + Infinity, + ); + } -describe("Registry URLs", () => { - // Some of the non failing URLs are invalid, but bun's URL parser ignores - // the validation error and returns a valid serialized URL anyway. - const registryURLs: [url: string, fails: boolean | -1][] = [ - ["asdfghjklqwertyuiop", true], - [" ", true], - ["::::::::::::::::", true], - ["https://ex ample.org/", true], - ["example", true], - ["https://example.com:demo", true], - ["http://[www.example.com]/", true], - ["c:a", true], - ["https://registry.npmjs.org/", false], - ["http://artifactory.xxx.yyy/artifactory/api/npm/my-npm/", false], // https://github.com/oven-sh/bun/issues/3899 - ["http://artifactory.xxx.yyy/artifactory/api/npm/my-npm", false], // https://github.com/oven-sh/bun/issues/5368 - // ["", true], - ["https:example.org", false], - ["https://////example.com///", false], - ["https://example.com/https:example.org", false], - ["https://example.com/[]?[]#[]", false], - ["http://example/%?%#%", false], - ["c:", true], - ["c:/", -1], - ["http://點看", false], // gets converted to punycode - ["http://xn--c1yn36f/", false], - ]; + it("shouldn't fail joining invalid registry and package URLs for optional dependencies", async () => { + await withContext(defaultOpts, async ctx => { + const regURL = "asdfghjklqwertyuiop"; - for (const entry of registryURLs) { - const regURL = entry[0]; - const fails = entry[1]; - - it( - `should ${fails ? "fail" : "handle"} joining registry and package URLs (${regURL})`, - async () => { - await writeFile(join(package_dir, "bunfig.toml"), `[install]\ncache = false\nregistry = "${regURL}"`); + await writeFile(join(ctx.package_dir, "bunfig.toml"), `[install]\ncache = false\nregistry = "${regURL}"`); await writeFile( - join(package_dir, "package.json"), + join(ctx.package_dir, "package.json"), JSON.stringify({ name: "foo", version: "0.0.1", - dependencies: { + optionalDependencies: { notapackage: "0.0.2", }, }), @@ -8223,403 +8693,381 @@ describe("Registry URLs", () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], - cwd: package_dir, + cwd: ctx.package_dir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); - expect(await stdout.text()).toEqual(expect.stringContaining("bun install v1.")); + expect(await stdout.text()).not.toBeEmpty(); const err = await stderr.text(); - if (fails === -1) { - expect(err).toContain(`Registry URL must be http:// or https://`); - } else if (fails) { - expect(err).toContain(`Failed to join registry "${regURL}" and package "notapackage" URLs`); - } else { - expect(err).toContain("error: notapackage@0.0.2 failed to resolve"); - } - // fails either way, since notapackage is, well, not a real package. - expect(await exited).not.toBe(0); - }, - Infinity, - ); - } + expect(err).toContain(`Failed to join registry "${regURL}" and package "notapackage" URLs`); - it("shouldn't fail joining invalid registry and package URLs for optional dependencies", async () => { - const regURL = "asdfghjklqwertyuiop"; - - await writeFile(join(package_dir, "bunfig.toml"), `[install]\ncache = false\nregistry = "${regURL}"`); - - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - optionalDependencies: { - notapackage: "0.0.2", - }, - }), - ); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + expect(await exited).toBe(0); + }); }); - expect(await stdout.text()).not.toBeEmpty(); - const err = await stderr.text(); + // TODO: This test should fail if the param `warn_on_error` is true in + // `(install.zig).NetworkTask.forManifest()`. Unfortunately, that + // code never gets run for peer dependencies unless you do some package + // manifest magic. I doubt it'd ever fail, but having a dedicated + // test would be nice. + test.todo("shouldn't fail joining invalid registry and package URLs for peer dependencies", async () => { + const regURL = "asdfghjklqwertyuiop"; - expect(err).toContain(`Failed to join registry "${regURL}" and package "notapackage" URLs`); + await writeFile(join(ctx.package_dir, "bunfig.toml"), `[install]\ncache = false\nregistry = "${regURL}"`); - expect(await exited).toBe(0); - }); + await writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + peerDependencies: { + notapackage: "0.0.2", + }, + }), + ); - // TODO: This test should fail if the param `warn_on_error` is true in - // `(install.zig).NetworkTask.forManifest()`. Unfortunately, that - // code never gets run for peer dependencies unless you do some package - // manifest magic. I doubt it'd ever fail, but having a dedicated - // test would be nice. - test.todo("shouldn't fail joining invalid registry and package URLs for peer dependencies", async () => { - const regURL = "asdfghjklqwertyuiop"; + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stdin: "pipe", + stderr: "pipe", + env, + }); + expect(await stdout.text()).not.toBeEmpty(); - await writeFile(join(package_dir, "bunfig.toml"), `[install]\ncache = false\nregistry = "${regURL}"`); + const err = await stderr.text(); - await writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - peerDependencies: { - notapackage: "0.0.2", - }, - }), - ); + expect(err).toContain(`Failed to join registry "${regURL}" and package "notapackage" URLs`); + expect(err).toContain("warn: InvalidURL"); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stdin: "pipe", - stderr: "pipe", - env, + expect(await exited).toBe(0); }); - expect(await stdout.text()).not.toBeEmpty(); - - const err = await stderr.text(); - - expect(err).toContain(`Failed to join registry "${regURL}" and package "notapackage" URLs`); - expect(err).toContain("warn: InvalidURL"); - - expect(await exited).toBe(0); }); -}); -it("should ensure read permissions of all extracted files", async () => { - await Promise.all([ - cp(join(import.meta.dir, "pkg-only-owner-2.2.2.tgz"), join(package_dir, "pkg-only-owner-2.2.2.tgz")), - writeFile( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - "pkg-only-owner": "file:pkg-only-owner-2.2.2.tgz", - }, - }), - ), - ]); - - await runBunInstall(env, package_dir); - - expect((await stat(join(package_dir, "node_modules", "pkg-only-owner", "package.json"))).mode & 0o444).toBe(0o444); - expect((await stat(join(package_dir, "node_modules", "pkg-only-owner", "src", "index.js"))).mode & 0o444).toBe(0o444); -}); - -it("should handle @scoped name that contains tilde, issue#7045", async () => { - await writeFile( - join(package_dir, "bunfig.toml"), - ` -[install] -cache = false -`, - ); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install", "@~39/empty"], - cwd: package_dir, - stdin: null, - stdout: "pipe", - stderr: "pipe", - env, - }); - expect(await stderr.text()).toContain("Saved lockfile"); - expect(await stdout.text()).toContain("installed @~39/empty@1.0.0"); - expect(await exited).toBe(0); -}); - -it("should handle modified git resolutions in bun.lock", async () => { - // install-test-8 has a dependency but because it's not in the lockfile - // it won't be included in the install. - await Promise.all([ - write( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - version: "0.0.1", - dependencies: { - "jquery": "3.7.1", - }, - }), - ), - write( - join(package_dir, "bun.lock"), - JSON.stringify({ - "lockfileVersion": 0, - "configVersion": 1, - "workspaces": { - "": { - "dependencies": { - "jquery": "3.7.1", + it("should ensure read permissions of all extracted files", async () => { + await withContext(defaultOpts, async ctx => { + await Promise.all([ + cp(join(import.meta.dir, "pkg-only-owner-2.2.2.tgz"), join(ctx.package_dir, "pkg-only-owner-2.2.2.tgz")), + writeFile( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", + dependencies: { + "pkg-only-owner": "file:pkg-only-owner-2.2.2.tgz", }, - }, - }, - "packages": { - "jquery": [ - "jquery@git+ssh://git@github.com/dylan-conway/install-test-8.git#3a1288830817d13da39e9231302261896f8721ea", - {}, - "3a1288830817d13da39e9231302261896f8721ea", - ], - }, - }), - ), - ]); + }), + ), + ]); - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stderr: "pipe", - env, + await runBunInstall(env, ctx.package_dir); + + expect((await stat(join(ctx.package_dir, "node_modules", "pkg-only-owner", "package.json"))).mode & 0o444).toBe( + 0o444, + ); + expect( + (await stat(join(ctx.package_dir, "node_modules", "pkg-only-owner", "src", "index.js"))).mode & 0o444, + ).toBe(0o444); + }); }); - const err = await stderr.text(); - const out = await stdout.text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).not.toContain("error:"); - - expect(out).toContain("1 package installed"); - expect(await exited).toBe(0); - - expect( - (await file(join(package_dir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234"), - ).toMatchSnapshot(); -}); - -it("should read install.saveTextLockfile from bunfig.toml", async () => { - await Promise.all([ - write( - join(package_dir, "bunfig.toml"), - ` -[install] -cache = false -registry = "http://localhost:${getPort()}/" -saveTextLockfile = true -`, - ), - write( - join(package_dir, "package.json"), - JSON.stringify({ - name: "foo", - workspaces: ["packages/*"], - dependencies: { - "pkg-one": "workspace:*", - }, - }), - ), - write( - join(package_dir, "packages", "pkg1", "package.json"), - JSON.stringify({ - name: "pkg-one", - version: "1.0.0", - }), - ), - ]); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stderr: "pipe", - env, + it("should handle @scoped name that contains tilde, issue#7045", async () => { + await withContext(defaultOpts, async ctx => { + await writeFile( + join(ctx.package_dir, "bunfig.toml"), + ` + [install] + cache = false + `, + ); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install", "@~39/empty"], + cwd: ctx.package_dir, + stdin: null, + stdout: "pipe", + stderr: "pipe", + env, + }); + expect(await stderr.text()).toContain("Saved lockfile"); + expect(await stdout.text()).toContain("installed @~39/empty@1.0.0"); + expect(await exited).toBe(0); + }); }); - const err = await stderr.text(); - expect(err).not.toContain("error:"); - expect(err).toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out).toContain("Checked 3 installs across 2 packages (no changes)"); - - expect(await exited).toBe(0); - expect(await Bun.file(join(package_dir, "node_modules", "pkg-one", "package.json")).json()).toEqual({ - name: "pkg-one", - version: "1.0.0", - }); - expect(await exists(join(package_dir, "bun.lockb"))).toBeFalse(); - expect(await file(join(package_dir, "bun.lock")).text()).toMatchInlineSnapshot(` - "{ - "lockfileVersion": 1, - "configVersion": 1, - "workspaces": { - "": { - "name": "foo", - "dependencies": { - "pkg-one": "workspace:*", - }, - }, - "packages/pkg1": { - "name": "pkg-one", - "version": "1.0.0", - }, - }, - "packages": { - "pkg-one": ["pkg-one@workspace:packages/pkg1"], - } - } - " - `); -}); - -test("providing invalid url in lockfile does not crash", async () => { - await Promise.all([ - write( - join(package_dir, "package.json"), - JSON.stringify({ - dependencies: { - "jquery": "3.7.1", - }, - }), - ), - write( - join(package_dir, "bun.lock"), - textLockfile(0, { - "workspaces": { - "": { - "dependencies": { - "jquery": "3.7.1", - }, - }, - }, - "packages": { - "jquery": [ - "jquery@3.7.1", - "invalid-url", - {}, - "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", - ], - }, - }), - ), - ]); - - const { stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stderr: "pipe", - env, - }); - - const err = await stderr.text(); - expect(err).toContain( - 'error: Expected tarball URL to start with https:// or http://, got "invalid-url" while fetching package "jquery"', - ); - expect(await exited).toBe(1); -}); - -test("optional dependencies do not need to be resolvable in text lockfile", async () => { - await Promise.all([ - write( - join(package_dir, "package.json"), - JSON.stringify({ - optionalDependencies: { - jquery: "3.7.1", - }, - }), - ), - write( - join(package_dir, "bun.lock"), - textLockfile(0, { - "workspaces": { - "": { - "optionalDependencies": { - "jquery": "3.7.1", - }, - }, - }, - "packages": {}, - }), - ), - ]); - - const { stdout, stderr, exited } = spawn({ - cmd: [bunExe(), "install"], - cwd: package_dir, - stdout: "pipe", - stderr: "pipe", - env, - }); - - const err = await stderr.text(); - expect(err).not.toContain("Saved lockfile"); - const out = await stdout.text(); - expect(out).not.toContain("1 package installed"); - - expect(await exited).toBe(0); -}); - -test("non-optional dependencies need to be resolvable in text lockfile", async () => { - await Promise.all([ - write( - join(package_dir, "package.json"), - JSON.stringify({ - dependencies: { - jquery: "3.7.1", - }, - }), - ), - write( - join(package_dir, "bun.lock"), - textLockfile(0, { - workspaces: { - "": { + test.serial("should handle modified git resolutions in bun.lock", async () => { + await withContext(defaultOpts, async ctx => { + // install-test-8 has a dependency but because it's not in the lockfile + // it won't be included in the install. + await Promise.all([ + write( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + version: "0.0.1", dependencies: { "jquery": "3.7.1", }, - }, - }, - packages: {}, - }), - ), - ]); + }), + ), + write( + join(ctx.package_dir, "bun.lock"), + JSON.stringify({ + "lockfileVersion": 0, + "configVersion": 1, + "workspaces": { + "": { + "dependencies": { + "jquery": "3.7.1", + }, + }, + }, + "packages": { + "jquery": [ + "jquery@git+ssh://git@github.com/dylan-conway/install-test-8.git#3a1288830817d13da39e9231302261896f8721ea", + {}, + "3a1288830817d13da39e9231302261896f8721ea", + ], + }, + }), + ), + ]); - const { stdout, stderr, exited } = spawn({ - // --production to fail early - cmd: [bunExe(), "install", "--production"], - cwd: package_dir, - stdout: "pipe", - stderr: "pipe", - env, + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + const err = await stderr.text(); + const out = await stdout.text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).not.toContain("error:"); + + expect(out).toContain("1 package installed"); + expect(await exited).toBe(0); + + expect( + (await file(join(ctx.package_dir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234"), + ).toMatchSnapshot(); + }); }); - const err = await stderr.text(); - expect(err).not.toContain("Saved lockfile"); - expect(err).toContain("error: Failed to resolve root prod dependency 'jquery'"); - const out = await stdout.text(); - expect(out).not.toContain("1 package installed"); + it("should read install.saveTextLockfile from bunfig.toml", async () => { + await withContext(defaultOpts, async ctx => { + await Promise.all([ + write( + join(ctx.package_dir, "bunfig.toml"), + ` + [install] + cache = false + registry = "${ctx.registry_url}" + saveTextLockfile = true + `, + ), + write( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + name: "foo", + workspaces: ["packages/*"], + dependencies: { + "pkg-one": "workspace:*", + }, + }), + ), + write( + join(ctx.package_dir, "packages", "pkg1", "package.json"), + JSON.stringify({ + name: "pkg-one", + version: "1.0.0", + }), + ), + ]); - expect(await exited).toBe(1); + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + const err = await stderr.text(); + expect(err).not.toContain("error:"); + expect(err).toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out).toContain("Checked 3 installs across 2 packages (no changes)"); + + expect(await exited).toBe(0); + expect(await Bun.file(join(ctx.package_dir, "node_modules", "pkg-one", "package.json")).json()).toEqual({ + name: "pkg-one", + version: "1.0.0", + }); + expect(await exists(join(ctx.package_dir, "bun.lockb"))).toBeFalse(); + expect(await file(join(ctx.package_dir, "bun.lock")).text()).toMatchInlineSnapshot(` + "{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "foo", + "dependencies": { + "pkg-one": "workspace:*", + }, + }, + "packages/pkg1": { + "name": "pkg-one", + "version": "1.0.0", + }, + }, + "packages": { + "pkg-one": ["pkg-one@workspace:packages/pkg1"], + } + } + " + `); + }); + }); + + test("providing invalid url in lockfile does not crash", async () => { + await withContext(defaultOpts, async ctx => { + await Promise.all([ + write( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + dependencies: { + "jquery": "3.7.1", + }, + }), + ), + write( + join(ctx.package_dir, "bun.lock"), + textLockfile(0, { + "workspaces": { + "": { + "dependencies": { + "jquery": "3.7.1", + }, + }, + }, + "packages": { + "jquery": [ + "jquery@3.7.1", + "invalid-url", + {}, + "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", + ], + }, + }), + ), + ]); + + const { stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stderr: "pipe", + env, + }); + + const err = await stderr.text(); + expect(err).toContain( + 'error: Expected tarball URL to start with https:// or http://, got "invalid-url" while fetching package "jquery"', + ); + expect(await exited).toBe(1); + }); + }); + + test("optional dependencies do not need to be resolvable in text lockfile", async () => { + await withContext(defaultOpts, async ctx => { + await Promise.all([ + write( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + optionalDependencies: { + jquery: "3.7.1", + }, + }), + ), + write( + join(ctx.package_dir, "bun.lock"), + textLockfile(0, { + "workspaces": { + "": { + "optionalDependencies": { + "jquery": "3.7.1", + }, + }, + }, + "packages": {}, + }), + ), + ]); + + const { stdout, stderr, exited } = spawn({ + cmd: [bunExe(), "install"], + cwd: ctx.package_dir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + const err = await stderr.text(); + expect(err).not.toContain("Saved lockfile"); + const out = await stdout.text(); + expect(out).not.toContain("1 package installed"); + + expect(await exited).toBe(0); + }); + }); + + test("non-optional dependencies need to be resolvable in text lockfile", async () => { + await withContext(defaultOpts, async ctx => { + await Promise.all([ + write( + join(ctx.package_dir, "package.json"), + JSON.stringify({ + dependencies: { + jquery: "3.7.1", + }, + }), + ), + write( + join(ctx.package_dir, "bun.lock"), + textLockfile(0, { + workspaces: { + "": { + dependencies: { + "jquery": "3.7.1", + }, + }, + }, + packages: {}, + }), + ), + ]); + + const { stdout, stderr, exited } = spawn({ + // --production to fail early + cmd: [bunExe(), "install", "--production"], + cwd: ctx.package_dir, + stdout: "pipe", + stderr: "pipe", + env, + }); + + const err = await stderr.text(); + expect(err).not.toContain("Saved lockfile"); + expect(err).toContain("error: Failed to resolve root prod dependency 'jquery'"); + const out = await stdout.text(); + expect(out).not.toContain("1 package installed"); + + expect(await exited).toBe(1); + }); + }); }); diff --git a/test/cli/install/dummy.registry.ts b/test/cli/install/dummy.registry.ts index cc22c7d9fa..ff1e622bfb 100644 --- a/test/cli/install/dummy.registry.ts +++ b/test/cli/install/dummy.registry.ts @@ -2,6 +2,30 @@ * This file can be directly run * * PACKAGE_DIR_TO_USE=(realpath .) bun test/cli/install/dummy.registry.ts + * + * ## Concurrent Test Support + * + * This module supports running tests concurrently by using URL prefixes to isolate + * each test's registry requests. Each test gets a unique context with: + * - Its own handler for registry requests + * - Its own package_dir (temp directory) + * - Its own request counter + * - A unique registry URL with a prefix (e.g., http://localhost:PORT/test-123/) + * + * ### Usage for concurrent tests: + * ```typescript + * it("my test", async () => { + * const ctx = await createTestContext({ linker: "hoisted" }); + * try { + * const urls: string[] = []; + * setContextHandler(ctx, dummyRegistry(urls, ctx)); + * // Use ctx.package_dir, ctx.registry_url, ctx.requested + * await writeFile(join(ctx.package_dir, "package.json"), ...); + * } finally { + * destroyTestContext(ctx); + * } + * }); + * ``` */ import { file, Server } from "bun"; import { tmpdirSync } from "harness"; @@ -19,24 +43,202 @@ type Pkg = { tarball: string; }; }; -let handler: Handler; + let server: Server; -export let package_dir: string; -export let requested: number; export let root_url: string; export let check_npm_auth_type = { check: true }; -export async function write(path: string, content: string | object) { - if (!package_dir) throw new Error("writeToPackageDir() must be called in a test"); +// ============================================================================ +// Concurrent Test Context Support +// ============================================================================ - await Bun.write(join(package_dir, path), typeof content === "string" ? content : JSON.stringify(content)); +/** Global counter for generating unique test IDs */ +let testIdCounter = 0; + +/** + * Context for a single test, containing all per-test state. + * Use this for concurrent test execution. + */ +export interface TestContext { + /** Unique identifier for this test context (e.g., "test-1") */ + id: string; + /** The package directory for this test (a unique temp directory) */ + package_dir: string; + /** Number of requests made to this test's handler */ + requested: number; + /** The handler for this test's registry requests */ + handler: Handler; + /** The registry URL for this test (includes prefix, e.g., http://localhost:PORT/test-1/) */ + registry_url: string; } -export function read(path: string) { - return Bun.file(join(package_dir, path)); +/** Map of test ID prefix -> test context */ +const testContexts = new Map(); + +/** Default handler for unmatched requests */ +function defaultHandler(): Response { + return new Response("Tea Break~", { status: 418 }); } -export function dummyRegistry(urls: string[], info: any = { "0.0.2": {} }, numberOfTimesTo500PerURL = 0) { +/** + * Extract the test ID prefix from a URL path. + * URL format: /test-123/package-name or /test-123/@scope%2fpackage-name + */ +function extractTestPrefix(url: string): { prefix: string; remainingPath: string } | null { + const urlObj = new URL(url); + const path = urlObj.pathname; + + // Match /test-N/ followed by anything + const match = path.match(/^\/(test-\d+)(\/.*)?$/); + if (match) { + return { + prefix: match[1], + remainingPath: match[2] || "/", + }; + } + return null; +} + +/** + * Creates a new isolated test context for concurrent test execution. + * Each context has its own handler, package_dir, and request counter. + * + * The bunfig.toml is automatically created with the prefixed registry URL. + * + * @param opts - Optional configuration for the test context + * @returns A new TestContext that should be used for all test operations + */ +export async function createTestContext(opts?: { linker: "hoisted" | "isolated" }): Promise { + const id = `test-${++testIdCounter}`; + const pkg_dir = tmpdirSync(); + + const ctx: TestContext = { + id, + package_dir: pkg_dir, + requested: 0, + handler: defaultHandler, + registry_url: `${root_url}/${id}/`, + }; + + testContexts.set(id, ctx); + + // Create bunfig.toml with the prefixed registry URL + await writeFile( + join(pkg_dir, "bunfig.toml"), + ` +[install] +cache = false +registry = "${ctx.registry_url}" +saveTextLockfile = false +${opts ? `linker = "${opts.linker}"` : ""} +`, + ); + + return ctx; +} + +/** + * Cleans up a test context after the test is done. + * This removes the context from the registry so requests won't be routed to it. + */ +export function destroyTestContext(ctx: TestContext): void { + testContexts.delete(ctx.id); +} + +/** + * Sets the handler for a specific test context. + * The handler will receive all requests that have this context's URL prefix. + */ +export function setContextHandler(ctx: TestContext, newHandler: Handler): void { + ctx.handler = newHandler; +} + +/** + * Creates a dummy registry handler for a specific test context. + * This is the concurrent-safe version that uses the context's registry_url for tarballs. + * + * @param ctx - The test context (provides registry_url for tarball URLs) + * @param urls - Array to collect requested URLs (passed by reference) + * @param info - Package version info (default: { "0.0.2": {} }) + * @param numberOfTimesTo500PerURL - Number of times to return 500 before success (for retry testing) + */ +export function dummyRegistryForContext( + ctx: TestContext, + urls: string[], + info: any = { "0.0.2": {} }, + numberOfTimesTo500PerURL = 0, +): Handler { + let retryCountsByURL = new Map(); + const _handler: Handler = async request => { + urls.push(request.url); + const url = request.url.replaceAll("%2f", "/"); + + let status = 200; + + if (numberOfTimesTo500PerURL > 0) { + let currentCount = retryCountsByURL.get(request.url); + if (currentCount === undefined) { + retryCountsByURL.set(request.url, numberOfTimesTo500PerURL); + status = 500; + } else { + retryCountsByURL.set(request.url, currentCount - 1); + status = currentCount > 0 ? 500 : 200; + } + } + + expect(request.method).toBe("GET"); + if (url.endsWith(".tgz")) { + return new Response(file(join(import.meta.dir, basename(url).toLowerCase())), { status }); + } + expect(request.headers.get("accept")).toBe( + "application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*", + ); + if (check_npm_auth_type.check) { + expect(request.headers.get("npm-auth-type")).toBe(null); + } + expect(await request.text()).toBe(""); + + // For context-based requests, strip the test prefix + const urlObj = new URL(url); + const pathAfterPrefix = urlObj.pathname.replace(`/${ctx.id}/`, "/"); + const name = pathAfterPrefix.slice(1); // Remove leading slash + + const versions: Record = {}; + let version; + for (version in info) { + if (!/^[0-9]/.test(version)) continue; + versions[version] = { + name, + version, + dist: { + tarball: `${ctx.registry_url}${name}-${info[version].as ?? version}.tgz`, + }, + ...info[version], + }; + } + + return new Response( + JSON.stringify({ + name, + versions, + "dist-tags": { + latest: info.latest ?? version, + }, + }), + { status }, + ); + }; + return _handler; +} + +/** + * Creates a dummy registry handler (legacy version for backward compatibility). + * + * @param urls - Array to collect requested URLs (passed by reference) + * @param info - Package version info (default: { "0.0.2": {} }) + * @param numberOfTimesTo500PerURL - Number of times to return 500 before success (for retry testing) + */ +export function dummyRegistry(urls: string[], info: any = { "0.0.2": {} }, numberOfTimesTo500PerURL = 0): Handler { let retryCountsByURL = new Map(); const _handler: Handler = async request => { urls.push(request.url); @@ -96,19 +298,58 @@ export function dummyRegistry(urls: string[], info: any = { "0.0.2": {} }, numbe return _handler; } +// ============================================================================ +// Legacy API (for backward compatibility with non-concurrent tests) +// ============================================================================ + +/** @deprecated Use createTestContext() for concurrent tests */ +export let package_dir: string; + +/** @deprecated Use ctx.requested for concurrent tests */ +export let requested: number; + +/** Legacy handler for non-prefixed requests */ +let legacyHandler: Handler = defaultHandler; + +export async function write(path: string, content: string | object) { + if (!package_dir) throw new Error("writeToPackageDir() must be called in a test"); + + await Bun.write(join(package_dir, path), typeof content === "string" ? content : JSON.stringify(content)); +} + +export function read(path: string) { + return Bun.file(join(package_dir, path)); +} + +/** @deprecated Use setContextHandler() for concurrent tests */ export function setHandler(newHandler: Handler) { - handler = newHandler; + legacyHandler = newHandler; } function resetHandler() { - setHandler(() => new Response("Tea Break~", { status: 418 })); + setHandler(defaultHandler); } export function dummyBeforeAll() { server = Bun.serve({ async fetch(request) { + const url = request.url; + + // Check if this is a prefixed request (for concurrent tests) + const prefixInfo = extractTestPrefix(url); + if (prefixInfo) { + const ctx = testContexts.get(prefixInfo.prefix); + if (ctx) { + ctx.requested++; + return await ctx.handler(request); + } + // Unknown test prefix - return 404 + return new Response(`Unknown test prefix: ${prefixInfo.prefix}`, { status: 404 }); + } + + // Legacy non-prefixed request requested++; - return await handler(request); + return await legacyHandler(request); }, port: 0, }); @@ -117,6 +358,7 @@ export function dummyBeforeAll() { export function dummyAfterAll() { server.stop(); + testContexts.clear(); } export function getPort() { @@ -126,6 +368,8 @@ export function getPort() { let packageDirGetter: () => string = () => { return tmpdirSync(); }; + +/** @deprecated Use createTestContext() for concurrent tests */ export async function dummyBeforeEach(opts?: { linker: "hoisted" | "isolated" }) { resetHandler(); requested = 0; @@ -142,6 +386,7 @@ ${opts ? `linker = "${opts.linker}"` : ""} ); } +/** @deprecated Use destroyTestContext() for concurrent tests */ export async function dummyAfterEach() { resetHandler(); }