import { file, spawn, write } from "bun"; import { install_test_helpers } from "bun:internal-for-testing"; import { afterAll, beforeEach, describe, expect, setDefaultTimeout, test } from "bun:test"; import { copyFileSync, mkdirSync } from "fs"; import { cp, exists, lstat, mkdir, readlink, rm, writeFile } from "fs/promises"; import { assertManifestsPopulated, bunExe, bunEnv as env, isFlaky, isMacOS, isWindows, mergeWindowEnvs, readdirSorted, runBunInstall, runBunUpdate, stderrForInstall, tempDirWithFiles, tls, tmpdirSync, toBeValidBin, toHaveBins, toMatchNodeModulesAt, VerdaccioRegistry, writeShebangScript, } from "harness"; import { join, resolve } from "path"; const { parseLockfile } = install_test_helpers; expect.extend({ toBeValidBin, toHaveBins, toMatchNodeModulesAt, }); var registry: VerdaccioRegistry; var port: number; var packageDir: string; /** packageJson = join(packageDir, "package.json"); */ var packageJson: string; let users: Record = {}; setDefaultTimeout(1000 * 60 * 5); registry = new VerdaccioRegistry(); port = registry.port; await registry.start(); afterAll(async () => { await Bun.$`rm -f ${import.meta.dir}/htpasswd`.throws(false); registry.stop(); }); beforeEach(async () => { ({ packageDir, packageJson } = await registry.createTestDir({ bunfigOpts: { saveTextLockfile: false, linker: "hoisted" }, })); await Bun.$`rm -f ${import.meta.dir}/htpasswd`.throws(false); await Bun.$`rm -rf ${import.meta.dir}/packages/private-pkg-dont-touch`.throws(false); users = {}; env.BUN_INSTALL_CACHE_DIR = join(packageDir, ".bun-cache"); env.BUN_TMPDIR = env.TMPDIR = env.TEMP = join(packageDir, ".bun-tmp"); }); function registryUrl() { return registry.registryUrl(); } /** * Returns auth token */ async function generateRegistryUser(username: string, password: string): Promise { if (users[username]) { throw new Error("that user already exists"); } else users[username] = password; const url = `http://localhost:${port}/-/user/org.couchdb.user:${username}`; const user = { name: username, password: password, email: `${username}@example.com`, }; const response = await fetch(url, { method: "PUT", headers: { "Content-Type": "application/json", }, body: JSON.stringify(user), }); if (response.ok) { const data = await response.json(); return data.token; } else { throw new Error("Failed to create user:", response.statusText); } } describe("auto-install", () => { test("symlinks (and junctions) are created correctly in the install cache", async () => { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "--print", "require('is-number')"], cwd: packageDir, stdout: "pipe", stderr: "pipe", env: { ...env, BUN_INSTALL_CACHE_DIR: join(packageDir, ".bun-cache"), }, }); const out = await stdout.text(); expect(out).toMatchSnapshot(); const err = await stderr.text(); expect(err).not.toContain("error:"); expect(await exited).toBe(0); expect(resolve(await readlink(join(packageDir, ".bun-cache", "is-number", "2.0.0@@localhost@@@1")))).toBe( join(packageDir, ".bun-cache", "is-number@2.0.0@@localhost@@@1"), ); }); }); describe("certificate authority", () => { const mockRegistryFetch = function (opts?: any): (req: Request) => Promise { return async function (req: Request) { if (req.url.includes("no-deps")) { return new Response(Bun.file(join(import.meta.dir, "registry", "packages", "no-deps", "no-deps-1.0.0.tgz"))); } return new Response("OK", { status: 200 }); }; }; test("valid --cafile", async () => { using server = Bun.serve({ port: 0, fetch: mockRegistryFetch(), ...tls, }); await Promise.all([ write( packageJson, JSON.stringify({ name: "foo", version: "1.1.1", dependencies: { "no-deps": `https://localhost:${server.port}/no-deps-1.0.0.tgz`, }, }), ), write( join(packageDir, "bunfig.toml"), ` [install] cache = false registry = "https://localhost:${server.port}/"`, ), write(join(packageDir, "cafile"), tls.cert), ]); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "--cafile", "cafile"], cwd: packageDir, stderr: "pipe", stdout: "pipe", env, }); const out = await stdout.text(); expect(out).toContain("+ no-deps@"); const err = await stderr.text(); expect(err).not.toContain("ConnectionClosed"); expect(err).not.toContain("error:"); expect(err).not.toContain("DEPTH_ZERO_SELF_SIGNED_CERT"); expect(await exited).toBe(0); }); test("valid --ca", async () => { using server = Bun.serve({ port: 0, fetch: mockRegistryFetch(), ...tls, }); await Promise.all([ write( packageJson, JSON.stringify({ name: "foo", version: "1.1.1", dependencies: { "no-deps": `https://localhost:${server.port}/no-deps-1.0.0.tgz`, }, }), ), write( join(packageDir, "bunfig.toml"), ` [install] cache = false registry = "https://localhost:${server.port}/"`, ), ]); // first without ca, should fail let { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stderr: "pipe", stdout: "pipe", env, }); let out = await stdout.text(); let err = stderrForInstall(await stderr.text()); expect(err).toContain("DEPTH_ZERO_SELF_SIGNED_CERT"); expect(await exited).toBe(1); // now with a valid ca ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "--ca", tls.cert], cwd: packageDir, stderr: "pipe", stdout: "pipe", env, })); out = await stdout.text(); expect(out).toContain("+ no-deps@"); err = await stderr.text(); expect(err).not.toContain("DEPTH_ZERO_SELF_SIGNED_CERT"); expect(err).not.toContain("error:"); expect(await exited).toBe(0); }); test(`non-existent --cafile`, async () => { await write(packageJson, JSON.stringify({ name: "foo", version: "1.0.0", "dependencies": { "no-deps": "1.1.1" } })); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "--cafile", "does-not-exist"], cwd: packageDir, stderr: "pipe", stdout: "pipe", env, }); const out = await stdout.text(); expect(out).not.toContain("no-deps"); const err = await stderr.text(); expect(err).toContain(`HTTPThread: could not find CA file: '${join(packageDir, "does-not-exist")}'`); expect(await exited).toBe(1); }); test("non-existent --cafile (absolute path)", async () => { await write(packageJson, JSON.stringify({ name: "foo", version: "1.0.0", "dependencies": { "no-deps": "1.1.1" } })); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "--cafile", "/does/not/exist"], cwd: packageDir, stderr: "pipe", stdout: "pipe", env, }); const out = await stdout.text(); expect(out).not.toContain("no-deps"); const err = await stderr.text(); expect(err).toContain(`HTTPThread: could not find CA file: '/does/not/exist'`); expect(await exited).toBe(1); }); test("cafile from bunfig does not exist", async () => { await Promise.all([ write( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "no-deps": "1.1.1", }, }), ), write( join(packageDir, "bunfig.toml"), ` [install] cache = false registry = "http://localhost:${port}/" cafile = "does-not-exist"`, ), ]); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stderr: "pipe", stdout: "pipe", env, }); const out = await stdout.text(); expect(out).not.toContain("no-deps"); const err = await stderr.text(); expect(err).toContain(`HTTPThread: could not find CA file: '${join(packageDir, "does-not-exist")}'`); expect(await exited).toBe(1); }); test("invalid cafile", async () => { await Promise.all([ write( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "no-deps": "1.1.1", }, }), ), write( join(packageDir, "invalid-cafile"), `-----BEGIN CERTIFICATE----- jlwkjekfjwlejlgldjfljlkwjef -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- ljelkjwelkgjw;lekj;lkejflkj -----END CERTIFICATE-----`, ), ]); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "--cafile", join(packageDir, "invalid-cafile")], cwd: packageDir, stderr: "pipe", stdout: "pipe", env, }); const out = await stdout.text(); expect(out).not.toContain("no-deps"); const err = await stderr.text(); expect(err).toContain(`HTTPThread: invalid CA file: '${join(packageDir, "invalid-cafile")}'`); expect(await exited).toBe(1); }); test("invalid --ca", async () => { await write( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "no-deps": "1.1.1", }, }), ); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "--ca", "not-valid"], cwd: packageDir, stderr: "pipe", stdout: "pipe", env, }); const out = await stdout.text(); expect(out).not.toContain("no-deps"); const err = await stderr.text(); expect(err).toContain("HTTPThread: the CA is invalid"); expect(await exited).toBe(1); }); }); describe("whoami", async () => { test("can get username", async () => { const bunfig = await registry.authBunfig("whoami"); await Promise.all([ write( packageJson, JSON.stringify({ name: "whoami-pkg", version: "1.1.1", }), ), write(join(packageDir, "bunfig.toml"), bunfig), ]); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "pm", "whoami"], cwd: packageDir, stdout: "pipe", stderr: "pipe", env, }); const out = await stdout.text(); expect(out).toBe("whoami\n"); const err = await stderr.text(); expect(err).not.toContain("error:"); expect(await exited).toBe(0); }); test("username from .npmrc", async () => { // It should report the username from npmrc, even without an account const bunfig = ` [install] cache = false registry = "http://localhost:${port}/"`; const npmrc = ` //localhost:${port}/:username=whoami-npmrc //localhost:${port}/:_password=123456 `; await Promise.all([ write(packageJson, JSON.stringify({ name: "whoami-pkg", version: "1.1.1" })), write(join(packageDir, "bunfig.toml"), bunfig), write(join(packageDir, ".npmrc"), npmrc), ]); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "pm", "whoami"], cwd: packageDir, stdout: "pipe", stderr: "pipe", env, }); const out = await stdout.text(); expect(out).toBe("whoami-npmrc\n"); const err = await stderr.text(); expect(err).not.toContain("error:"); expect(await exited).toBe(0); }); test("only .npmrc", async () => { const token = await generateRegistryUser("whoami-npmrc", "whoami-npmrc"); const npmrc = ` //localhost:${port}/:_authToken=${token} registry=http://localhost:${port}/`; await Promise.all([ write(packageJson, JSON.stringify({ name: "whoami-pkg", version: "1.1.1" })), write(join(packageDir, ".npmrc"), npmrc), ]); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "pm", "whoami"], cwd: packageDir, stdout: "pipe", stderr: "pipe", env, }); const out = await stdout.text(); expect(out).toBe("whoami-npmrc\n"); const err = await stderr.text(); expect(err).not.toContain("error:"); expect(await exited).toBe(0); }); test("two .npmrc", async () => { const token = await generateRegistryUser("whoami-two-npmrc", "whoami-two-npmrc"); const packageNpmrc = `registry=http://localhost:${port}/`; const homeNpmrc = `//localhost:${port}/:_authToken=${token}`; const homeDir = `${packageDir}/home_dir`; await Bun.$`mkdir -p ${homeDir}`; await Promise.all([ write(packageJson, JSON.stringify({ name: "whoami-pkg", version: "1.1.1" })), write(join(packageDir, ".npmrc"), packageNpmrc), write(join(homeDir, ".npmrc"), homeNpmrc), ]); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "pm", "whoami"], cwd: packageDir, stdout: "pipe", stderr: "pipe", env: { ...env, XDG_CONFIG_HOME: `${homeDir}`, }, }); const out = await stdout.text(); expect(out).toBe("whoami-two-npmrc\n"); const err = await stderr.text(); expect(err).not.toContain("error:"); expect(await exited).toBe(0); }); test("not logged in", async () => { await write(packageJson, JSON.stringify({ name: "whoami-pkg", version: "1.1.1" })); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "pm", "whoami"], cwd: packageDir, env, stdout: "pipe", stderr: "pipe", }); const out = await stdout.text(); expect(out).toBeEmpty(); const err = await stderr.text(); expect(err).toBe("error: missing authentication (run `bunx npm login`)\n"); expect(await exited).toBe(1); }); test("invalid token", async () => { // create the user and provide an invalid token const token = await generateRegistryUser("invalid-token", "invalid-token"); const bunfig = ` [install] cache = false registry = { url = "http://localhost:${port}/", token = "1234567" }`; await rm(join(packageDir, "bunfig.toml")); await Promise.all([ write(packageJson, JSON.stringify({ name: "whoami-pkg", version: "1.1.1" })), write(join(packageDir, "bunfig.toml"), bunfig), ]); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "pm", "whoami"], cwd: packageDir, env, stdout: "pipe", stderr: "pipe", }); const out = await stdout.text(); expect(out).toBeEmpty(); const err = await stderr.text(); expect(err).toBe(`error: failed to authenticate with registry 'http://localhost:${port}/'\n`); expect(await exited).toBe(1); }); }); describe("package.json indentation", async () => { test("works for root and workspace packages", async () => { await Promise.all([ // 5 space indentation write(packageJson, `\n{\n\n "name": "foo",\n"workspaces": ["packages/*"]\n}`), // 1 tab indentation write(join(packageDir, "packages", "bar", "package.json"), `\n{\n\n\t"name": "bar",\n}`), ]); let { exited } = spawn({ cmd: [bunExe(), "add", "no-deps"], cwd: packageDir, stdout: "ignore", stderr: "ignore", env, }); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); const rootPackageJson = await file(packageJson).text(); expect(rootPackageJson).toBe( `{\n "name": "foo",\n "workspaces": ["packages/*"],\n "dependencies": {\n "no-deps": "^2.0.0"\n }\n}`, ); // now add to workspace. it should keep tab indentation ({ exited } = spawn({ cmd: [bunExe(), "add", "no-deps"], cwd: join(packageDir, "packages", "bar"), stdout: "inherit", stderr: "inherit", env, })); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(packageJson).text()).toBe(rootPackageJson); const workspacePackageJson = await file(join(packageDir, "packages", "bar", "package.json")).text(); expect(workspacePackageJson).toBe(`{\n\t"name": "bar",\n\t"dependencies": {\n\t\t"no-deps": "^2.0.0"\n\t}\n}`); }); test("install maintains indentation", async () => { await write(packageJson, `{\n "dependencies": {}\n }\n`); let { exited } = spawn({ cmd: [bunExe(), "add", "no-deps"], cwd: packageDir, stdout: "ignore", stderr: "ignore", env, }); expect(await exited).toBe(0); expect(await file(packageJson).text()).toBe(`{\n "dependencies": {\n "no-deps": "^2.0.0"\n }\n}\n`); }); }); describe("text lockfile", () => { test("workspace sorting", async () => { await Promise.all([ write( join(packageDir, "package.json"), JSON.stringify({ name: "foo", workspaces: ["packages/*"], dependencies: { "no-deps": "1.0.0", }, }), ), write( join(packageDir, "packages", "b", "package.json"), JSON.stringify({ name: "b", dependencies: { "no-deps": "1.0.0", }, }), ), write( join(packageDir, "packages", "c", "package.json"), JSON.stringify({ name: "c", dependencies: { "no-deps": "1.0.0", }, }), ), ]); let { exited } = spawn({ cmd: [bunExe(), "install", "--save-text-lockfile"], cwd: packageDir, stdout: "ignore", stderr: "ignore", env, }); expect(await exited).toBe(0); expect( (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234"), ).toMatchSnapshot(); // now add workspace 'a' await write( join(packageDir, "packages", "a", "package.json"), JSON.stringify({ name: "a", dependencies: { "no-deps": "1.0.0", }, }), ); ({ exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "ignore", stderr: "ignore", env, })); expect(await exited).toBe(0); const lockfile = await Bun.file(join(packageDir, "bun.lock")).text(); expect(lockfile.replaceAll(/localhost:\d+/g, "localhost:1234")).toMatchSnapshot(); }); test("--frozen-lockfile", async () => { await Promise.all([ write( join(packageDir, "package.json"), JSON.stringify({ name: "foo", workspaces: ["packages/*"], dependencies: { "no-deps": "^1.0.0", "a-dep": "^1.0.2", }, }), ), write( join(packageDir, "packages", "pkg1", "package.json"), JSON.stringify({ name: "package1", dependencies: { "peer-deps-too": "1.0.0", }, }), ), ]); let { stderr, exited } = spawn({ cmd: [bunExe(), "install", "--save-text-lockfile"], cwd: packageDir, stdout: "ignore", stderr: "pipe", env, }); let err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); expect(await exited).toBe(0); const firstLockfile = await Bun.file(join(packageDir, "bun.lock")).text(); expect(firstLockfile.replace(/localhost:\d+/g, "localhost:1234")).toMatchSnapshot(); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); ({ stderr, exited } = spawn({ cmd: [bunExe(), "install", "--frozen-lockfile"], cwd: packageDir, stdout: "ignore", stderr: "pipe", env, })); err = await stderr.text(); expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("error:"); expect(await exited).toBe(0); expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ "a-dep", "no-deps", "package1", "peer-deps-too", ]); expect(await Bun.file(join(packageDir, "bun.lock")).text()).toBe(firstLockfile); }); for (const omit of ["dev", "peer", "optional"]) { test(`resolvable lockfile with ${omit} dependencies disabled`, async () => { await Promise.all([ write( join(packageDir, "package.json"), JSON.stringify({ name: "foo", peerDependencies: { "no-deps": "1.0.0" }, devDependencies: { "a-dep": "1.0.1" }, optionalDependencies: { "basic-1": "1.0.0" }, }), ), ]); let { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "--save-text-lockfile", `--omit=${omit}`], cwd: packageDir, stdout: "pipe", stderr: "pipe", env, }); let err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); expect(await exited).toBe(0); const depName = omit === "dev" ? "a-dep" : omit === "peer" ? "no-deps" : "basic-1"; expect(await exists(join(packageDir, "node_modules", depName))).toBeFalse(); const lockfile = (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll( /localhost:\d+/g, "localhost:1234", ); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stderr: "pipe", env, })); err = await stderr.text(); expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("error:"); expect(await exited).toBe(0); expect(await exists(join(packageDir, "node_modules", depName))).toBeTrue(); expect((await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234")).toBe( lockfile, ); }); } test("optionalPeers", async () => { await Promise.all([ write( packageJson, JSON.stringify({ name: "foo", workspaces: ["packages/*"], dependencies: { "a-dep": "1.0.1", }, }), ), write( join(packageDir, "packages", "pkg1", "package.json"), JSON.stringify({ name: "pkg1", peerDependencies: { "no-deps": "1.0.0", }, peerDependenciesMeta: { "no-deps": { optional: true, }, }, }), ), ]); let { exited } = spawn({ cmd: [bunExe(), "install", "--save-text-lockfile"], cwd: packageDir, stdout: "ignore", stderr: "ignore", env, }); expect(await exited).toBe(0); expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeFalse(); const firstLockfile = (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll( /localhost:\d+/g, "localhost:1234", ); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); // another install should recognize the peer dependency as `"optional": true` ({ exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "ignore", stderr: "ignore", env, })); expect(await exited).toBe(0); expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeFalse(); expect((await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234")).toBe( firstLockfile, ); }); }); test("--lockfile-only", async () => { await Promise.all([ write( packageJson, JSON.stringify({ name: "foo", workspaces: ["packages/*"], dependencies: { "no-deps": "^1.0.0", }, }), ), write( join(packageDir, "packages", "pkg1", "package.json"), JSON.stringify({ name: "package1", dependencies: { "two-range-deps": "1.0.0", }, }), ), ]); let { exited } = spawn({ cmd: [bunExe(), "install", "--save-text-lockfile", "--lockfile-only"], cwd: packageDir, stdout: "ignore", stderr: "ignore", env, }); expect(await exited).toBe(0); expect(await exists(join(packageDir, "node_modules"))).toBeFalse(); const firstLockfile = (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll( /localhost:\d+/g, "localhost:1234", ); // nothing changes with another --lockfile-only ({ exited } = spawn({ cmd: [bunExe(), "install", "--lockfile-only"], cwd: packageDir, stdout: "ignore", stderr: "ignore", env, })); expect(await exited).toBe(0); expect(await exists(join(packageDir, "node_modules"))).toBeFalse(); expect((await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234")).toBe( firstLockfile, ); // --silent works const { stdout, stderr, exited: exited2, } = spawn({ cmd: [bunExe(), "install", "--lockfile-only", "--silent"], cwd: packageDir, stdout: "pipe", stderr: "pipe", env, }); expect(await exited2).toBe(0); const out = await stdout.text(); const err = await stderr.text(); expect(out).toBe(""); expect(err).toBe(""); }); describe("bundledDependencies", () => { for (const textLockfile of [true, false]) { test(`(${textLockfile ? "bun.lock" : "bun.lockb"}) basic`, async () => { await Promise.all([ write( packageJson, JSON.stringify({ name: "bundled-basic", version: "1.1.1", dependencies: { "bundled-1": "1.0.0", }, }), ), ]); const cmd = textLockfile ? [bunExe(), "install", "--save-text-lockfile"] : [bunExe(), "install"]; let { exited } = spawn({ cmd, cwd: packageDir, stdout: "ignore", stderr: "ignore", env, }); expect(await exited).toBe(0); expect( await Promise.all([ exists(join(packageDir, "node_modules", "no-deps", "package.json")), exists(join(packageDir, "node_modules", "bundled-1", "node_modules", "no-deps", "package.json")), ]), ).toEqual([false, true]); ({ exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "ignore", stderr: "ignore", env, })); expect(await exited).toBe(0); expect( await Promise.all([ exists(join(packageDir, "node_modules", "no-deps", "package.json")), exists(join(packageDir, "node_modules", "bundled-1", "node_modules", "no-deps", "package.json")), ]), ).toEqual([false, true]); }); test(`(${textLockfile ? "bun.lock" : "bun.lockb"}) bundledDependencies === true`, async () => { await Promise.all([ write( packageJson, JSON.stringify({ name: "bundled-true", version: "1.1.1", dependencies: { "bundled-true": "1.0.0", }, }), ), ]); const cmd = textLockfile ? [bunExe(), "install", "--save-text-lockfile"] : [bunExe(), "install"]; let { exited } = spawn({ cmd, cwd: packageDir, stdout: "ignore", stderr: "ignore", env, }); expect(await exited).toBe(0); async function check() { return Promise.all([ exists(join(packageDir, "node_modules", "no-deps", "package.json")), exists(join(packageDir, "node_modules", "one-dep", "package.json")), exists(join(packageDir, "node_modules", "bundled-true", "node_modules", "no-deps", "package.json")), exists(join(packageDir, "node_modules", "bundled-true", "node_modules", "one-dep", "package.json")), exists( join( packageDir, "node_modules", "bundled-true", "node_modules", "one-dep", "node_modules", "no-deps", "package.json", ), ), ]); } expect(await check()).toEqual([false, false, true, true, true]); ({ exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "ignore", stderr: "ignore", env, })); expect(await exited).toBe(0); expect(await check()).toEqual([false, false, true, true, true]); }); test(`(${textLockfile ? "bun.lock" : "bun.lockb"}) transitive bundled dependency collision`, async () => { // Install a package with one bundled dependency and one regular dependency. // The bundled dependency has a transitive dependency of the same regular dependency, // but at a different version. Test that the regular dependency does not replace the // other version (both should exist). await Promise.all([ write( packageJson, JSON.stringify({ name: "bundled-collision", dependencies: { "bundled-transitive": "1.0.0", // prevent both transitive dependencies from hoisting "no-deps": "npm:a-dep@1.0.2", "one-dep": "npm:a-dep@1.0.3", }, }), ), ]); const cmd = textLockfile ? [bunExe(), "install", "--save-text-lockfile"] : [bunExe(), "install"]; let { exited } = spawn({ cmd, cwd: packageDir, stdout: "ignore", stderr: "ignore", env, }); expect(await exited).toBe(0); async function check() { expect( await Promise.all([ file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), file( join(packageDir, "node_modules", "bundled-transitive", "node_modules", "no-deps", "package.json"), ).json(), file( join( packageDir, "node_modules", "bundled-transitive", "node_modules", "one-dep", "node_modules", "no-deps", "package.json", ), ).json(), exists(join(packageDir, "node_modules", "bundled-transitive", "node_modules", "one-dep", "package.json")), ]), ).toEqual([ { name: "a-dep", version: "1.0.2" }, { name: "no-deps", version: "1.0.0" }, { name: "no-deps", version: "1.0.1" }, true, ]); } await check(); ({ exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "ignore", stderr: "ignore", env, })); expect(await exited).toBe(0); await check(); }); test(`(${textLockfile ? "bun.lock" : "bun.lockb"}) git dependencies`, async () => { await Promise.all([ write( packageJson, JSON.stringify({ name: "bundled-git", dependencies: { // bundledDependencies: ["zod"], "install-test1": "dylan-conway/bundled-install-test#7824752", // bundledDependencies: true, "install-test2": "git+ssh://git@github.com/dylan-conway/bundled-install-test#1301309", }, }), ), write( join(packageDir, "bunfig.toml"), ` [install] cache = "${join(packageDir, ".bun-cache")}" `, ), ]); const cmd = textLockfile ? [bunExe(), "install", "--save-text-lockfile"] : [bunExe(), "install"]; let { exited } = spawn({ cmd, cwd: packageDir, stdout: "ignore", stderr: "ignore", env, }); expect(await exited).toBe(0); async function check() { expect( await Promise.all([ exists(join(packageDir, "node_modules", "zod", "package.json")), exists(join(packageDir, "node_modules", "install-test1", "node_modules", "zod", "package.json")), exists(join(packageDir, "node_modules", "install-test2", "node_modules", "zod", "package.json")), ]), ).toEqual([false, true, true]); } await check(); ({ exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "ignore", stderr: "ignore", env, })); expect(await exited).toBe(0); await check(); }); test(`(${textLockfile ? "bun.lock" : "bun.lockb"}) workspace dependencies bundle correctly`, async () => { await Promise.all([ write( packageJson, JSON.stringify({ name: "bundled-workspace", workspaces: ["packages/*"], }), ), write( join(packageDir, "packages", "pkg-one-one-one", "package.json"), JSON.stringify({ name: "pkg-one-one-one", dependencies: { "no-deps": "1.0.0", "bundled-1": "1.0.0", }, bundledDependencies: ["no-deps"], }), ), ]); const cmd = textLockfile ? [bunExe(), "install", "--save-text-lockfile"] : [bunExe(), "install"]; let { exited } = spawn({ cmd, cwd: packageDir, stdout: "ignore", stderr: "ignore", env, }); expect(await exited).toBe(0); async function check() { expect( await Promise.all([ exists(join(packageDir, "node_modules", "no-deps", "package.json")), exists(join(packageDir, "packages", "pkg-one-one-one", "node_modules", "no-deps", "package.json")), exists(join(packageDir, "node_modules", "bundled-1", "node_modules", "no-deps", "package.json")), ]), ).toEqual([true, false, true]); } await check(); ({ exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "ignore", stderr: "ignore", env, })); expect(await exited).toBe(0); await check(); }); } }); describe("optionalDependencies", () => { for (const optional of [true, false]) { test(`exit code is ${optional ? 0 : 1} when ${optional ? "optional" : ""} dependency tarball is missing`, async () => { await write( packageJson, JSON.stringify({ name: "foo", [optional ? "optionalDependencies" : "dependencies"]: { "missing-tarball": "1.0.0", "uses-what-bin": "1.0.0", }, "trustedDependencies": ["uses-what-bin"], }), ); const { exited, err } = await runBunInstall(env, packageDir, { [optional ? "allowWarnings" : "allowErrors"]: true, expectedExitCode: optional ? 0 : 1, savesLockfile: false, }); expect(err).toContain( `${optional ? "warn" : "error"}: GET http://localhost:${port}/missing-tarball/-/missing-tarball-1.0.0.tgz - `, ); expect(await exited).toBe(optional ? 0 : 1); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "uses-what-bin", "what-bin"]); expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); }); } for (const rootOptional of [true, false]) { test(`exit code is 0 when ${rootOptional ? "root" : ""} optional dependency does not exist in registry`, async () => { await write( packageJson, JSON.stringify({ name: "foo", [rootOptional ? "optionalDependencies" : "dependencies"]: { [rootOptional ? "this-package-does-not-exist-in-the-registry" : "has-missing-optional-dep"]: "||", }, }), ); const { err } = await runBunInstall(env, packageDir, { allowWarnings: true, savesLockfile: !rootOptional, }); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(err).toMatch(`warn: GET http://localhost:${port}/this-package-does-not-exist-in-the-registry - 404`); }); } test("should not install optional deps if false in bunfig", async () => { await writeFile( join(packageDir, "bunfig.toml"), ` [install] cache = "${join(packageDir, ".bun-cache")}" optional = false registry = "http://localhost:${port}/" `, ); await writeFile( packageJson, JSON.stringify( { name: "publish-pkg-deps", version: "1.1.1", dependencies: { "no-deps": "1.0.0", }, optionalDependencies: { "basic-1": "1.0.0", }, }, null, 2, ), ); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "--linker=hoisted"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); const err = await stderr.text(); const out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", expect.stringContaining("+ no-deps@1.0.0"), "", "1 package installed", ]); expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["no-deps"]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test("lifecycle scripts failures from transitive dependencies are ignored", async () => { // Dependency with a transitive optional dependency that fails during its preinstall script. await write( packageJson, JSON.stringify({ name: "foo", version: "2.2.2", dependencies: { "optional-lifecycle-fail": "1.1.1", }, trustedDependencies: ["lifecycle-fail"], }), ); const { err, exited } = await runBunInstall(env, packageDir); expect(err).not.toContain("error:"); expect(err).not.toContain("warn:"); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect( await Promise.all([ exists(join(packageDir, "node_modules", "optional-lifecycle-fail", "package.json")), exists(join(packageDir, "node_modules", "lifecycle-fail", "package.json")), ]), ).toEqual([true, false]); }); }); test("it should ignore peerDependencies within workspaces", async () => { await Promise.all([ write( packageJson, JSON.stringify({ name: "foo", workspaces: ["packages/baz"], peerDependencies: { "no-deps": ">=1.0.0", }, }), ), write( join(packageDir, "packages", "baz", "package.json"), JSON.stringify({ name: "Baz", peerDependencies: { "a-dep": ">=1.0.1", }, }), ), write(join(packageDir, ".npmrc"), `omit=peer`), ]); const { exited } = spawn({ cmd: [bunExe(), "install", "--save-text-lockfile"], cwd: packageDir, env, }); expect(await exited).toBe(0); expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["Baz"]); expect( (await file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234"), ).toMatchSnapshot(); // installing with them enabled works await rm(join(packageDir, ".npmrc")); await runBunInstall(env, packageDir, { savesLockfile: false }); expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["Baz", "a-dep", "no-deps"]); }); test("disabled dev/peer/optional dependencies are still included in the lockfile", async () => { await Promise.all([ write( packageJson, JSON.stringify({ devDependencies: { "no-deps": "1.0.0", }, peerDependencies: { "a-dep": "1.0.1", }, optionalDependencies: { "basic-1": "1.0.0", }, }), ), ]); await runBunInstall; }); test("tarball override does not crash", async () => { await write( packageJson, JSON.stringify({ name: "foo", dependencies: { "two-range-deps": "||", }, overrides: { "no-deps": `http://localhost:${port}/no-deps/-/no-deps-2.0.0.tgz`, }, }), ); await runBunInstall(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ name: "no-deps", version: "2.0.0", }); }); describe.each(["--production", "without --production"])("%s", flag => { const prod = flag === "--production"; const order = ["devDependencies", "dependencies"]; // const stdio = process.versions.bun.includes("debug") ? "inherit" : "ignore"; const stdio = "ignore"; if (prod) { test("modifying package.json with --production should not save lockfile", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "bin-change-dir": "1.0.0", }, devDependencies: { "bin-change-dir": "1.0.1", "basic-1": "1.0.0", }, }), ); var { exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: stdio, stdin: stdio, stderr: stdio, env, }); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); const initialHash = Bun.hash(await file(join(packageDir, "bun.lockb")).arrayBuffer()); expect(await file(join(packageDir, "node_modules", "bin-change-dir", "package.json")).json()).toMatchObject({ name: "bin-change-dir", version: "1.0.1", }); var { exited } = spawn({ cmd: [bunExe(), "install", "--production"], cwd: packageDir, stdout: stdio, stdin: stdio, stderr: stdio, env, }); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(join(packageDir, "node_modules", "bin-change-dir", "package.json")).json()).toMatchObject({ name: "bin-change-dir", version: "1.0.0", }); var { exited } = spawn({ cmd: [bunExe(), "install", "--production", "bin-change-dir@1.0.1"], cwd: packageDir, stdout: stdio, stdin: stdio, stderr: stdio, env, }); expect(await exited).toBe(1); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); // We should not have saved bun.lockb expect(Bun.hash(await file(join(packageDir, "bun.lockb")).arrayBuffer())).toBe(initialHash); // We should not have installed bin-change-dir@1.0.1 expect(await file(join(packageDir, "node_modules", "bin-change-dir", "package.json")).json()).toMatchObject({ name: "bin-change-dir", version: "1.0.0", }); // This is a no-op. It should work. var { exited } = spawn({ cmd: [bunExe(), "install", "--production", "bin-change-dir@1.0.0"], cwd: packageDir, stdout: stdio, stdin: stdio, stderr: stdio, env, }); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); // We should not have saved bun.lockb expect(Bun.hash(await file(join(packageDir, "bun.lockb")).arrayBuffer())).toBe(initialHash); // We should have installed bin-change-dir@1.0.0 expect(await file(join(packageDir, "node_modules", "bin-change-dir", "package.json")).json()).toMatchObject({ name: "bin-change-dir", version: "1.0.0", }); }); } test(`should prefer ${order[+prod % 2]} over ${order[1 - (+prod % 2)]}`, async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "bin-change-dir": "1.0.0", }, devDependencies: { "bin-change-dir": "1.0.1", "basic-1": "1.0.0", }, }), ); let initialLockfileHash; async function saveWithoutProd() { var hash; // First install without --production // so that the lockfile is up to date var { exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: stdio, stdin: stdio, stderr: stdio, env, }); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); await Promise.all([ (async () => expect(await file(join(packageDir, "node_modules", "bin-change-dir", "package.json")).json()).toMatchObject({ name: "bin-change-dir", version: "1.0.1", }))(), (async () => expect(await file(join(packageDir, "node_modules", "basic-1", "package.json")).json()).toMatchObject({ name: "basic-1", version: "1.0.0", }))().then( async () => await rm(join(packageDir, "node_modules", "basic-1"), { recursive: true, force: true }), ), (async () => (hash = Bun.hash(await file(join(packageDir, "bun.lockb")).arrayBuffer())))(), ]); return hash; } if (prod) { initialLockfileHash = await saveWithoutProd(); } var { exited } = spawn({ cmd: [bunExe(), "install", prod ? "--production" : ""].filter(Boolean), cwd: packageDir, stdout: stdio, stdin: stdio, stderr: stdio, env, }); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(join(packageDir, "node_modules", "bin-change-dir", "package.json")).json()).toMatchObject({ name: "bin-change-dir", version: prod ? "1.0.0" : "1.0.1", }); if (!prod) { expect(await file(join(packageDir, "node_modules", "basic-1", "package.json")).json()).toMatchObject({ name: "basic-1", version: "1.0.0", }); } else { // it should not install devDependencies expect(await exists(join(packageDir, "node_modules", "basic-1"))).toBeFalse(); // it should not mutate the lockfile when there were no changes to begin with. const newHash = Bun.hash(await file(join(packageDir, "bun.lockb")).arrayBuffer()); expect(newHash).toBe(initialLockfileHash!); } if (prod) { // lets now try to install again without --production const newHash = await saveWithoutProd(); expect(newHash).toBe(initialLockfileHash); } }); }); test("hardlinks on windows dont fail with long paths", async () => { await mkdir(join(packageDir, "a-package")); await writeFile( join(packageDir, "a-package", "package.json"), JSON.stringify({ name: "a-package", version: "1.0.0", }), ); await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.2.3", dependencies: { // 255 characters "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": "file:./a-package", }, }), ); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stderr: "pipe", env, }); const err = await stderr.text(); const out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@a-package", "", "1 package installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test("basic 1", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "basic-1": "1.0.0", }, }), ); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ basic-1@1.0.0", "", "1 package installed", ]); expect(await file(join(packageDir, "node_modules", "basic-1", "package.json")).json()).toEqual({ name: "basic-1", version: "1.0.0", } as any); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); err = await stderr.text(); out = await stdout.text(); expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ basic-1@1.0.0", "", "1 package installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test("manifest cache will invalidate when registry changes", async () => { const cacheDir = join(packageDir, ".bun-cache"); await Promise.all([ write( join(packageDir, "bunfig.toml"), ` [install] cache = "${cacheDir}" registry = "http://localhost:${port}" saveTextLockfile = false `, ), write( packageJson, JSON.stringify({ name: "foo", dependencies: { // is-number exists in our custom registry and in npm. Switching the registry should invalidate // the manifest cache, the package could be a completely different package. "is-number": "2.0.0", }, }), ), ]); // first install this package from registry await runBunInstall(env, packageDir); const lockfile = await parseLockfile(packageDir); for (const pkg of Object.values(lockfile.packages) as any) { if (pkg.tag === "npm") { expect(pkg.resolution.resolved).toContain(`http://localhost:${port}`); } } // now use default registry await Promise.all([ rm(join(packageDir, "node_modules"), { force: true, recursive: true }), rm(join(packageDir, "bun.lockb"), { force: true }), write( join(packageDir, "bunfig.toml"), ` [install] cache = "${cacheDir}" `, ), ]); await runBunInstall(env, packageDir); const npmLockfile = await parseLockfile(packageDir); for (const pkg of Object.values(npmLockfile.packages) as any) { if (pkg.tag === "npm") { expect(pkg.resolution.resolved).not.toContain(`http://localhost:${port}`); } } }); test("dependency from root satisfies range from dependency", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "one-range-dep": "1.0.0", "no-deps": "1.0.0", }, }), ); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", expect.stringContaining("+ no-deps@1.0.0"), "+ one-range-dep@1.0.0", "", "2 packages installed", ]); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ name: "no-deps", version: "1.0.0", } as any); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); err = await stderr.text(); out = await stdout.text(); expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", expect.stringContaining("+ no-deps@1.0.0"), "+ one-range-dep@1.0.0", "", "2 packages installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test("duplicate names and versions in a manifest do not install incorrect packages", async () => { /** * `duplicate-name-and-version` has two versions: * 1.0.1: * dependencies: { * "no-deps": "a-dep" * } * 1.0.2: * dependencies: { * "a-dep": "1.0.1" * } * Note: version for `no-deps` is the same as second dependency name. * * When this manifest is parsed, the strings for dependency names and versions are stored * with different lists offset length pairs, but we were deduping with the same map. Since * the version of the first dependency is the same as the name as the second, it would try to * dedupe them, and doing so would give the wrong name for the deduped dependency. * (`a-dep@1.0.1` would become `no-deps@1.0.1`) */ await write( packageJson, JSON.stringify({ name: "foo", dependencies: { "duplicate-name-and-version": "1.0.2", }, }), ); await runBunInstall(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); const lockfile = parseLockfile(packageDir); expect(lockfile).toMatchNodeModulesAt(packageDir); const results = await Promise.all([ file(join(packageDir, "node_modules", "duplicate-name-and-version", "package.json")).json(), file(join(packageDir, "node_modules", "a-dep", "package.json")).json(), exists(join(packageDir, "node_modules", "no-deps")), ]); expect(results).toMatchObject([ { name: "duplicate-name-and-version", version: "1.0.2" }, { name: "a-dep", version: "1.0.1" }, false, ]); }); describe("peerDependency index out of bounds", async () => { // Test for "index of out bounds" errors with peer dependencies when adding/removing a package // // Repro: // - Install `1-peer-dep-a`. It depends on peer dep `no-deps@1.0.0`. // - Replace `1-peer-dep-a` with `1-peer-dep-b` (identical other than name), delete manifest cache and // node_modules, then reinstall. // - `no-deps` will enqueue a dependency id that goes out of bounds const dependencies = ["1-peer-dep-a", "1-peer-dep-b", "2-peer-deps-c"]; for (const firstDep of dependencies) { for (const secondDep of dependencies) { if (firstDep === secondDep) continue; test(`replacing ${firstDep} with ${secondDep}`, async () => { await write( packageJson, JSON.stringify({ name: "foo", dependencies: { [firstDep]: "1.0.0", }, }), ); await runBunInstall(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); const lockfile = parseLockfile(packageDir); expect(lockfile).toMatchNodeModulesAt(packageDir); const results = await Promise.all([ file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), file(join(packageDir, "node_modules", firstDep, "package.json")).json(), exists(join(packageDir, "node_modules", firstDep, "node_modules", "no-deps")), ]); expect(results).toMatchObject([ { name: "no-deps", version: "1.0.0" }, { name: firstDep, version: "1.0.0" }, false, ]); await Promise.all([ rm(join(packageDir, "node_modules"), { recursive: true, force: true }), rm(join(packageDir, ".bun-cache"), { recursive: true, force: true }), write( packageJson, JSON.stringify({ name: "foo", dependencies: { [secondDep]: "1.0.0", }, }), ), ]); await runBunInstall(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); const newLockfile = parseLockfile(packageDir); expect(newLockfile).toMatchNodeModulesAt(packageDir); const newResults = await Promise.all([ file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), file(join(packageDir, "node_modules", secondDep, "package.json")).json(), exists(join(packageDir, "node_modules", secondDep, "node_modules", "no-deps")), ]); expect(newResults).toMatchObject([ { name: "no-deps", version: "1.0.0" }, { name: secondDep, version: "1.0.0" }, false, ]); }); } } // Install 2 dependencies, one is a normal dependency, the other is a dependency with a optional // peer dependency on the first dependency. Delete node_modules and cache, then update the dependency // with the optional peer to a new version. Doing this will cause the peer dependency to get enqueued // internally, testing for index out of bounds. It's also important cache is deleted to ensure a tarball // task is created for it. test("optional", async () => { await write( packageJson, JSON.stringify({ name: "foo", dependencies: { "optional-peer-deps": "1.0.0", "no-deps": "1.0.0", }, }), ); await runBunInstall(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); // update version and delete node_modules and cache await Promise.all([ write( packageJson, JSON.stringify({ name: "foo", dependencies: { "optional-peer-deps": "1.0.1", "no-deps": "1.0.0", }, }), ), rm(join(packageDir, "node_modules"), { recursive: true, force: true }), rm(join(packageDir, ".bun-cache"), { recursive: true, force: true }), ]); // this install would trigger the index out of bounds error await runBunInstall(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); const lockfile = parseLockfile(packageDir); expect(lockfile).toMatchNodeModulesAt(packageDir); }); }); test("peerDependency in child npm dependency should not maintain old version when package is upgraded", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "peer-deps-fixed": "1.0.0", "no-deps": "1.0.0", }, }), ); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", expect.stringContaining("+ no-deps@1.0.0"), "+ peer-deps-fixed@1.0.0", "", "2 packages installed", ]); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ name: "no-deps", version: "1.0.0", } as any); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "peer-deps-fixed": "1.0.0", "no-deps": "1.0.1", // upgrade the package }, }), ); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); err = await stderr.text(); out = await stdout.text(); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ name: "no-deps", version: "1.0.1", } as any); expect(await exists(join(packageDir, "node_modules", "peer-deps-fixed", "node_modules"))).toBeFalse(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", expect.stringContaining("+ no-deps@1.0.1"), "", "1 package installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test("package added after install", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "one-range-dep": "1.0.0", }, }), ); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ one-range-dep@1.0.0", "", "2 packages installed", ]); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ name: "no-deps", version: "1.1.0", } as any); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); // add `no-deps` to root package.json with a smaller but still compatible // version for `one-range-dep`. await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "one-range-dep": "1.0.0", "no-deps": "1.0.0", }, }), ); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); err = await stderr.text(); out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", expect.stringContaining("+ no-deps@1.0.0"), "", "2 packages installed", ]); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ name: "no-deps", version: "1.0.0", } as any); expect( await file(join(packageDir, "node_modules", "one-range-dep", "node_modules", "no-deps", "package.json")).json(), ).toEqual({ name: "no-deps", version: "1.1.0", } as any); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); err = await stderr.text(); out = await stdout.text(); expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", expect.stringContaining("+ no-deps@1.0.0"), "+ one-range-dep@1.0.0", "", "3 packages installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test("--production excludes devDependencies in workspaces", async () => { await Promise.all([ write( packageJson, JSON.stringify({ name: "foo", workspaces: ["packages/*"], dependencies: { "no-deps": "1.0.0", }, devDependencies: { "a1": "npm:no-deps@1.0.0", }, }), ), write( join(packageDir, "packages", "pkg1", "package.json"), JSON.stringify({ name: "pkg1", dependencies: { "a-dep": "1.0.2", }, devDependencies: { "a2": "npm:a-dep@1.0.2", }, }), ), write( join(packageDir, "packages", "pkg2", "package.json"), JSON.stringify({ name: "pkg2", devDependencies: { "a3": "npm:a-dep@1.0.3", "a4": "npm:a-dep@1.0.4", "a5": "npm:a-dep@1.0.5", }, }), ), ]); // without lockfile const expectedResults = [ ["a-dep", "no-deps", "pkg1", "pkg2"], { name: "no-deps", version: "1.0.0" }, { name: "a-dep", version: "1.0.2" }, ]; let { out } = await runBunInstall(env, packageDir, { production: true }); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", expect.stringContaining("+ no-deps@1.0.0"), "", "4 packages installed", ]); let results = await Promise.all([ readdirSorted(join(packageDir, "node_modules")), file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), file(join(packageDir, "node_modules", "a-dep", "package.json")).json(), ]); expect(results).toMatchObject(expectedResults); // create non-production lockfile, then install with --production await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); ({ out } = await runBunInstall(env, packageDir)); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ a1@1.0.0", expect.stringContaining("+ no-deps@1.0.0"), "", "7 packages installed", ]); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); ({ out } = await runBunInstall(env, packageDir, { production: true })); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", expect.stringContaining("+ no-deps@1.0.0"), "", "4 packages installed", ]); results = await Promise.all([ readdirSorted(join(packageDir, "node_modules")), file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), file(join(packageDir, "node_modules", "a-dep", "package.json")).json(), ]); expect(results).toMatchObject(expectedResults); }); test("--production without a lockfile will install and not save lockfile", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.2.3", dependencies: { "no-deps": "1.0.0", }, }), ); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "--production"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); const out = await stdout.text(); const err = await stderr.text(); expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", expect.stringContaining("+ no-deps@1.0.0"), "", "1 package installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await exists(join(packageDir, "node_modules", "no-deps", "index.js"))).toBeTrue(); }); describe("binaries", () => { for (const global of [false, true]) { describe(`existing destinations${global ? " (global)" : ""}`, () => { test("existing non-symlink", async () => { await Promise.all([ write( packageJson, JSON.stringify({ name: "foo", dependencies: { "what-bin": "1.0.0", }, }), ), write(join(packageDir, "node_modules", ".bin", "what-bin"), "hi"), ]); await runBunInstall(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(join(packageDir, "node_modules", ".bin", "what-bin")).toBeValidBin( join("..", "what-bin", "what-bin.js"), ); }); }); } test("it should correctly link binaries after deleting node_modules", async () => { const json: any = { name: "foo", version: "1.0.0", dependencies: { "what-bin": "1.0.0", "uses-what-bin": "1.5.0", }, }; await writeFile(packageJson, JSON.stringify(json)); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", expect.stringContaining("+ uses-what-bin@1.5.0"), expect.stringContaining("+ what-bin@1.0.0"), "", "3 packages installed", "", "Blocked 1 postinstall. Run `bun pm untrusted` for details.", "", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); err = await stderr.text(); out = await stdout.text(); expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", expect.stringContaining("+ uses-what-bin@1.5.0"), expect.stringContaining("+ what-bin@1.0.0"), "", "3 packages installed", "", "Blocked 1 postinstall. Run `bun pm untrusted` for details.", "", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test("will link binaries for packages installed multiple times", async () => { await Promise.all([ write( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "uses-what-bin": "1.5.0", }, workspaces: ["packages/*"], trustedDependencies: ["uses-what-bin"], }), ), write( join(packageDir, "packages", "pkg1", "package.json"), JSON.stringify({ name: "pkg1", dependencies: { "uses-what-bin": "1.0.0", }, }), ), write( join(packageDir, "packages", "pkg2", "package.json"), JSON.stringify({ name: "pkg2", dependencies: { "uses-what-bin": "1.0.0", }, }), ), ]); // Root dependends on `uses-what-bin@1.5.0` and both packages depend on `uses-what-bin@1.0.0`. // This test makes sure the binaries used by `pkg1` and `pkg2` are the correct version (`1.0.0`) // instead of using the root version (`1.5.0`). await runBunInstall(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); const results = await Promise.all([ file(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt")).text(), file(join(packageDir, "packages", "pkg1", "node_modules", "uses-what-bin", "what-bin.txt")).text(), file(join(packageDir, "packages", "pkg2", "node_modules", "uses-what-bin", "what-bin.txt")).text(), ]); expect(results).toEqual(["what-bin@1.5.0", "what-bin@1.0.0", "what-bin@1.0.0"]); }); test("it should re-symlink binaries that become invalid when updating package versions", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "bin-change-dir": "1.0.0", }, scripts: { postinstall: "bin-change-dir", }, }), ); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", expect.stringContaining("+ bin-change-dir@1.0.0"), "", "1 package installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(join(packageDir, "bin-1.0.0.txt")).text()).toEqual("success!"); expect(await exists(join(packageDir, "bin-1.0.1.txt"))).toBeFalse(); await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "bin-change-dir": "1.0.1", }, scripts: { postinstall: "bin-change-dir", }, }), ); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); err = await stderr.text(); out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", expect.stringContaining("+ bin-change-dir@1.0.1"), "", "1 package installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(join(packageDir, "bin-1.0.0.txt")).text()).toEqual("success!"); expect(await file(join(packageDir, "bin-1.0.1.txt")).text()).toEqual("success!"); }); test("will only link global binaries for requested packages", async () => { await Promise.all([ write( join(packageDir, "bunfig.toml"), ` [install] cache = false registry = "http://localhost:${port}/" globalBinDir = "${join(packageDir, "global-bin-dir").replace(/\\/g, "\\\\")}" `, ), , ]); let { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "i", "--linker=hoisted", "-g", `--config=${join(packageDir, "bunfig.toml")}`, "uses-what-bin"], cwd: packageDir, stdout: "pipe", stderr: "pipe", env: { ...env, BUN_INSTALL: join(packageDir, "global-install-dir") }, }); let err = await stderr.text(); expect(err).not.toContain("error:"); let out = await stdout.text(); expect(out).toContain("uses-what-bin@1.5.0"); expect(await exited).toBe(0); expect(await exists(join(packageDir, "global-bin-dir", "what-bin"))).toBeFalse(); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "i", "--linker=hoisted", "-g", `--config=${join(packageDir, "bunfig.toml")}`, "what-bin"], cwd: packageDir, stdout: "pipe", stderr: "pipe", env: { ...env, BUN_INSTALL: join(packageDir, "global-install-dir") }, })); err = await stderr.text(); expect(err).not.toContain("error:"); out = await stdout.text(); expect(out).toContain("what-bin@1.5.0"); expect(await exited).toBe(0); // now `what-bin` should be installed in the global bin directory if (isWindows) { expect( await Promise.all([ exists(join(packageDir, "global-bin-dir", "what-bin.exe")), exists(join(packageDir, "global-bin-dir", "what-bin.bunx")), ]), ).toEqual([true, true]); } else { expect(await exists(join(packageDir, "global-bin-dir", "what-bin"))).toBeTrue(); } }); for (const global of [false, true]) { test(`bin types${global ? " (global)" : ""}`, async () => { if (global) { await write( join(packageDir, "bunfig.toml"), ` [install] cache = false registry = "http://localhost:${port}/" globalBinDir = "${join(packageDir, "global-bin-dir").replace(/\\/g, "\\\\")}" `, ); } else { await write( packageJson, JSON.stringify({ name: "foo", }), ); } const args = [ bunExe(), "install", "--linker=hoisted", ...(global ? ["-g"] : []), ...(global ? [`--config=${join(packageDir, "bunfig.toml")}`] : []), "dep-with-file-bin", "dep-with-single-entry-map-bin", "dep-with-directory-bins", "dep-with-map-bins", ]; const { stdout, stderr, exited } = spawn({ cmd: args, cwd: packageDir, stdout: "pipe", stderr: "pipe", env: global ? { ...env, BUN_INSTALL: join(packageDir, "global-install-dir") } : env, }); const err = await stderr.text(); expect(err).not.toContain("error:"); const out = await stdout.text(); expect(await exited).toBe(0); await runBin("dep-with-file-bin", "file-bin\n", global); await runBin("single-entry-map-bin", "single-entry-map-bin\n", global); await runBin("directory-bin-1", "directory-bin-1\n", global); await runBin("directory-bin-2", "directory-bin-2\n", global); await runBin("map-bin-1", "map-bin-1\n", global); await runBin("map-bin-2", "map-bin-2\n", global); }); } test("each type of binary serializes correctly to text lockfile", async () => { await Promise.all([ write( packageJson, JSON.stringify({ name: "foo", version: "1.1.1", dependencies: { "file-bin": "./file-bin", "named-file-bin": "./named-file-bin", "dir-bin": "./dir-bin", "map-bin": "./map-bin", }, }), ), write( join(packageDir, "file-bin", "package.json"), JSON.stringify({ name: "file-bin", version: "1.1.1", bin: "./file-bin.js", }), ), write(join(packageDir, "file-bin", "file-bin.js"), `#!/usr/bin/env node\nconsole.log("file-bin")`), write( join(packageDir, "named-file-bin", "package.json"), JSON.stringify({ name: "named-file-bin", version: "1.1.1", bin: { "named-file-bin": "./named-file-bin.js" }, }), ), write( join(packageDir, "named-file-bin", "named-file-bin.js"), `#!/usr/bin/env node\nconsole.log("named-file-bin")`, ), write( join(packageDir, "dir-bin", "package.json"), JSON.stringify({ name: "dir-bin", version: "1.1.1", directories: { bin: "./bins", }, }), ), write(join(packageDir, "dir-bin", "bins", "dir-bin-1.js"), `#!/usr/bin/env node\nconsole.log("dir-bin-1")`), write( join(packageDir, "map-bin", "package.json"), JSON.stringify({ name: "map-bin", version: "1.1.1", bin: { "map-bin-1": "./map-bin-1.js", "map-bin-2": "./map-bin-2.js", }, }), ), write(join(packageDir, "map-bin", "map-bin-1.js"), `#!/usr/bin/env node\nconsole.log("map-bin-1")`), write(join(packageDir, "map-bin", "map-bin-2.js"), `#!/usr/bin/env node\nconsole.log("map-bin-2")`), ]); let { stderr, exited } = spawn({ cmd: [bunExe(), "install", "--save-text-lockfile"], cwd: packageDir, stdout: "ignore", stderr: "pipe", env, }); let err = await stderr.text(); expect(err).not.toContain("error:"); expect(await exited).toBe(0); const firstLockfile = (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll( /localhost:\d+/g, "localhost:1234", ); expect(join(packageDir, "node_modules", ".bin", "file-bin")).toBeValidBin(join("..", "file-bin", "file-bin.js")); expect(join(packageDir, "node_modules", ".bin", "named-file-bin")).toBeValidBin( join("..", "named-file-bin", "named-file-bin.js"), ); expect(join(packageDir, "node_modules", ".bin", "dir-bin-1.js")).toBeValidBin( join("..", "dir-bin", "bins", "dir-bin-1.js"), ); expect(join(packageDir, "node_modules", ".bin", "map-bin-1")).toBeValidBin(join("..", "map-bin", "map-bin-1.js")); expect(join(packageDir, "node_modules", ".bin", "map-bin-2")).toBeValidBin(join("..", "map-bin", "map-bin-2.js")); await rm(join(packageDir, "node_modules", ".bin"), { recursive: true, force: true }); // now install from the lockfile, only linking bins ({ stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "ignore", stderr: "pipe", env, })); err = await stderr.text(); expect(err).not.toContain("error:"); expect(err).not.toContain("Saved lockfile"); expect(await exited).toBe(0); expect(firstLockfile).toBe( (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234"), ); expect(firstLockfile).toMatchSnapshot(); expect(join(packageDir, "node_modules", ".bin", "file-bin")).toBeValidBin(join("..", "file-bin", "file-bin.js")); expect(join(packageDir, "node_modules", ".bin", "named-file-bin")).toBeValidBin( join("..", "named-file-bin", "named-file-bin.js"), ); expect(join(packageDir, "node_modules", ".bin", "dir-bin-1.js")).toBeValidBin( join("..", "dir-bin", "bins", "dir-bin-1.js"), ); expect(join(packageDir, "node_modules", ".bin", "map-bin-1")).toBeValidBin(join("..", "map-bin", "map-bin-1.js")); expect(join(packageDir, "node_modules", ".bin", "map-bin-2")).toBeValidBin(join("..", "map-bin", "map-bin-2.js")); }); test.todo("text lockfile updates with new bin entry for folder dependencies", async () => { await Promise.all([ write( packageJson, JSON.stringify({ name: "foo", dependencies: { "change-bin": "./change-bin", }, }), ), write( join(packageDir, "change-bin", "package.json"), JSON.stringify({ name: "change-bin", version: "1.0.0", bin: { "change-bin-1": "./change-bin-1.js", }, }), ), write(join(packageDir, "change-bin", "change-bin-1.js"), `#!/usr/bin/env node\nconsole.log("change-bin-1")`), ]); let { stderr, exited } = spawn({ cmd: [bunExe(), "install", "--save-text-lockfile"], cwd: packageDir, stdout: "ignore", stderr: "pipe", env, }); let err = await stderr.text(); expect(err).not.toContain("error:"); expect(err).toContain("Saved lockfile"); expect(await exited).toBe(0); const firstLockfile = (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll( /localhost:\d+/g, "localhost:1234", ); expect(join(packageDir, "node_modules", ".bin", "change-bin-1")).toBeValidBin( join("..", "change-bin", "change-bin-1.js"), ); await Promise.all([ write( join(packageDir, "change-bin", "package.json"), JSON.stringify({ name: "change-bin", version: "1.0.0", bin: { "change-bin-1": "./change-bin-1.js", "change-bin-2": "./change-bin-2.js", }, }), ), write(join(packageDir, "change-bin", "change-bin-2.js"), `#!/usr/bin/env node\nconsole.log("change-bin-2")`), ]); ({ stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "ignore", stderr: "pipe", env, })); err = await stderr.text(); expect(err).not.toContain("error:"); // it should save expect(err).toContain("Saved lockfile"); expect(await exited).toBe(0); const secondLockfile = (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll( /localhost:\d+/g, "localhost:1234", ); expect(firstLockfile).not.toBe(secondLockfile); expect(secondLockfile).toMatchSnapshot(); expect(join(packageDir, "node_modules", ".bin", "change-bin-1")).toBeValidBin( join("..", "change-bin", "change-bin-1.js"), ); expect(join(packageDir, "node_modules", ".bin", "change-bin-2")).toBeValidBin( join("..", "change-bin", "change-bin-2.js"), ); }); test("root resolution bins", async () => { // As of writing this test, the only way to get a root resolution // is to migrate a package-lock.json with a root resolution. For now, // we'll just mock the bun.lock. await Promise.all([ write( join(packageDir, "package.json"), JSON.stringify({ name: "fooooo", version: "2.2.2", dependencies: { "fooooo": ".", "no-deps": "1.0.0", }, bin: "fooooo.js", }), ), write(join(packageDir, "fooooo.js"), `#!/usr/bin/env node\nconsole.log("fooooo")`), write( join(packageDir, "bun.lock"), JSON.stringify({ "lockfileVersion": 0, "workspaces": { "": { "name": "fooooo", "dependencies": { "fooooo": ".", // out of date, no no-deps }, }, }, "packages": { "fooooo": ["fooooo@root:", { bin: "fooooo.js" }], }, }), ), ]); let { stderr, stdout, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stderr: "pipe", env, }); let err = await stderr.text(); expect(err).not.toContain("error:"); expect(err).toContain("Saved lockfile"); let out = await stdout.text(); expect(out).toContain("no-deps@1.0.0"); expect(await exited).toBe(0); const firstLockfile = (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll( /localhost:\d+/g, "localhost:1234", ); expect(join(packageDir, "node_modules", ".bin", "fooooo")).toBeValidBin(join("..", "fooooo", "fooooo.js")); await rm(join(packageDir, "node_modules", ".bin"), { recursive: true, force: true }); ({ stderr, stdout, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stderr: "pipe", env, })); err = await stderr.text(); expect(err).not.toContain("error:"); expect(err).not.toContain("Saved lockfile"); out = await stdout.text(); expect(out).not.toContain("no-deps@1.0.0"); expect(await exited).toBe(0); expect(firstLockfile).toBe( (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234"), ); expect(firstLockfile).toMatchSnapshot(); expect(join(packageDir, "node_modules", ".bin", "fooooo")).toBeValidBin(join("..", "fooooo", "fooooo.js")); }); async function runBin(binName: string, expected: string, global: boolean) { const args = global ? [`./global-bin-dir/${binName}`] : [bunExe(), binName]; const result = Bun.spawn({ cmd: [...args, "--linker=hoisted"], stdout: "pipe", stderr: "pipe", cwd: packageDir, env, }); const out = await result.stdout.text(); expect(out).toEqual(expected); const err = await result.stderr.text(); expect(err).toBeEmpty(); expect(await result.exited).toBe(0); } test("it will skip (without errors) if a folder from `directories.bin` does not exist", async () => { await Promise.all([ write( packageJson, JSON.stringify({ name: "foo", dependencies: { "missing-directory-bin": "file:missing-directory-bin-1.1.1.tgz", }, }), ), cp(join(import.meta.dir, "missing-directory-bin-1.1.1.tgz"), join(packageDir, "missing-directory-bin-1.1.1.tgz")), ]); const { stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stderr: "pipe", env, }); const err = await stderr.text(); expect(err).not.toContain("error:"); expect(await exited).toBe(0); }); }); test("--config cli flag works", async () => { await Promise.all([ write( join(packageDir, "package.json"), JSON.stringify({ name: "foo", dependencies: { "no-deps": "1.0.0", }, devDependencies: { "a-dep": "1.0.1", }, }), ), write( join(packageDir, "bunfig2.toml"), ` [install] cache = "${join(packageDir, ".bun-cache")}" registry = "http://localhost:${port}/" dev = false `, ), ]); // should install dev dependencies let { exited } = spawn({ cmd: [bunExe(), "i"], cwd: packageDir, stdout: "pipe", stderr: "pipe", env, }); expect(await exited).toBe(0); expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toEqual({ name: "a-dep", version: "1.0.1", }); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); // should not install dev dependencies ({ exited } = spawn({ cmd: [bunExe(), "i", "--config=bunfig2.toml"], cwd: packageDir, stdout: "pipe", stderr: "pipe", env, })); expect(await exited).toBe(0); expect(await exists(join(packageDir, "node_modules", "a-dep"))).toBeFalse(); }); test("it should invalid cached package if package.json is missing", async () => { await Promise.all([ write( packageJson, JSON.stringify({ name: "foo", dependencies: { "no-deps": "2.0.0", }, }), ), ]); let { out } = await runBunInstall(env, packageDir); expect(out).toContain("+ no-deps@2.0.0"); // node_modules and cache should be populated expect( await Promise.all([ readdirSorted(join(packageDir, "node_modules", "no-deps")), readdirSorted(join(packageDir, ".bun-cache", "no-deps@2.0.0@@localhost@@@1")), ]), ).toEqual([ ["index.js", "package.json"], ["index.js", "package.json"], ]); // with node_modules package.json deleted, the package should be reinstalled await rm(join(packageDir, "node_modules", "no-deps", "package.json")); ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); expect(out).toContain("+ no-deps@2.0.0"); // another install is a no-op ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); expect(out).not.toContain("+ no-deps@2.0.0"); // with cache package.json deleted, install is a no-op and cache is untouched await rm(join(packageDir, ".bun-cache", "no-deps@2.0.0@@localhost@@@1", "package.json")); ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); expect(out).not.toContain("+ no-deps@2.0.0"); expect( await Promise.all([ readdirSorted(join(packageDir, "node_modules", "no-deps")), readdirSorted(join(packageDir, ".bun-cache", "no-deps@2.0.0@@localhost@@@1")), ]), ).toEqual([["index.js", "package.json"], ["index.js"]]); // now with node_modules package.json deleted, the package AND the cache should // be repopulated await rm(join(packageDir, "node_modules", "no-deps", "package.json")); ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); expect(out).toContain("+ no-deps@2.0.0"); expect( await Promise.all([ readdirSorted(join(packageDir, "node_modules", "no-deps")), readdirSorted(join(packageDir, ".bun-cache", "no-deps@2.0.0@@localhost@@@1")), ]), ).toEqual([ ["index.js", "package.json"], ["index.js", "package.json"], ]); }); test("it should install with missing bun.lockb, node_modules, and/or cache", async () => { // first clean install await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "what-bin": "1.0.0", "uses-what-bin": "1.5.0", "optional-native": "1.0.0", "peer-deps-too": "1.0.0", "two-range-deps": "1.0.0", "one-fixed-dep": "2.0.0", "no-deps-bins": "2.0.0", "left-pad": "1.0.0", "native": "1.0.0", "dep-loop-entry": "1.0.0", "dep-with-tags": "3.0.0", "dev-deps": "1.0.0", }, }), ); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ dep-loop-entry@1.0.0", expect.stringContaining("+ dep-with-tags@3.0.0"), "+ dev-deps@1.0.0", "+ left-pad@1.0.0", "+ native@1.0.0", "+ no-deps-bins@2.0.0", expect.stringContaining("+ one-fixed-dep@2.0.0"), "+ optional-native@1.0.0", "+ peer-deps-too@1.0.0", "+ two-range-deps@1.0.0", expect.stringContaining("+ uses-what-bin@1.5.0"), expect.stringContaining("+ what-bin@1.0.0"), "", "19 packages installed", "", "Blocked 1 postinstall. Run `bun pm untrusted` for details.", "", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); let lockfile = parseLockfile(packageDir); expect(lockfile).toMatchNodeModulesAt(packageDir); // delete node_modules await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); [err, out] = await Promise.all([stderr.text(), stdout.text()]); expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ dep-loop-entry@1.0.0", expect.stringContaining("+ dep-with-tags@3.0.0"), "+ dev-deps@1.0.0", "+ left-pad@1.0.0", "+ native@1.0.0", "+ no-deps-bins@2.0.0", "+ one-fixed-dep@2.0.0", "+ optional-native@1.0.0", "+ peer-deps-too@1.0.0", "+ two-range-deps@1.0.0", expect.stringContaining("+ uses-what-bin@1.5.0"), expect.stringContaining("+ what-bin@1.0.0"), "", "19 packages installed", "", "Blocked 1 postinstall. Run `bun pm untrusted` for details.", "", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); lockfile = parseLockfile(packageDir); expect(lockfile).toMatchNodeModulesAt(packageDir); for (var i = 0; i < 100; i++) { // Situation: // // Root package has a dependency on one-fixed-dep, peer-deps-too and two-range-deps. // Each of these dependencies depends on no-deps. // // - one-fixed-dep: no-deps@2.0.0 // - two-range-deps: no-deps@^1.0.0 (will choose 1.1.0) // - peer-deps-too: peer no-deps@* // // We want peer-deps-too to choose the version of no-deps from one-fixed-dep because // it's the highest version. It should hoist to the root. // delete bun.lockb await rm(join(packageDir, "bun.lockb"), { recursive: true, force: true }); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); [err, out] = await Promise.all([stderr.text(), stdout.text()]); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", expect.stringContaining("Checked 19 installs across 23 packages (no changes)"), ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); } // delete cache await rm(join(packageDir, ".bun-cache"), { recursive: true, force: true }); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); [err, out] = await Promise.all([stderr.text(), stdout.text()]); expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", expect.stringContaining("Checked 19 installs across 23 packages (no changes)"), ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); // delete bun.lockb and cache await rm(join(packageDir, "bun.lockb"), { recursive: true, force: true }); await rm(join(packageDir, ".bun-cache"), { recursive: true, force: true }); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); [err, out] = await Promise.all([stderr.text(), stdout.text()]); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", expect.stringContaining("Checked 19 installs across 23 packages (no changes)"), ]); }); describe("hoisting", async () => { var tests: any = [ { situation: "1.0.0 - 1.0.10 is in order", dependencies: { "uses-a-dep-1": "1.0.0", "uses-a-dep-2": "1.0.0", "uses-a-dep-3": "1.0.0", "uses-a-dep-4": "1.0.0", "uses-a-dep-5": "1.0.0", "uses-a-dep-6": "1.0.0", "uses-a-dep-7": "1.0.0", "uses-a-dep-8": "1.0.0", "uses-a-dep-9": "1.0.0", "uses-a-dep-10": "1.0.0", }, expected: "1.0.1", }, { situation: "1.0.1 in the middle", dependencies: { "uses-a-dep-2": "1.0.0", "uses-a-dep-3": "1.0.0", "uses-a-dep-4": "1.0.0", "uses-a-dep-5": "1.0.0", "uses-a-dep-6": "1.0.0", "uses-a-dep-7": "1.0.0", "uses-a-dep-1": "1.0.0", "uses-a-dep-8": "1.0.0", "uses-a-dep-9": "1.0.0", "uses-a-dep-10": "1.0.0", }, expected: "1.0.1", }, { situation: "1.0.1 is missing", dependencies: { "uses-a-dep-2": "1.0.0", "uses-a-dep-3": "1.0.0", "uses-a-dep-4": "1.0.0", "uses-a-dep-5": "1.0.0", "uses-a-dep-6": "1.0.0", "uses-a-dep-7": "1.0.0", "uses-a-dep-8": "1.0.0", "uses-a-dep-9": "1.0.0", "uses-a-dep-10": "1.0.0", }, expected: "1.0.10", }, { situation: "1.0.10 and 1.0.1 are missing", dependencies: { "uses-a-dep-2": "1.0.0", "uses-a-dep-3": "1.0.0", "uses-a-dep-4": "1.0.0", "uses-a-dep-5": "1.0.0", "uses-a-dep-6": "1.0.0", "uses-a-dep-7": "1.0.0", "uses-a-dep-8": "1.0.0", "uses-a-dep-9": "1.0.0", }, expected: "1.0.2", }, { situation: "1.0.10 is missing and 1.0.1 is last", dependencies: { "uses-a-dep-2": "1.0.0", "uses-a-dep-3": "1.0.0", "uses-a-dep-4": "1.0.0", "uses-a-dep-5": "1.0.0", "uses-a-dep-6": "1.0.0", "uses-a-dep-7": "1.0.0", "uses-a-dep-8": "1.0.0", "uses-a-dep-9": "1.0.0", "uses-a-dep-1": "1.0.0", }, expected: "1.0.1", }, ]; for (const { dependencies, expected, situation } of tests) { test(`it should hoist ${expected} when ${situation}`, async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", dependencies, }), ); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); for (const dep of Object.keys(dependencies)) { expect(out).toContain(`+ ${dep}@${dependencies[dep]}`); } expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).text()).toContain(expected); await rm(join(packageDir, "bun.lockb")); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); err = await stderr.text(); out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out).not.toContain("package installed"); expect(out).toContain(`Checked ${Object.keys(dependencies).length * 2} installs across`); expect(await exited).toBe(0); }); } describe("peers", async () => { var peerTests: any = [ { situation: "peer 1.0.2", dependencies: { "uses-a-dep-1": "1.0.0", "uses-a-dep-2": "1.0.0", "uses-a-dep-3": "1.0.0", "uses-a-dep-4": "1.0.0", "uses-a-dep-5": "1.0.0", "uses-a-dep-6": "1.0.0", "uses-a-dep-7": "1.0.0", "uses-a-dep-8": "1.0.0", "uses-a-dep-9": "1.0.0", "uses-a-dep-10": "1.0.0", "peer-a-dep-1-0-2": "1.0.0", }, expected: "1.0.2", }, { situation: "peer >= 1.0.2", dependencies: { "uses-a-dep-1": "1.0.0", "uses-a-dep-2": "1.0.0", "uses-a-dep-3": "1.0.0", "uses-a-dep-4": "1.0.0", "uses-a-dep-5": "1.0.0", "uses-a-dep-6": "1.0.0", "uses-a-dep-7": "1.0.0", "uses-a-dep-8": "1.0.0", "uses-a-dep-9": "1.0.0", "uses-a-dep-10": "1.0.0", "peer-a-dep-gte-1-0-2": "1.0.0", }, expected: "1.0.10", }, { situation: "peer ^1.0.2", dependencies: { "uses-a-dep-1": "1.0.0", "uses-a-dep-2": "1.0.0", "uses-a-dep-3": "1.0.0", "uses-a-dep-4": "1.0.0", "uses-a-dep-5": "1.0.0", "uses-a-dep-6": "1.0.0", "uses-a-dep-7": "1.0.0", "uses-a-dep-8": "1.0.0", "uses-a-dep-9": "1.0.0", "uses-a-dep-10": "1.0.0", "peer-a-dep-caret-1-0-2": "1.0.0", }, expected: "1.0.10", }, { situation: "peer ~1.0.2", dependencies: { "uses-a-dep-1": "1.0.0", "uses-a-dep-2": "1.0.0", "uses-a-dep-3": "1.0.0", "uses-a-dep-4": "1.0.0", "uses-a-dep-5": "1.0.0", "uses-a-dep-6": "1.0.0", "uses-a-dep-7": "1.0.0", "uses-a-dep-8": "1.0.0", "uses-a-dep-9": "1.0.0", "uses-a-dep-10": "1.0.0", "peer-a-dep-tilde-1-0-2": "1.0.0", }, expected: "1.0.10", }, { situation: "peer *", dependencies: { "uses-a-dep-1": "1.0.0", "uses-a-dep-2": "1.0.0", "uses-a-dep-3": "1.0.0", "uses-a-dep-4": "1.0.0", "uses-a-dep-5": "1.0.0", "uses-a-dep-6": "1.0.0", "uses-a-dep-7": "1.0.0", "uses-a-dep-8": "1.0.0", "uses-a-dep-9": "1.0.0", "uses-a-dep-10": "1.0.0", "peer-a-dep-star": "1.0.0", }, expected: "1.0.1", }, { situation: "peer * and peer 1.0.2", dependencies: { "uses-a-dep-1": "1.0.0", "uses-a-dep-2": "1.0.0", "uses-a-dep-3": "1.0.0", "uses-a-dep-4": "1.0.0", "uses-a-dep-5": "1.0.0", "uses-a-dep-6": "1.0.0", "uses-a-dep-7": "1.0.0", "uses-a-dep-8": "1.0.0", "uses-a-dep-9": "1.0.0", "uses-a-dep-10": "1.0.0", "peer-a-dep-1-0-2": "1.0.0", "peer-a-dep-star": "1.0.0", }, expected: "1.0.2", }, ]; for (const { dependencies, expected, situation } of peerTests) { test.todoIf(isFlaky && isMacOS && situation === "peer ^1.0.2")( `it should hoist ${expected} when ${situation}`, async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", dependencies, }), ); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); for (const dep of Object.keys(dependencies)) { expect(out).toContain(`+ ${dep}@${dependencies[dep]}`); } expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).text()).toContain(expected); await rm(join(packageDir, "bun.lockb")); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); err = await stderr.text(); out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); if (out.includes("installed")) { console.log("stdout:", out); } expect(out).not.toContain("package installed"); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).text()).toContain(expected); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); err = await stderr.text(); out = await stdout.text(); expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out).not.toContain("package installed"); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).text()).toContain(expected); }, ); } }); test("hoisting/using incorrect peer dep after install", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", dependencies: { "peer-deps-fixed": "1.0.0", "no-deps": "1.0.0", }, }), ); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(err).not.toContain("incorrect peer dependency"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", expect.stringContaining("+ no-deps@1.0.0"), "+ peer-deps-fixed@1.0.0", "", "2 packages installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ name: "no-deps", version: "1.0.0", } as any); expect(await file(join(packageDir, "node_modules", "peer-deps-fixed", "package.json")).json()).toEqual({ name: "peer-deps-fixed", version: "1.0.0", peerDependencies: { "no-deps": "^1.0.0", }, } as any); expect(await exists(join(packageDir, "node_modules", "peer-deps-fixed", "node_modules"))).toBeFalse(); await writeFile( packageJson, JSON.stringify({ name: "foo", dependencies: { "peer-deps-fixed": "1.0.0", "no-deps": "2.0.0", }, }), ); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); err = await stderr.text(); out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ no-deps@2.0.0", "", "1 package installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ name: "no-deps", version: "2.0.0", } as any); expect(await file(join(packageDir, "node_modules", "peer-deps-fixed", "package.json")).json()).toEqual({ name: "peer-deps-fixed", version: "1.0.0", peerDependencies: { "no-deps": "^1.0.0", }, } as any); expect(await exists(join(packageDir, "node_modules", "peer-deps-fixed", "node_modules"))).toBeFalse(); }); test("root workspace (other than root) dependency will not hoist incorrect peer", async () => { await Promise.all([ write( packageJson, JSON.stringify({ name: "foo", workspaces: ["bar"], }), ), write( join(packageDir, "bar", "package.json"), JSON.stringify({ name: "bar", dependencies: { "peer-deps-fixed": "1.0.0", "no-deps": "1.0.0", }, }), ), ]); let { exited, stdout } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stderr: "ignore", stdout: "pipe", env, }); let 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); // now run the install again but from the workspace and with `no-deps@2.0.0` await write( join(packageDir, "bar", "package.json"), JSON.stringify({ name: "bar", dependencies: { "peer-deps-fixed": "1.0.0", "no-deps": "2.0.0", }, }), ); ({ exited, stdout } = spawn({ cmd: [bunExe(), "install"], cwd: join(packageDir, "bar"), stderr: "ignore", stdout: "pipe", env, })); out = await stdout.text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ no-deps@2.0.0", "", "2 packages installed", ]); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ version: "2.0.0", }); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test("hoisting/using incorrect peer dep on initial install", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", dependencies: { "peer-deps-fixed": "1.0.0", "no-deps": "2.0.0", }, }), ); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(err).toContain("incorrect peer dependency"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ no-deps@2.0.0", "+ peer-deps-fixed@1.0.0", "", "2 packages installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ name: "no-deps", version: "2.0.0", } as any); expect(await file(join(packageDir, "node_modules", "peer-deps-fixed", "package.json")).json()).toEqual({ name: "peer-deps-fixed", version: "1.0.0", peerDependencies: { "no-deps": "^1.0.0", }, } as any); expect(await exists(join(packageDir, "node_modules", "peer-deps-fixed", "node_modules"))).toBeFalse(); await writeFile( packageJson, JSON.stringify({ name: "foo", dependencies: { "peer-deps-fixed": "1.0.0", "no-deps": "1.0.0", }, }), ); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); err = await stderr.text(); out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", expect.stringContaining("+ no-deps@1.0.0"), "", "1 package installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ name: "no-deps", version: "1.0.0", } as any); expect(await file(join(packageDir, "node_modules", "peer-deps-fixed", "package.json")).json()).toEqual({ name: "peer-deps-fixed", version: "1.0.0", peerDependencies: { "no-deps": "^1.0.0", }, } as any); expect(await exists(join(packageDir, "node_modules", "peer-deps-fixed", "node_modules"))).toBeFalse(); }); describe("devDependencies", () => { test("from normal dependency", async () => { // Root package should choose no-deps@1.0.1. // // `normal-dep-and-dev-dep` should install `no-deps@1.0.0` and `normal-dep@1.0.1`. // It should not hoist (skip) `no-deps` for `normal-dep-and-dev-dep`. await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "no-deps": "1.0.0", "normal-dep-and-dev-dep": "1.0.2", }, devDependencies: { "no-deps": "1.0.1", }, }), ); const { stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "ignore", stderr: "pipe", stdin: "ignore", env, }); const err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ name: "no-deps", version: "1.0.1", }); expect( await file( join(packageDir, "node_modules", "normal-dep-and-dev-dep", "node_modules", "no-deps", "package.json"), ).json(), ).toEqual({ name: "no-deps", version: "1.0.0", }); }); test("from workspace", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", workspaces: ["packages/*"], dependencies: { "no-deps": "1.0.0", }, devDependencies: { "no-deps": "1.0.1", }, }), ); await mkdir(join(packageDir, "packages", "moo"), { recursive: true }); await writeFile( join(packageDir, "packages", "moo", "package.json"), JSON.stringify({ name: "moo", version: "1.2.3", dependencies: { "no-deps": "2.0.0", "normal-dep-and-dev-dep": "1.0.0", }, devDependencies: { "no-deps": "1.1.0", }, }), ); const { stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stderr: "pipe", stdout: "ignore", stdin: "ignore", env, }); const err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ name: "no-deps", version: "1.0.1", }); expect( await file(join(packageDir, "node_modules", "moo", "node_modules", "no-deps", "package.json")).json(), ).toEqual({ name: "no-deps", version: "1.1.0", }); }); test("from linked package", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "no-deps": "1.1.0", "folder-dep": "file:./folder-dep", }, devDependencies: { "no-deps": "2.0.0", }, }), ); await mkdir(join(packageDir, "folder-dep")); await writeFile( join(packageDir, "folder-dep", "package.json"), JSON.stringify({ name: "folder-dep", version: "1.2.3", dependencies: { "no-deps": "1.0.0", "normal-dep-and-dev-dep": "1.0.1", }, devDependencies: { "no-deps": "1.0.1", }, }), ); const { stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stderr: "pipe", stdout: "ignore", stdin: "ignore", env, }); const err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ name: "no-deps", version: "2.0.0", }); expect( await file( join(packageDir, "node_modules", "normal-dep-and-dev-dep", "node_modules", "no-deps", "package.json"), ).json(), ).toEqual({ "name": "no-deps", "version": "1.1.0", }); expect( await file(join(packageDir, "node_modules", "folder-dep", "node_modules", "no-deps", "package.json")).json(), ).toEqual({ name: "no-deps", version: "1.0.1", }); }); test("dependency with normal dependency same as root", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "no-deps": "1.0.0", "one-dep": "1.0.0", }, devDependencies: { "no-deps": "2.0.0", }, }), ); const { stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stderr: "pipe", stdout: "ignore", stdin: "ignore", env, }); const err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ name: "no-deps", version: "2.0.0", }); expect( await file(join(packageDir, "node_modules", "one-dep", "node_modules", "no-deps", "package.json")).json(), ).toEqual({ name: "no-deps", version: "1.0.1", }); }); }); test.todoIf(isFlaky && isWindows)("text lockfile is hoisted", async () => { // Each dependency depends on 'hoist-lockfile-shared'. // 1 - "*" // 2 - "^1.0.1" // 3 - ">=1.0.1" await write( packageJson, JSON.stringify({ name: "foo", dependencies: { "hoist-lockfile-1": "1.0.0", "hoist-lockfile-2": "1.0.0", "hoist-lockfile-3": "1.0.0", }, }), ); let { exited, stderr } = spawn({ cmd: [bunExe(), "install", "--save-text-lockfile"], cwd: packageDir, stderr: "pipe", stdout: "ignore", env, }); let err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); expect(await exited).toBe(0); const lockfile = (await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll( /localhost:\d+/g, "localhost:1234", ); expect(lockfile).toMatchSnapshot(); // second install should not save the lockfile // with a different set of resolutions ({ exited, stderr } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stderr: "pipe", stdout: "ignore", env, })); err = await stderr.text(); expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("error:"); expect(await exited).toBe(0); expect((await Bun.file(join(packageDir, "bun.lock")).text()).replaceAll(/localhost:\d+/g, "localhost:1234")).toBe( lockfile, ); }); }); describe("transitive file dependencies", () => { async function checkHoistedFiles() { const aliasedFileDepFilesPackageJson = join( packageDir, "node_modules", "aliased-file-dep", "node_modules", "files", "the-files", "package.json", ); const results = await Promise.all([ ( await lstat(join(packageDir, "node_modules", "file-dep", "node_modules", "files", "package.json")) ).isSymbolicLink(), readdirSorted(join(packageDir, "node_modules", "missing-file-dep", "node_modules")), exists(join(packageDir, "node_modules", "aliased-file-dep", "package.json")), isWindows ? file(await readlink(aliasedFileDepFilesPackageJson)).json() : file(aliasedFileDepFilesPackageJson).json(), ( await lstat( join(packageDir, "node_modules", "@scoped", "file-dep", "node_modules", "@scoped", "files", "package.json"), ) ).isSymbolicLink(), ( await lstat( join( packageDir, "node_modules", "@another-scope", "file-dep", "node_modules", "@scoped", "files", "package.json", ), ) ).isSymbolicLink(), ( await lstat(join(packageDir, "node_modules", "self-file-dep", "node_modules", "self-file-dep", "package.json")) ).isSymbolicLink(), ]); expect(results).toEqual([ true, [], true, { "name": "files", "version": "1.1.1", "dependencies": { "no-deps": "2.0.0", }, }, true, true, true, ]); } async function checkUnhoistedFiles() { const results = await Promise.all([ file(join(packageDir, "node_modules", "dep-file-dep", "package.json")).json(), file(join(packageDir, "node_modules", "file-dep", "package.json")).json(), file(join(packageDir, "node_modules", "missing-file-dep", "package.json")).json(), file(join(packageDir, "node_modules", "aliased-file-dep", "package.json")).json(), file(join(packageDir, "node_modules", "@scoped", "file-dep", "package.json")).json(), file(join(packageDir, "node_modules", "@another-scope", "file-dep", "package.json")).json(), file(join(packageDir, "node_modules", "self-file-dep", "package.json")).json(), ( await lstat(join(packageDir, "pkg1", "node_modules", "file-dep", "node_modules", "files", "package.json")) ).isSymbolicLink(), readdirSorted(join(packageDir, "pkg1", "node_modules", "missing-file-dep", "node_modules")), // [] exists(join(packageDir, "pkg1", "node_modules", "aliased-file-dep")), // false ( await lstat( join( packageDir, "pkg1", "node_modules", "@scoped", "file-dep", "node_modules", "@scoped", "files", "package.json", ), ) ).isSymbolicLink(), ( await lstat( join( packageDir, "pkg1", "node_modules", "@another-scope", "file-dep", "node_modules", "@scoped", "files", "package.json", ), ) ).isSymbolicLink(), ( await lstat( join(packageDir, "pkg1", "node_modules", "self-file-dep", "node_modules", "self-file-dep", "package.json"), ) ).isSymbolicLink(), readdirSorted(join(packageDir, "pkg1", "node_modules")), ]); const expected = [ ...(Array(7).fill({ name: "a-dep", version: "1.0.1" }) as any), true, [] as string[], false, true, true, true, ["@another-scope", "@scoped", "dep-file-dep", "file-dep", "missing-file-dep", "self-file-dep"], ]; // @ts-ignore expect(results).toEqual(expected); } test("from hoisted workspace dependencies", async () => { await Promise.all([ write( packageJson, JSON.stringify({ name: "foo", workspaces: ["pkg1"], }), ), write( join(packageDir, "pkg1", "package.json"), JSON.stringify({ name: "pkg1", dependencies: { // hoisted "dep-file-dep": "1.0.0", // root "file-dep": "1.0.0", // dangling symlink "missing-file-dep": "1.0.0", // aliased. has `"file-dep": "file:."` "aliased-file-dep": "npm:file-dep@1.0.1", // scoped "@scoped/file-dep": "1.0.0", // scoped with different names "@another-scope/file-dep": "1.0.0", // file dependency on itself "self-file-dep": "1.0.0", }, }), ), ]); var { out } = await runBunInstall(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "14 packages installed", ]); await checkHoistedFiles(); expect(await exists(join(packageDir, "pkg1", "node_modules"))).toBeFalse(); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); // reinstall ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "14 packages installed", ]); await checkHoistedFiles(); ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "1 package installed", ]); await checkHoistedFiles(); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); await rm(join(packageDir, "bun.lockb"), { force: true }); // install from workspace ({ out } = await runBunInstall(env, join(packageDir, "pkg1"))); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ @another-scope/file-dep@1.0.0", "+ @scoped/file-dep@1.0.0", "+ aliased-file-dep@1.0.1", "+ dep-file-dep@1.0.0", expect.stringContaining("+ file-dep@1.0.0"), "+ missing-file-dep@1.0.0", "+ self-file-dep@1.0.0", "", "14 packages installed", ]); await checkHoistedFiles(); expect(await exists(join(packageDir, "pkg1", "node_modules"))).toBeFalse(); ({ out } = await runBunInstall(env, join(packageDir, "pkg1"), { savesLockfile: false })); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "1 package installed", ]); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); ({ out } = await runBunInstall(env, join(packageDir, "pkg1"), { savesLockfile: false })); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ @another-scope/file-dep@1.0.0", "+ @scoped/file-dep@1.0.0", "+ aliased-file-dep@1.0.1", "+ dep-file-dep@1.0.0", expect.stringContaining("+ file-dep@1.0.0"), "+ missing-file-dep@1.0.0", "+ self-file-dep@1.0.0", "", "14 packages installed", ]); }); test("from non-hoisted workspace dependencies", async () => { await Promise.all([ write( packageJson, JSON.stringify({ name: "foo", workspaces: ["pkg1"], // these dependencies exist to make the workspace // dependencies non-hoisted dependencies: { "dep-file-dep": "npm:a-dep@1.0.1", "file-dep": "npm:a-dep@1.0.1", "missing-file-dep": "npm:a-dep@1.0.1", "aliased-file-dep": "npm:a-dep@1.0.1", "@scoped/file-dep": "npm:a-dep@1.0.1", "@another-scope/file-dep": "npm:a-dep@1.0.1", "self-file-dep": "npm:a-dep@1.0.1", }, }), ), write( join(packageDir, "pkg1", "package.json"), JSON.stringify({ name: "pkg1", dependencies: { // hoisted "dep-file-dep": "1.0.0", // root "file-dep": "1.0.0", // dangling symlink "missing-file-dep": "1.0.0", // aliased. has `"file-dep": "file:."` "aliased-file-dep": "npm:file-dep@1.0.1", // scoped "@scoped/file-dep": "1.0.0", // scoped with different names "@another-scope/file-dep": "1.0.0", // file dependency on itself "self-file-dep": "1.0.0", }, }), ), ]); var { out } = await runBunInstall(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ @another-scope/file-dep@1.0.1", "+ @scoped/file-dep@1.0.1", "+ aliased-file-dep@1.0.1", "+ dep-file-dep@1.0.1", expect.stringContaining("+ file-dep@1.0.1"), "+ missing-file-dep@1.0.1", "+ self-file-dep@1.0.1", "", "13 packages installed", ]); await checkUnhoistedFiles(); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); await rm(join(packageDir, "pkg1", "node_modules"), { recursive: true, force: true }); // reinstall ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ @another-scope/file-dep@1.0.1", "+ @scoped/file-dep@1.0.1", "+ aliased-file-dep@1.0.1", "+ dep-file-dep@1.0.1", expect.stringContaining("+ file-dep@1.0.1"), "+ missing-file-dep@1.0.1", "+ self-file-dep@1.0.1", "", "13 packages installed", ]); await checkUnhoistedFiles(); ({ out } = await runBunInstall(env, packageDir, { savesLockfile: false })); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "1 package installed", ]); await checkUnhoistedFiles(); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); await rm(join(packageDir, "pkg1", "node_modules"), { recursive: true, force: true }); await rm(join(packageDir, "bun.lockb"), { force: true }); // install from workspace ({ out } = await runBunInstall(env, join(packageDir, "pkg1"))); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ @another-scope/file-dep@1.0.0", "+ @scoped/file-dep@1.0.0", "+ aliased-file-dep@1.0.1", "+ dep-file-dep@1.0.0", expect.stringContaining("+ file-dep@1.0.0"), "+ missing-file-dep@1.0.0", "+ self-file-dep@1.0.0", "", "13 packages installed", ]); await checkUnhoistedFiles(); ({ out } = await runBunInstall(env, join(packageDir, "pkg1"), { savesLockfile: false })); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "1 package installed", ]); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); await rm(join(packageDir, "pkg1", "node_modules"), { recursive: true, force: true }); ({ out } = await runBunInstall(env, join(packageDir, "pkg1"), { savesLockfile: false })); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ @another-scope/file-dep@1.0.0", "+ @scoped/file-dep@1.0.0", "+ aliased-file-dep@1.0.1", "+ dep-file-dep@1.0.0", expect.stringContaining("+ file-dep@1.0.0"), "+ missing-file-dep@1.0.0", "+ self-file-dep@1.0.0", "", "13 packages installed", ]); }); test("from root dependencies", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { // hoisted "dep-file-dep": "1.0.0", // root "file-dep": "1.0.0", // dangling symlink "missing-file-dep": "1.0.0", // aliased. has `"file-dep": "file:."` "aliased-file-dep": "npm:file-dep@1.0.1", // scoped "@scoped/file-dep": "1.0.0", // scoped with different names "@another-scope/file-dep": "1.0.0", // file dependency on itself "self-file-dep": "1.0.0", }, }), ); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ @another-scope/file-dep@1.0.0", "+ @scoped/file-dep@1.0.0", "+ aliased-file-dep@1.0.1", "+ dep-file-dep@1.0.0", expect.stringContaining("+ file-dep@1.0.0"), "+ missing-file-dep@1.0.0", "+ self-file-dep@1.0.0", "", "13 packages installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ "@another-scope", "@scoped", "aliased-file-dep", "dep-file-dep", "file-dep", "missing-file-dep", "self-file-dep", ]); await checkHoistedFiles(); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); err = await stderr.text(); out = await stdout.text(); expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(err).not.toContain("panic:"); 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); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); await checkHoistedFiles(); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); err = await stderr.text(); out = await stdout.text(); expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(err).not.toContain("panic:"); expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ "@another-scope", "@scoped", "aliased-file-dep", "dep-file-dep", "file-dep", "missing-file-dep", "self-file-dep", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); await checkHoistedFiles(); }); test("it should install folder dependencies with absolute paths", async () => { async function writePackages(num: number) { await rm(join(packageDir, `pkg0`), { recursive: true, force: true }); for (let i = 0; i < num; i++) { await mkdir(join(packageDir, `pkg${i}`)); await writeFile( join(packageDir, `pkg${i}`, "package.json"), JSON.stringify({ name: `pkg${i}`, version: "1.1.1", }), ); } } await writePackages(2); await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { // without and without file protocol "pkg0": `file:${resolve(packageDir, "pkg0").replace(/\\/g, "\\\\")}`, "pkg1": `${resolve(packageDir, "pkg1").replace(/\\/g, "\\\\")}`, }, }), ); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stderr: "pipe", stdin: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(err).not.toContain("panic:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ pkg0@pkg0", "+ pkg1@pkg1", "", "2 packages installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["pkg0", "pkg1"]); expect(await file(join(packageDir, "node_modules", "pkg0", "package.json")).json()).toEqual({ name: "pkg0", version: "1.1.1", }); expect(await file(join(packageDir, "node_modules", "pkg1", "package.json")).json()).toEqual({ name: "pkg1", version: "1.1.1", }); }); }); test("name from manifest is scoped and url encoded", async () => { await write( packageJson, JSON.stringify({ name: "foo", dependencies: { // `name` in the manifest for these packages is manually changed // to use `%40` and `%2f` "@url/encoding.2": "1.0.1", "@url/encoding.3": "1.0.1", }, }), ); await runBunInstall(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); const files = await Promise.all([ file(join(packageDir, "node_modules", "@url", "encoding.2", "package.json")).json(), file(join(packageDir, "node_modules", "@url", "encoding.3", "package.json")).json(), ]); expect(files).toEqual([ { name: "@url/encoding.2", version: "1.0.1" }, { name: "@url/encoding.3", version: "1.0.1" }, ]); }); describe("update", () => { test("duplicate peer dependency (one package is invalid_package_id)", async () => { await write( packageJson, JSON.stringify({ name: "foo", dependencies: { "no-deps": "^1.0.0", }, peerDependencies: { "no-deps": "^1.0.0", }, }), ); await runBunUpdate(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(packageJson).json()).toEqual({ name: "foo", dependencies: { "no-deps": "^1.1.0", }, peerDependencies: { "no-deps": "^1.0.0", }, }); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ version: "1.1.0", }); }); test("dist-tags", async () => { await write( packageJson, JSON.stringify({ name: "foo", dependencies: { "a-dep": "latest", }, }), ); await runBunInstall(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toMatchObject({ name: "a-dep", version: "1.0.10", }); // Update without args, `latest` should stay await runBunUpdate(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(packageJson).json()).toEqual({ name: "foo", dependencies: { "a-dep": "latest", }, }); // Update with `a-dep` and `--latest`, `latest` should be replaced with the installed version await runBunUpdate(env, packageDir, ["a-dep"]); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(packageJson).json()).toEqual({ name: "foo", dependencies: { "a-dep": "^1.0.10", }, }); await runBunUpdate(env, packageDir, ["--latest"]); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(packageJson).json()).toEqual({ name: "foo", dependencies: { "a-dep": "^1.0.10", }, }); }); test("exact versions stay exact", async () => { const runs = [ { version: "1.0.1", dependency: "a-dep" }, { version: "npm:a-dep@1.0.1", dependency: "aliased" }, ]; for (const { version, dependency } of runs) { await write( packageJson, JSON.stringify({ name: "foo", dependencies: { [dependency]: version, }, }), ); async function check(version: string) { assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(join(packageDir, "node_modules", dependency, "package.json")).json()).toMatchObject({ name: "a-dep", version: version.replace(/.*@/, ""), }); expect(await file(packageJson).json()).toMatchObject({ dependencies: { [dependency]: version, }, }); } await runBunInstall(env, packageDir); await check(version); await runBunUpdate(env, packageDir); await check(version); await runBunUpdate(env, packageDir, [dependency]); await check(version); // this will actually update the package, but the version should remain exact await runBunUpdate(env, packageDir, ["--latest"]); await check(dependency === "aliased" ? "npm:a-dep@1.0.10" : "1.0.10"); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); await rm(join(packageDir, "bun.lockb")); } }); describe("tilde", () => { test("without args", async () => { await write( packageJson, JSON.stringify({ name: "foo", dependencies: { "no-deps": "~1.0.0", }, }), ); await runBunInstall(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ name: "no-deps", version: "1.0.1", }); let { out } = await runBunUpdate(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out).toEqual([ expect.stringContaining("bun update v1."), "", "Checked 1 install across 2 packages (no changes)", ]); expect(await file(packageJson).json()).toEqual({ name: "foo", dependencies: { "no-deps": "~1.0.1", }, }); // another update does not change anything (previously the version would update because it was changed to `^1.0.1`) ({ out } = await runBunUpdate(env, packageDir)); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out).toEqual([ expect.stringContaining("bun update v1."), "", "Checked 1 install across 2 packages (no changes)", ]); expect(await file(packageJson).json()).toEqual({ name: "foo", dependencies: { "no-deps": "~1.0.1", }, }); }); for (const latest of [true, false]) { test(`update no args${latest ? " --latest" : ""}`, async () => { await write( packageJson, JSON.stringify({ name: "foo", dependencies: { "a1": "npm:no-deps@1", "a10": "npm:no-deps@~1.0", "a11": "npm:no-deps@^1.0", "a12": "npm:no-deps@~1.0.1", "a13": "npm:no-deps@^1.0.1", "a14": "npm:no-deps@~1.1.0", "a15": "npm:no-deps@^1.1.0", "a2": "npm:no-deps@1.0", "a3": "npm:no-deps@1.1", "a4": "npm:no-deps@1.0.1", "a5": "npm:no-deps@1.1.0", "a6": "npm:no-deps@~1", "a7": "npm:no-deps@^1", "a8": "npm:no-deps@~1.1", "a9": "npm:no-deps@^1.1", }, }), ); if (latest) { await runBunUpdate(env, packageDir, ["--latest"]); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(packageJson).json()).toEqual({ name: "foo", dependencies: { "a1": "npm:no-deps@^2.0.0", "a10": "npm:no-deps@~2.0.0", "a11": "npm:no-deps@^2.0.0", "a12": "npm:no-deps@~2.0.0", "a13": "npm:no-deps@^2.0.0", "a14": "npm:no-deps@~2.0.0", "a15": "npm:no-deps@^2.0.0", "a2": "npm:no-deps@~2.0.0", "a3": "npm:no-deps@~2.0.0", "a4": "npm:no-deps@2.0.0", "a5": "npm:no-deps@2.0.0", "a6": "npm:no-deps@~2.0.0", "a7": "npm:no-deps@^2.0.0", "a8": "npm:no-deps@~2.0.0", "a9": "npm:no-deps@^2.0.0", }, }); } else { await runBunUpdate(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(packageJson).json()).toEqual({ name: "foo", dependencies: { "a1": "npm:no-deps@^1.1.0", "a10": "npm:no-deps@~1.0.1", "a11": "npm:no-deps@^1.1.0", "a12": "npm:no-deps@~1.0.1", "a13": "npm:no-deps@^1.1.0", "a14": "npm:no-deps@~1.1.0", "a15": "npm:no-deps@^1.1.0", "a2": "npm:no-deps@~1.0.1", "a3": "npm:no-deps@~1.1.0", "a4": "npm:no-deps@1.0.1", "a5": "npm:no-deps@1.1.0", "a6": "npm:no-deps@~1.1.0", "a7": "npm:no-deps@^1.1.0", "a8": "npm:no-deps@~1.1.0", "a9": "npm:no-deps@^1.1.0", }, }); } const files = await Promise.all( ["a1", "a10", "a11", "a12", "a13", "a14", "a15", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9"].map(alias => file(join(packageDir, "node_modules", alias, "package.json")).json(), ), ); if (latest) { // each version should be "2.0.0" expect(files).toMatchObject(Array(15).fill({ version: "2.0.0" })); } else { expect(files).toMatchObject([ { version: "1.1.0" }, { version: "1.0.1" }, { version: "1.1.0" }, { version: "1.0.1" }, { version: "1.1.0" }, { version: "1.1.0" }, { version: "1.1.0" }, { version: "1.0.1" }, { version: "1.1.0" }, { version: "1.0.1" }, { version: "1.1.0" }, { version: "1.1.0" }, { version: "1.1.0" }, { version: "1.1.0" }, { version: "1.1.0" }, ]); } }); } test("with package name in args", async () => { await write( packageJson, JSON.stringify({ name: "foo", dependencies: { "a-dep": "1.0.3", "no-deps": "~1.0.0", }, }), ); await runBunInstall(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ name: "no-deps", version: "1.0.1", }); let { out } = await runBunUpdate(env, packageDir, ["no-deps"]); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out).toEqual([ expect.stringContaining("bun update v1."), "", "installed no-deps@1.0.1", "", expect.stringContaining("done"), "", ]); expect(await file(packageJson).json()).toEqual({ name: "foo", dependencies: { "a-dep": "1.0.3", "no-deps": "~1.0.1", }, }); // update with --latest should only change the update request and keep `~` ({ out } = await runBunUpdate(env, packageDir, ["no-deps", "--latest"])); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out).toEqual([ expect.stringContaining("bun update v1."), "", "installed no-deps@2.0.0", "", "1 package installed", ]); expect(await file(packageJson).json()).toEqual({ name: "foo", dependencies: { "a-dep": "1.0.3", "no-deps": "~2.0.0", }, }); }); }); describe("alises", () => { test("update all", async () => { await write( packageJson, JSON.stringify({ name: "foo", dependencies: { "aliased-dep": "npm:no-deps@^1.0.0", }, }), ); await runBunUpdate(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(packageJson).json()).toEqual({ name: "foo", dependencies: { "aliased-dep": "npm:no-deps@^1.1.0", }, }); expect(await file(join(packageDir, "node_modules", "aliased-dep", "package.json")).json()).toMatchObject({ name: "no-deps", version: "1.1.0", }); }); test("update specific aliased package", async () => { await write( packageJson, JSON.stringify({ name: "foo", dependencies: { "aliased-dep": "npm:no-deps@^1.0.0", }, }), ); await runBunUpdate(env, packageDir, ["aliased-dep"]); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(packageJson).json()).toEqual({ name: "foo", dependencies: { "aliased-dep": "npm:no-deps@^1.1.0", }, }); expect(await file(join(packageDir, "node_modules", "aliased-dep", "package.json")).json()).toMatchObject({ name: "no-deps", version: "1.1.0", }); }); test("with pre and build tags", async () => { await write( packageJson, JSON.stringify({ name: "foo", dependencies: { "aliased-dep": "npm:prereleases-3@5.0.0-alpha.150", }, }), ); await runBunUpdate(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(packageJson).json()).toMatchObject({ name: "foo", dependencies: { "aliased-dep": "npm:prereleases-3@5.0.0-alpha.150", }, }); expect(await file(join(packageDir, "node_modules", "aliased-dep", "package.json")).json()).toMatchObject({ name: "prereleases-3", version: "5.0.0-alpha.150", }); const { out } = await runBunUpdate(env, packageDir, ["--latest"]); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out).toEqual([ expect.stringContaining("bun update v1."), "", "^ aliased-dep 5.0.0-alpha.150 -> 5.0.0-alpha.153", "", "1 package installed", ]); expect(await file(packageJson).json()).toMatchObject({ name: "foo", dependencies: { "aliased-dep": "npm:prereleases-3@5.0.0-alpha.153", }, }); }); }); test("--no-save will update packages in node_modules and not save to package.json", async () => { await write( packageJson, JSON.stringify({ name: "foo", dependencies: { "a-dep": "1.0.1", }, }), ); let { out } = await runBunUpdate(env, packageDir, ["--no-save"]); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out).toEqual([ expect.stringContaining("bun update v1."), "", expect.stringContaining("+ a-dep@1.0.1"), "", "1 package installed", ]); expect(await file(packageJson).json()).toEqual({ name: "foo", dependencies: { "a-dep": "1.0.1", }, }); await write( packageJson, JSON.stringify({ name: "foo", dependencies: { "a-dep": "^1.0.1", }, }), ); ({ out } = await runBunUpdate(env, packageDir, ["--no-save"])); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out).toEqual([ expect.stringContaining("bun update v1."), "", expect.stringContaining("+ a-dep@1.0.10"), "", "1 package installed", ]); expect(await file(packageJson).json()).toEqual({ name: "foo", dependencies: { "a-dep": "^1.0.1", }, }); // now save ({ out } = await runBunUpdate(env, packageDir)); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out).toEqual([ expect.stringContaining("bun update v1."), "", "Checked 1 install across 2 packages (no changes)", ]); expect(await file(packageJson).json()).toEqual({ name: "foo", dependencies: { "a-dep": "^1.0.10", }, }); }); test("update won't update beyond version range unless the specified version allows it", async () => { await write( packageJson, JSON.stringify({ name: "foo", dependencies: { "dep-with-tags": "^1.0.0", }, }), ); await runBunUpdate(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(packageJson).json()).toEqual({ name: "foo", dependencies: { "dep-with-tags": "^1.0.1", }, }); expect(await file(join(packageDir, "node_modules", "dep-with-tags", "package.json")).json()).toMatchObject({ version: "1.0.1", }); // update with package name does not update beyond version range await runBunUpdate(env, packageDir, ["dep-with-tags"]); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(packageJson).json()).toEqual({ name: "foo", dependencies: { "dep-with-tags": "^1.0.1", }, }); expect(await file(join(packageDir, "node_modules", "dep-with-tags", "package.json")).json()).toMatchObject({ version: "1.0.1", }); // now update with a higher version range await runBunUpdate(env, packageDir, ["dep-with-tags@^2.0.0"]); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(packageJson).json()).toEqual({ name: "foo", dependencies: { "dep-with-tags": "^2.0.1", }, }); expect(await file(join(packageDir, "node_modules", "dep-with-tags", "package.json")).json()).toMatchObject({ version: "2.0.1", }); }); test("update should update all packages in the current workspace", async () => { await write( packageJson, JSON.stringify({ name: "foo", workspaces: ["packages/*"], dependencies: { "what-bin": "^1.0.0", "uses-what-bin": "^1.0.0", "optional-native": "^1.0.0", "peer-deps-too": "^1.0.0", "two-range-deps": "^1.0.0", "one-fixed-dep": "^1.0.0", "no-deps-bins": "^2.0.0", "left-pad": "^1.0.0", "native": "1.0.0", "dep-loop-entry": "1.0.0", "dep-with-tags": "^2.0.0", "dev-deps": "1.0.0", "a-dep": "^1.0.0", }, }), ); const originalWorkspaceJSON = { name: "pkg1", version: "1.0.0", dependencies: { "what-bin": "^1.0.0", "uses-what-bin": "^1.0.0", "optional-native": "^1.0.0", "peer-deps-too": "^1.0.0", "two-range-deps": "^1.0.0", "one-fixed-dep": "^1.0.0", "no-deps-bins": "^2.0.0", "left-pad": "^1.0.0", "native": "1.0.0", "dep-loop-entry": "1.0.0", "dep-with-tags": "^2.0.0", "dev-deps": "1.0.0", "a-dep": "^1.0.0", }, }; await write(join(packageDir, "packages", "pkg1", "package.json"), JSON.stringify(originalWorkspaceJSON)); // initial install, update root let { out } = await runBunUpdate(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out).toEqual([ expect.stringContaining("bun update v1."), "", "+ a-dep@1.0.10", "+ dep-loop-entry@1.0.0", expect.stringContaining("+ dep-with-tags@2.0.1"), "+ dev-deps@1.0.0", "+ left-pad@1.0.0", "+ native@1.0.0", "+ no-deps-bins@2.0.0", expect.stringContaining("+ one-fixed-dep@1.0.0"), "+ optional-native@1.0.0", "+ peer-deps-too@1.0.0", "+ two-range-deps@1.0.0", expect.stringContaining("+ uses-what-bin@1.5.0"), expect.stringContaining("+ what-bin@1.5.0"), "", // Due to optional-native dependency, this can be either 20 or 19 packages expect.stringMatching(/(?:20|19) packages installed/), "", "Blocked 1 postinstall. Run `bun pm untrusted` for details.", "", ]); let lockfile = parseLockfile(packageDir); // make sure this is valid expect(lockfile).toMatchNodeModulesAt(packageDir); expect(await file(packageJson).json()).toEqual({ name: "foo", workspaces: ["packages/*"], dependencies: { "what-bin": "^1.5.0", "uses-what-bin": "^1.5.0", "optional-native": "^1.0.0", "peer-deps-too": "^1.0.0", "two-range-deps": "^1.0.0", "one-fixed-dep": "^1.0.0", "no-deps-bins": "^2.0.0", "left-pad": "^1.0.0", "native": "1.0.0", "dep-loop-entry": "1.0.0", "dep-with-tags": "^2.0.1", "dev-deps": "1.0.0", "a-dep": "^1.0.10", }, }); // workspace hasn't changed expect(await file(join(packageDir, "packages", "pkg1", "package.json")).json()).toEqual(originalWorkspaceJSON); // now update the workspace, first a couple packages, then all ({ out } = await runBunUpdate(env, join(packageDir, "packages", "pkg1"), [ "what-bin", "uses-what-bin", "a-dep@1.0.5", ])); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out).toEqual([ expect.stringContaining("bun update v1."), "", "installed what-bin@1.5.0 with binaries:", " - what-bin", "installed uses-what-bin@1.5.0", "installed a-dep@1.0.5", "", "3 packages installed", ]); // lockfile = parseLockfile(packageDir); // expect(lockfile).toMatchNodeModulesAt(packageDir); expect(await file(join(packageDir, "packages", "pkg1", "package.json")).json()).toMatchObject({ dependencies: { "what-bin": "^1.5.0", "uses-what-bin": "^1.5.0", "optional-native": "^1.0.0", "peer-deps-too": "^1.0.0", "two-range-deps": "^1.0.0", "one-fixed-dep": "^1.0.0", "no-deps-bins": "^2.0.0", "left-pad": "^1.0.0", "native": "1.0.0", "dep-loop-entry": "1.0.0", "dep-with-tags": "^2.0.0", "dev-deps": "1.0.0", // a-dep should keep caret "a-dep": "^1.0.5", }, }); expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toMatchObject({ name: "a-dep", version: "1.0.10", }); expect( await file(join(packageDir, "packages", "pkg1", "node_modules", "a-dep", "package.json")).json(), ).toMatchObject({ name: "a-dep", version: "1.0.5", }); ({ out } = await runBunUpdate(env, join(packageDir, "packages", "pkg1"), ["a-dep@^1.0.5"])); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out).toEqual([ expect.stringContaining("bun update v1."), "", "installed a-dep@1.0.10", "", expect.stringMatching(/(\[\d+\.\d+m?s\])/), "", ]); expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toMatchObject({ name: "a-dep", version: "1.0.10", }); expect(await file(join(packageDir, "packages", "pkg1", "package.json")).json()).toMatchObject({ dependencies: { "what-bin": "^1.5.0", "uses-what-bin": "^1.5.0", "optional-native": "^1.0.0", "peer-deps-too": "^1.0.0", "two-range-deps": "^1.0.0", "one-fixed-dep": "^1.0.0", "no-deps-bins": "^2.0.0", "left-pad": "^1.0.0", "native": "1.0.0", "dep-loop-entry": "1.0.0", "dep-with-tags": "^2.0.0", "dev-deps": "1.0.0", "a-dep": "^1.0.10", }, }); }); test("update different dependency groups", async () => { for (const args of [true, false]) { for (const group of ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"]) { await write( packageJson, JSON.stringify({ name: "foo", [group]: { "a-dep": "^1.0.0", }, }), ); const { out } = args ? await runBunUpdate(env, packageDir, ["a-dep"]) : await runBunUpdate(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out).toEqual([ expect.stringContaining("bun update v1."), "", args ? "installed a-dep@1.0.10" : expect.stringContaining("+ a-dep@1.0.10"), "", "1 package installed", ]); expect(await file(packageJson).json()).toEqual({ name: "foo", [group]: { "a-dep": "^1.0.10", }, }); await rm(join(packageDir, "node_modules"), { recursive: true, force: true }); await rm(join(packageDir, "bun.lockb")); } } }); test("it should update packages from update requests", async () => { await write( packageJson, JSON.stringify({ name: "foo", dependencies: { "no-deps": "1.0.0", }, workspaces: ["packages/*"], }), ); await write( join(packageDir, "packages", "pkg1", "package.json"), JSON.stringify({ name: "pkg1", version: "1.0.0", dependencies: { "a-dep": "^1.0.0", }, }), ); await write( join(packageDir, "packages", "pkg2", "package.json"), JSON.stringify({ name: "pkg2", dependencies: { "pkg1": "*", "is-number": "*", }, }), ); await runBunInstall(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ version: "1.0.0", }); expect(await file(join(packageDir, "node_modules", "a-dep", "package.json")).json()).toMatchObject({ version: "1.0.10", }); expect(await file(join(packageDir, "node_modules", "pkg1", "package.json")).json()).toMatchObject({ version: "1.0.0", }); // update no-deps, no range, no change let { out } = await runBunUpdate(env, packageDir, ["no-deps"]); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out).toEqual([ expect.stringContaining("bun update v1."), "", "installed no-deps@1.0.0", "", expect.stringMatching(/(\[\d+\.\d+m?s\])/), "", ]); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ version: "1.0.0", }); // update package that doesn't exist to workspace, should add to package.json ({ out } = await runBunUpdate(env, join(packageDir, "packages", "pkg1"), ["no-deps"])); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out).toEqual([ expect.stringContaining("bun update v1."), "", "installed no-deps@2.0.0", "", "1 package installed", ]); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ version: "1.0.0", }); expect(await file(join(packageDir, "packages", "pkg1", "package.json")).json()).toMatchObject({ name: "pkg1", version: "1.0.0", dependencies: { "a-dep": "^1.0.0", "no-deps": "^2.0.0", }, }); // update root package.json no-deps to ^1.0.0 and update it await write( packageJson, JSON.stringify({ name: "foo", dependencies: { "no-deps": "^1.0.0", }, workspaces: ["packages/*"], }), ); ({ out } = await runBunUpdate(env, packageDir, ["no-deps"])); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out).toEqual([ expect.stringContaining("bun update v1."), "", "installed no-deps@1.1.0", "", "1 package installed", ]); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ version: "1.1.0", }); }); test("--latest works with packages from arguments", async () => { await write( packageJson, JSON.stringify({ name: "foo", dependencies: { "no-deps": "1.0.0", }, }), ); await runBunUpdate(env, packageDir, ["no-deps", "--latest"]); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); const files = await Promise.all([ file(join(packageDir, "node_modules", "no-deps", "package.json")).json(), file(packageJson).json(), ]); expect(files).toMatchObject([{ version: "2.0.0" }, { dependencies: { "no-deps": "2.0.0" } }]); }); }); test("packages dependening on each other with aliases does not infinitely loop", async () => { await write( packageJson, JSON.stringify({ name: "foo", dependencies: { "alias-loop-1": "1.0.0", "alias-loop-2": "1.0.0", }, }), ); await runBunInstall(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); const files = await Promise.all([ file(join(packageDir, "node_modules", "alias-loop-1", "package.json")).json(), file(join(packageDir, "node_modules", "alias-loop-2", "package.json")).json(), file(join(packageDir, "node_modules", "alias1", "package.json")).json(), file(join(packageDir, "node_modules", "alias2", "package.json")).json(), ]); expect(files).toMatchObject([ { name: "alias-loop-1", version: "1.0.0" }, { name: "alias-loop-2", version: "1.0.0" }, { name: "alias-loop-2", version: "1.0.0" }, { name: "alias-loop-1", version: "1.0.0" }, ]); }); test("it should re-populate .bin folder if package is reinstalled", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", dependencies: { "what-bin": "1.5.0", }, }), ); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stderr: "pipe", stdout: "pipe", stdin: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ what-bin@1.5.0", "", "1 package installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); const bin = process.platform === "win32" ? "what-bin.exe" : "what-bin"; expect(Bun.which("what-bin", { PATH: join(packageDir, "node_modules", ".bin") })).toBe( join(packageDir, "node_modules", ".bin", bin), ); if (process.platform === "win32") { expect(join(packageDir, "node_modules", ".bin", "what-bin")).toBeValidBin(join("..", "what-bin", "what-bin.js")); } else { expect(await file(join(packageDir, "node_modules", ".bin", bin)).text()).toContain("what-bin@1.5.0"); } await rm(join(packageDir, "node_modules", ".bin"), { recursive: true, force: true }); await rm(join(packageDir, "node_modules", "what-bin", "package.json"), { recursive: true, force: true }); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stderr: "pipe", stdout: "pipe", stdin: "pipe", env, })); err = await stderr.text(); out = await stdout.text(); expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ what-bin@1.5.0", "", "1 package installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(Bun.which("what-bin", { PATH: join(packageDir, "node_modules", ".bin") })).toBe( join(packageDir, "node_modules", ".bin", bin), ); if (process.platform === "win32") { expect(join(packageDir, "node_modules", ".bin", "what-bin")).toBeValidBin(join("..", "what-bin", "what-bin.js")); } else { expect(await file(join(packageDir, "node_modules", ".bin", "what-bin")).text()).toContain("what-bin@1.5.0"); } }); test("one version with binary map", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", dependencies: { "map-bin": "1.0.2", }, }), ); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stderr: "pipe", stdout: "pipe", env, }); const err = await stderr.text(); const out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ map-bin@1.0.2", "", "1 package installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toHaveBins(["map-bin", "map_bin"]); expect(join(packageDir, "node_modules", ".bin", "map-bin")).toBeValidBin(join("..", "map-bin", "bin", "map-bin")); expect(join(packageDir, "node_modules", ".bin", "map_bin")).toBeValidBin(join("..", "map-bin", "bin", "map-bin")); }); test("multiple versions with binary map", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.2.3", dependencies: { "map-bin-multiple": "1.0.2", }, }), ); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stderr: "pipe", stdout: "pipe", env, }); const err = await stderr.text(); const out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ map-bin-multiple@1.0.2", "", "1 package installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toHaveBins(["map-bin", "map_bin"]); expect(join(packageDir, "node_modules", ".bin", "map-bin")).toBeValidBin( join("..", "map-bin-multiple", "bin", "map-bin"), ); expect(join(packageDir, "node_modules", ".bin", "map_bin")).toBeValidBin( join("..", "map-bin-multiple", "bin", "map-bin"), ); }); test("duplicate dependency in optionalDependencies maintains sort order", async () => { await write( packageJson, JSON.stringify({ name: "foo", dependencies: { // `duplicate-optional` has `no-deps` as a normal dependency (1.0.0) and as an // optional dependency (1.0.1). The optional dependency version should be installed and // the sort order should remain the same (tested by `bun-debug bun.lockb`). "duplicate-optional": "1.0.1", }, }), ); await runBunInstall(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); const lockfile = parseLockfile(packageDir); expect(lockfile).toMatchNodeModulesAt(packageDir); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toMatchObject({ version: "1.0.1", }); const { stdout, exited } = spawn({ cmd: [bunExe(), "bun.lockb"], cwd: packageDir, stderr: "inherit", stdout: "pipe", env, }); const out = await stdout.text(); expect(out.replaceAll(`${port}`, "4873")).toMatchSnapshot(); expect(await exited).toBe(0); }); test("missing package on reinstall, some with binaries", async () => { await writeFile( packageJson, JSON.stringify({ name: "fooooo", dependencies: { "what-bin": "1.0.0", "uses-what-bin": "1.5.0", "optional-native": "1.0.0", "peer-deps-too": "1.0.0", "two-range-deps": "1.0.0", "one-fixed-dep": "2.0.0", "no-deps-bins": "2.0.0", "left-pad": "1.0.0", "native": "1.0.0", "dep-loop-entry": "1.0.0", "dep-with-tags": "3.0.0", "dev-deps": "1.0.0", }, }), ); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stderr: "pipe", stdout: "pipe", stdin: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ dep-loop-entry@1.0.0", expect.stringContaining("+ dep-with-tags@3.0.0"), "+ dev-deps@1.0.0", "+ left-pad@1.0.0", "+ native@1.0.0", "+ no-deps-bins@2.0.0", "+ one-fixed-dep@2.0.0", "+ optional-native@1.0.0", "+ peer-deps-too@1.0.0", "+ two-range-deps@1.0.0", expect.stringContaining("+ uses-what-bin@1.5.0"), expect.stringContaining("+ what-bin@1.0.0"), "", "19 packages installed", "", "Blocked 1 postinstall. Run `bun pm untrusted` for details.", "", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); await rm(join(packageDir, "node_modules", "native"), { recursive: true, force: true }); await rm(join(packageDir, "node_modules", "left-pad"), { recursive: true, force: true }); await rm(join(packageDir, "node_modules", "dep-loop-entry"), { recursive: true, force: true }); await rm(join(packageDir, "node_modules", "one-fixed-dep"), { recursive: true, force: true }); await rm(join(packageDir, "node_modules", "peer-deps-too"), { recursive: true, force: true }); await rm(join(packageDir, "node_modules", "two-range-deps", "node_modules", "no-deps"), { recursive: true, force: true, }); await rm(join(packageDir, "node_modules", "one-fixed-dep"), { recursive: true, force: true }); await rm(join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin"), { recursive: true, force: true }); await rm(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin"), { recursive: true, force: true, }); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stderr: "pipe", stdout: "pipe", stdin: "pipe", env, })); err = await stderr.text(); out = await stdout.text(); expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ dep-loop-entry@1.0.0", "+ left-pad@1.0.0", "+ native@1.0.0", "+ one-fixed-dep@2.0.0", "+ peer-deps-too@1.0.0", "", "7 packages installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await exists(join(packageDir, "node_modules", "native", "package.json"))).toBe(true); expect(await exists(join(packageDir, "node_modules", "left-pad", "package.json"))).toBe(true); expect(await exists(join(packageDir, "node_modules", "dep-loop-entry", "package.json"))).toBe(true); expect(await exists(join(packageDir, "node_modules", "one-fixed-dep", "package.json"))).toBe(true); expect(await exists(join(packageDir, "node_modules", "peer-deps-too", "package.json"))).toBe(true); expect(await exists(join(packageDir, "node_modules", "two-range-deps", "node_modules", "no-deps"))).toBe(true); expect(await exists(join(packageDir, "node_modules", "one-fixed-dep", "package.json"))).toBe(true); expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin"))).toBe(true); expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin"))).toBe(true); const bin = process.platform === "win32" ? "what-bin.exe" : "what-bin"; expect(Bun.which("what-bin", { PATH: join(packageDir, "node_modules", ".bin") })).toBe( join(packageDir, "node_modules", ".bin", bin), ); expect( Bun.which("what-bin", { PATH: join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin") }), ).toBe(join(packageDir, "node_modules", "uses-what-bin", "node_modules", ".bin", bin)); }); describe("pm trust", async () => { test("--default", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", }), ); let { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "pm", "default-trusted"], cwd: packageDir, stdout: "pipe", stderr: "pipe", env, }); let err = stderrForInstall(await stderr.text()); expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(err).not.toContain("warn:"); let out = await stdout.text(); expect(out).toContain("Default trusted dependencies"); expect(await exited).toBe(0); }); describe("--all", async () => { test("no dependencies", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", }), ); let { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "pm", "trust", "--all"], cwd: packageDir, stdout: "pipe", stderr: "pipe", env, }); let err = stderrForInstall(await stderr.text()); expect(err).toContain("error: Lockfile not found"); let out = await stdout.text(); expect(out).toBeEmpty(); expect(await exited).toBe(1); }); test("some dependencies, non with scripts", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", dependencies: { "uses-what-bin": "1.0.0", }, }), ); let { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "i"], cwd: packageDir, stdout: "pipe", stderr: "pipe", env, }); let err = stderrForInstall(await stderr.text()); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(err).not.toContain("warn:"); let out = await stdout.text(); expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", expect.stringContaining("+ uses-what-bin@1.0.0"), "", "2 packages installed", "", "Blocked 1 postinstall. Run `bun pm untrusted` for details.", "", ]); expect(await exited).toBe(0); expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse(); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "pm", "trust", "uses-what-bin"], cwd: packageDir, stdout: "pipe", stderr: "pipe", env, })); err = stderrForInstall(await stderr.text()); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(err).not.toContain("warn:"); out = await stdout.text(); expect(out).toContain("1 script ran across 1 package"); expect(await exited).toBe(0); expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue(); }); }); }); test("it should be able to find binary in node_modules/.bin from parent directory of root package", async () => { await mkdir(join(packageDir, "node_modules", ".bin"), { recursive: true }); await mkdir(join(packageDir, "morePackageDir")); await writeFile( join(packageDir, "morePackageDir", "package.json"), JSON.stringify({ name: "foo", version: "1.0.0", scripts: { install: "missing-bin", }, dependencies: { "what-bin": "1.0.0", }, }), ); await cp(join(packageDir, "bunfig.toml"), join(packageDir, "morePackageDir", "bunfig.toml")); await writeShebangScript( join(packageDir, "node_modules", ".bin", "missing-bin"), "node", `require("fs").writeFileSync("missing-bin.txt", "missing-bin@WHAT");`, ); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: join(packageDir, "morePackageDir"), stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); const err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); 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("+ what-bin@1.0.0"), "", "1 package installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(await file(join(packageDir, "morePackageDir", "missing-bin.txt")).text()).toBe("missing-bin@WHAT"); }); describe("semver", () => { const taggedVersionTests = [ { title: "tagged version last in range", depVersion: "1 || 2 || pre-3", expected: "2.0.1", }, { title: "tagged version in middle of range", depVersion: "1 || pre-3 || 2", expected: "2.0.1", }, { title: "tagged version first in range", depVersion: "pre-3 || 2 || 1", expected: "2.0.1", }, { title: "multiple tagged versions in range", depVersion: "pre-3 || 2 || pre-1 || 1 || 3 || pre-3", expected: "3.0.0", }, { title: "start with ||", depVersion: "|| 1", expected: "1.0.1", }, { title: "start with || no space", depVersion: "||2", expected: "2.0.1", }, { title: "|| with no space on both sides", depVersion: "1||2", expected: "2.0.1", }, { title: "no version is latest", depVersion: "", expected: "3.0.0", }, { title: "tagged version works", depVersion: "pre-2", expected: "2.0.1", }, { title: "tagged above latest", depVersion: "pre-3", expected: "3.0.1", }, { title: "'||'", depVersion: "||", expected: "3.0.0", }, { title: "'|'", depVersion: "|", expected: "3.0.0", }, { title: "'|||'", depVersion: "|||", expected: "3.0.0", }, { title: "'|| ||'", depVersion: "|| ||", expected: "3.0.0", }, { title: "'|| 1 ||'", depVersion: "|| 1 ||", expected: "1.0.1", }, { title: "'| | |'", depVersion: "| | |", expected: "3.0.0", }, { title: "'|||||||||||||||||||||||||'", depVersion: "|||||||||||||||||||||||||", expected: "3.0.0", }, { title: "'2 ||| 1'", depVersion: "2 ||| 1", expected: "2.0.1", }, { title: "'2 |||| 1'", depVersion: "2 |||| 1", expected: "2.0.1", }, ]; for (const { title, depVersion, expected } of taggedVersionTests) { test(title, async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "dep-with-tags": depVersion, }, }), ); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", expect.stringContaining(`+ dep-with-tags@${expected}`), "", "1 package installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); } test.todo("only tagged versions in range errors", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "dep-with-tags": "pre-1 || pre-2", }, }), ); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); expect(err).toContain('InvalidDependencyVersion parsing version "pre-1 || pre-2"'); expect(await exited).toBe(1); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); expect(out).toEqual(expect.stringContaining("bun install v1.")); }); }); test("doesn't error when the migration is out of sync", async () => { const cwd = tempDirWithFiles("out-of-sync-1", { "package.json": JSON.stringify({ "devDependencies": { "no-deps": "1.0.0", }, }), "package-lock.json": JSON.stringify({ "name": "reproo", "lockfileVersion": 3, "configVersion": 1, "requires": true, "packages": { "": { "name": "reproo", "dependencies": { "no-deps": "2.0.0", }, "devDependencies": { "no-deps": "1.0.0", }, }, "node_modules/no-deps": { "version": "1.0.0", "resolved": `http://localhost:${port}/no-deps/-/no-deps-1.0.0.tgz`, "integrity": "sha512-v4w12JRjUGvfHDUP8vFDwu0gUWu04j0cv9hLb1Abf9VdaXu4XcrddYFTMVBVvmldKViGWH7jrb6xPJRF0wq6gw==", "dev": true, }, }, }), }); const subprocess = Bun.spawn([bunExe(), "install"], { env, cwd, stdio: ["ignore", "ignore", "inherit"], }); await subprocess.exited; expect(subprocess.exitCode).toBe(0); let { stdout, exitCode } = Bun.spawnSync({ cmd: [bunExe(), "pm", "ls"], env, cwd, stdio: ["ignore", "pipe", "inherit"], }); let out = stdout.toString().trim(); expect(out).toContain("no-deps@1.0.0"); // only one no-deps is installed expect(out.lastIndexOf("no-deps")).toEqual(out.indexOf("no-deps")); expect(exitCode).toBe(0); expect(await file(join(cwd, "node_modules/no-deps/package.json")).json()).toMatchObject({ version: "1.0.0", name: "no-deps", }); }); const prereleaseTests = [ [ { title: "specific", depVersion: "1.0.0-future.1", expected: "1.0.0-future.1" }, { title: "latest", depVersion: "latest", expected: "1.0.0-future.4" }, { title: "range starting with latest", depVersion: "^1.0.0-future.4", expected: "1.0.0-future.4" }, { title: "range above latest", depVersion: "^1.0.0-future.5", expected: "1.0.0-future.7" }, ], [ { title: "#6683", depVersion: "^1.0.0-next.23", expected: "1.0.0-next.23" }, { title: "greater than or equal to", depVersion: ">=1.0.0-next.23", expected: "1.0.0-next.23", }, { title: "latest", depVersion: "latest", expected: "0.5.0" }, { title: "greater than or equal to latest", depVersion: ">=0.5.0", expected: "0.5.0" }, ], // package "prereleases-3" has four versions, all with prerelease tags: // - 5.0.0-alpha.150 // - 5.0.0-alpha.151 // - 5.0.0-alpha.152 // - 5.0.0-alpha.153 [ { title: "#6956", depVersion: "^5.0.0-alpha.153", expected: "5.0.0-alpha.153" }, { title: "range matches highest possible", depVersion: "^5.0.0-alpha.152", expected: "5.0.0-alpha.153" }, { title: "exact", depVersion: "5.0.0-alpha.152", expected: "5.0.0-alpha.152" }, { title: "exact latest", depVersion: "5.0.0-alpha.153", expected: "5.0.0-alpha.153" }, { title: "latest", depVersion: "latest", expected: "5.0.0-alpha.153" }, { title: "~ lower than latest", depVersion: "~5.0.0-alpha.151", expected: "5.0.0-alpha.153" }, { title: "~ equal semver and lower non-existant prerelease", depVersion: "~5.0.0-alpha.100", expected: "5.0.0-alpha.153", }, { title: "^ equal semver and lower non-existant prerelease", depVersion: "^5.0.0-alpha.100", expected: "5.0.0-alpha.153", }, { title: "~ and ^ latest prerelease", depVersion: "~5.0.0-alpha.153 || ^5.0.0-alpha.153", expected: "5.0.0-alpha.153", }, { title: "< latest prerelease", depVersion: "<5.0.0-alpha.153", expected: "5.0.0-alpha.152", }, { title: "< lower than latest prerelease", depVersion: "<5.0.0-alpha.152", expected: "5.0.0-alpha.151", }, { title: "< higher than latest prerelease", depVersion: "<5.0.0-alpha.22343423", expected: "5.0.0-alpha.153", }, { title: "< at lowest possible version", depVersion: "<5.0.0-alpha.151", expected: "5.0.0-alpha.150", }, { title: "<= latest prerelease", depVersion: "<=5.0.0-alpha.153", expected: "5.0.0-alpha.153", }, { title: "<= lower than latest prerelease", depVersion: "<=5.0.0-alpha.152", expected: "5.0.0-alpha.152", }, { title: "<= lowest possible version", depVersion: "<=5.0.0-alpha.150", expected: "5.0.0-alpha.150", }, { title: "<= higher than latest prerelease", depVersion: "<=5.0.0-alpha.153261345", expected: "5.0.0-alpha.153", }, { title: "> latest prerelease", depVersion: ">=5.0.0-alpha.153", expected: "5.0.0-alpha.153", }, ], ]; for (let i = 0; i < prereleaseTests.length; i++) { const tests = prereleaseTests[i]; const depName = `prereleases-${i + 1}`; describe(`${depName} should pass`, () => { for (const { title, depVersion, expected } of tests) { test(title, async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { [`${depName}`]: depVersion, }, }), ); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); const err = await stderr.text(); const out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", `+ ${depName}@${expected}`, "", "1 package installed", ]); expect(await file(join(packageDir, "node_modules", depName, "package.json")).json()).toEqual({ name: depName, version: expected, } as any); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); } }); } const prereleaseFailTests = [ [ // { title: "specific", depVersion: "1.0.0-future.1", expected: "1.0.0-future.1" }, // { title: "latest", depVersion: "latest", expected: "1.0.0-future.4" }, // { title: "range starting with latest", depVersion: "^1.0.0-future.4", expected: "1.0.0-future.4" }, // { title: "range above latest", depVersion: "^1.0.0-future.5", expected: "1.0.0-future.7" }, ], [ // { title: "#6683", depVersion: "^1.0.0-next.23", expected: "1.0.0-next.23" }, // { // title: "greater than or equal to", // depVersion: ">=1.0.0-next.23", // expected: "1.0.0-next.23", // }, // { title: "latest", depVersion: "latest", expected: "0.5.0" }, // { title: "greater than or equal to latest", depVersion: ">=0.5.0", expected: "0.5.0" }, ], // package "prereleases-3" has four versions, all with prerelease tags: // - 5.0.0-alpha.150 // - 5.0.0-alpha.151 // - 5.0.0-alpha.152 // - 5.0.0-alpha.153 [ { title: "^ with higher non-existant prerelease", depVersion: "^5.0.0-alpha.1000", }, { title: "~ with higher non-existant prerelease", depVersion: "~5.0.0-alpha.1000", }, { title: "> with higher non-existant prerelease", depVersion: ">5.0.0-alpha.1000", }, { title: ">= with higher non-existant prerelease", depVersion: ">=5.0.0-alpha.1000", }, { title: "^4.3.0", depVersion: "^4.3.0", }, { title: "~4.3.0", depVersion: "~4.3.0", }, { title: ">4.3.0", depVersion: ">4.3.0", }, { title: ">=4.3.0", depVersion: ">=4.3.0", }, { title: "<5.0.0-alpha.150", depVersion: "<5.0.0-alpha.150", }, { title: "<=5.0.0-alpha.149", depVersion: "<=5.0.0-alpha.149", }, { title: "greater than highest prerelease", depVersion: ">5.0.0-alpha.153", }, { title: "greater than or equal to highest prerelease + 1", depVersion: ">=5.0.0-alpha.154", }, { title: "`.` instead of `-` should fail", depVersion: "5.0.0.alpha.150", }, ], // prereleases-4 has one version // - 2.0.0-pre.0 [ { title: "wildcard should not match prerelease", depVersion: "x", }, { title: "major wildcard should not match prerelease", depVersion: "x.0.0", }, { title: "minor wildcard should not match prerelease", depVersion: "2.x", }, { title: "patch wildcard should not match prerelease", depVersion: "2.0.x", }, ], ]; for (let i = 0; i < prereleaseFailTests.length; i++) { const tests = prereleaseFailTests[i]; const depName = `prereleases-${i + 1}`; describe(`${depName} should fail`, () => { for (const { title, depVersion } of tests) { test(title, async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { [`${depName}`]: depVersion, }, }), ); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); const err = await stderr.text(); const out = await stdout.text(); expect(out).toEqual(expect.stringContaining("bun install v1.")); expect(err).toContain(`No version matching "${depVersion}" found for specifier "${depName}"`); expect(await exited).toBe(1); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); } }); } describe("yarn tests", () => { test("dragon test 1", async () => { await writeFile( packageJson, JSON.stringify({ name: "dragon-test-1", version: "1.0.0", dependencies: { "dragon-test-1-d": "1.0.0", "dragon-test-1-e": "1.0.0", }, }), ); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); const err = await stderr.text(); const out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ dragon-test-1-d@1.0.0", "+ dragon-test-1-e@1.0.0", "", "6 packages installed", ]); expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ "dragon-test-1-a", "dragon-test-1-b", "dragon-test-1-c", "dragon-test-1-d", "dragon-test-1-e", ]); expect(await file(join(packageDir, "node_modules", "dragon-test-1-b", "package.json")).json()).toEqual({ name: "dragon-test-1-b", version: "2.0.0", } as any); expect(await readdirSorted(join(packageDir, "node_modules", "dragon-test-1-c", "node_modules"))).toEqual([ "dragon-test-1-b", ]); expect( await file( join(packageDir, "node_modules", "dragon-test-1-c", "node_modules", "dragon-test-1-b", "package.json"), ).json(), ).toEqual({ name: "dragon-test-1-b", version: "1.0.0", dependencies: { "dragon-test-1-a": "1.0.0", }, } as any); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test("dragon test 2", async () => { await writeFile( packageJson, JSON.stringify({ name: "dragon-test-2", version: "1.0.0", workspaces: ["dragon-test-2-a", "dragon-test-2-b"], dependencies: { "dragon-test-2-a": "1.0.0", }, }), ); await mkdir(join(packageDir, "dragon-test-2-a")); await mkdir(join(packageDir, "dragon-test-2-b")); await writeFile( join(packageDir, "dragon-test-2-a", "package.json"), JSON.stringify({ name: "dragon-test-2-a", version: "1.0.0", dependencies: { "dragon-test-2-b": "1.0.0", "no-deps": "1.0.0", }, }), ); await writeFile( join(packageDir, "dragon-test-2-b", "package.json"), JSON.stringify({ name: "dragon-test-2-b", version: "1.0.0", dependencies: { "no-deps": "*", }, }), ); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); const err = await stderr.text(); const out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ dragon-test-2-a@workspace:dragon-test-2-a", "", "3 packages installed", ]); expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ "dragon-test-2-a", "dragon-test-2-b", "no-deps", ]); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ name: "no-deps", version: "1.0.0", }); expect(await exists(join(packageDir, "dragon-test-2-a", "node_modules"))).toBeFalse(); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test("dragon test 3", async () => { await writeFile( packageJson, JSON.stringify({ name: "dragon-test-3", version: "1.0.0", dependencies: { "dragon-test-3-a": "1.0.0", }, }), ); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); const err = await stderr.text(); const out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ dragon-test-3-a@1.0.0", "", "3 packages installed", ]); expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ "dragon-test-3-a", "dragon-test-3-b", "no-deps", ]); expect(await file(join(packageDir, "node_modules", "dragon-test-3-a", "package.json")).json()).toEqual({ name: "dragon-test-3-a", version: "1.0.0", dependencies: { "dragon-test-3-b": "1.0.0", }, peerDependencies: { "no-deps": "*", }, } as any); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test("dragon test 4", async () => { await writeFile( packageJson, JSON.stringify({ "name": "dragon-test-4", "version": "1.0.0", "workspaces": ["my-workspace"], }), ); await mkdir(join(packageDir, "my-workspace")); await writeFile( join(packageDir, "my-workspace", "package.json"), JSON.stringify({ "name": "my-workspace", "version": "1.0.0", "peerDependencies": { "no-deps": "*", "peer-deps": "*", }, "devDependencies": { "no-deps": "1.0.0", "peer-deps": "1.0.0", }, }), ); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); const err = await stderr.text(); const out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "3 packages installed", ]); expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["my-workspace", "no-deps", "peer-deps"]); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ name: "no-deps", version: "1.0.0", } as any); expect(await file(join(packageDir, "node_modules", "peer-deps", "package.json")).json()).toEqual({ name: "peer-deps", version: "1.0.0", peerDependencies: { "no-deps": "*", }, } as any); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test("dragon test 5", async () => { await writeFile( packageJson, JSON.stringify({ "name": "dragon-test-5", "version": "1.0.0", "workspaces": ["packages/*"], }), ); await mkdir(join(packageDir, "packages", "a"), { recursive: true }); await mkdir(join(packageDir, "packages", "b"), { recursive: true }); await writeFile( join(packageDir, "packages", "a", "package.json"), JSON.stringify({ "name": "a", "peerDependencies": { "various-requires": "*", }, "devDependencies": { "no-deps": "1.0.0", "peer-deps": "1.0.0", }, }), ); await writeFile( join(packageDir, "packages", "b", "package.json"), JSON.stringify({ "name": "b", "devDependencies": { "a": "workspace:*", "various-requires": "1.0.0", }, }), ); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); const err = await stderr.text(); const out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "5 packages installed", ]); expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ "a", "b", "no-deps", "peer-deps", "various-requires", ]); expect(await file(join(packageDir, "node_modules", "no-deps", "package.json")).json()).toEqual({ name: "no-deps", version: "1.0.0", } as any); expect(await file(join(packageDir, "node_modules", "peer-deps", "package.json")).json()).toEqual({ name: "peer-deps", version: "1.0.0", peerDependencies: { "no-deps": "*", }, } as any); expect(await file(join(packageDir, "node_modules", "various-requires", "package.json")).json()).toEqual({ name: "various-requires", version: "1.0.0", } as any); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test.todo("dragon test 6", async () => { await writeFile( packageJson, JSON.stringify({ "name": "dragon-test-6", "version": "1.0.0", "workspaces": ["packages/*"], }), ); await mkdir(join(packageDir, "packages", "a"), { recursive: true }); await mkdir(join(packageDir, "packages", "b"), { recursive: true }); await mkdir(join(packageDir, "packages", "c"), { recursive: true }); await mkdir(join(packageDir, "packages", "u"), { recursive: true }); await mkdir(join(packageDir, "packages", "v"), { recursive: true }); await mkdir(join(packageDir, "packages", "y"), { recursive: true }); await mkdir(join(packageDir, "packages", "z"), { recursive: true }); await writeFile( join(packageDir, "packages", "a", "package.json"), JSON.stringify({ name: `a`, dependencies: { [`z`]: `workspace:*`, }, }), ); await writeFile( join(packageDir, "packages", "b", "package.json"), JSON.stringify({ name: `b`, dependencies: { [`u`]: `workspace:*`, [`v`]: `workspace:*`, }, }), ); await writeFile( join(packageDir, "packages", "c", "package.json"), JSON.stringify({ name: `c`, dependencies: { [`u`]: `workspace:*`, [`v`]: `workspace:*`, [`y`]: `workspace:*`, [`z`]: `workspace:*`, }, }), ); await writeFile( join(packageDir, "packages", "u", "package.json"), JSON.stringify({ name: `u`, }), ); await writeFile( join(packageDir, "packages", "v", "package.json"), JSON.stringify({ name: `v`, peerDependencies: { [`u`]: `*`, }, }), ); await writeFile( join(packageDir, "packages", "y", "package.json"), JSON.stringify({ name: `y`, peerDependencies: { [`v`]: `*`, }, }), ); await writeFile( join(packageDir, "packages", "z", "package.json"), JSON.stringify({ name: `z`, dependencies: { [`y`]: `workspace:*`, }, peerDependencies: { [`v`]: `*`, }, }), ); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); const err = await stderr.text(); const out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ a@workspace:packages/a", "+ b@workspace:packages/b", "+ c@workspace:packages/c", "+ u@workspace:packages/u", "+ v@workspace:packages/v", "+ y@workspace:packages/y", "+ z@workspace:packages/z", "", "7 packages installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test.todo("dragon test 7", async () => { await writeFile( packageJson, JSON.stringify({ "name": "dragon-test-7", "version": "1.0.0", "dependencies": { "dragon-test-7-a": "1.0.0", "dragon-test-7-d": "1.0.0", "dragon-test-7-b": "2.0.0", "dragon-test-7-c": "3.0.0", }, }), ); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ dragon-test-7-a@1.0.0", "+ dragon-test-7-b@2.0.0", "+ dragon-test-7-c@3.0.0", "+ dragon-test-7-d@1.0.0", "", "7 packages installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); await writeFile( join(packageDir, "test.js"), `console.log(require("dragon-test-7-a"), require("dragon-test-7-d"));`, ); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "test.js"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); err = await stderr.text(); out = await stdout.text(); expect(err).toBeEmpty(); expect(out).toBe("1.0.0 1.0.0\n"); expect( await exists( join( packageDir, "node_modules", "dragon-test-7-a", "node_modules", "dragon-test-7-b", "node_modules", "dragon-test-7-c", ), ), ).toBeTrue(); expect( await exists( join(packageDir, "node_modules", "dragon-test-7-d", "node_modules", "dragon-test-7-b", "node_modules"), ), ).toBeFalse(); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test("dragon test 8", async () => { await writeFile( packageJson, JSON.stringify({ "name": "dragon-test-8", version: "1.0.0", dependencies: { "dragon-test-8-a": "1.0.0", "dragon-test-8-b": "1.0.0", "dragon-test-8-c": "1.0.0", "dragon-test-8-d": "1.0.0", }, }), ); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); const err = await stderr.text(); const out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ dragon-test-8-a@1.0.0", "+ dragon-test-8-b@1.0.0", "+ dragon-test-8-c@1.0.0", "+ dragon-test-8-d@1.0.0", "", "4 packages installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test("dragon test 9", async () => { await writeFile( packageJson, JSON.stringify({ name: "dragon-test-9", version: "1.0.0", dependencies: { [`first`]: `npm:peer-deps@1.0.0`, [`second`]: `npm:peer-deps@1.0.0`, [`no-deps`]: `1.0.0`, }, }), ); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("not found"); expect(err).not.toContain("error:"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ first@1.0.0", expect.stringContaining("+ no-deps@1.0.0"), "+ second@1.0.0", "", "2 packages installed", ]); expect(await file(join(packageDir, "node_modules", "first", "package.json")).json()).toEqual( await file(join(packageDir, "node_modules", "second", "package.json")).json(), ); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test.todo("dragon test 10", async () => { await writeFile( packageJson, JSON.stringify({ name: "dragon-test-10", version: "1.0.0", workspaces: ["packages/*"], }), ); await mkdir(join(packageDir, "packages", "a"), { recursive: true }); await mkdir(join(packageDir, "packages", "b"), { recursive: true }); await mkdir(join(packageDir, "packages", "c"), { recursive: true }); await writeFile( join(packageDir, "packages", "a", "package.json"), JSON.stringify({ name: "a", devDependencies: { b: "workspace:*", }, }), ); await writeFile( join(packageDir, "packages", "b", "package.json"), JSON.stringify({ name: "b", peerDependencies: { c: "*", }, devDependencies: { c: "workspace:*", }, }), ); await writeFile( join(packageDir, "packages", "c", "package.json"), JSON.stringify({ name: "c", peerDependencies: { "no-deps": "*", }, depedencies: { b: "workspace:*", }, }), ); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "--dev"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); const out = await stdout.text(); const err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); expect(err).not.toContain("not found"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ a@workspace:packages/a", "+ b@workspace:packages/b", "+ c@workspace:packages/c", "", " packages installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test("dragon test 12", async () => { await writeFile( packageJson, JSON.stringify({ name: "dragon-test-12", version: "1.0.0", workspaces: ["pkg-a", "pkg-b"], }), ); await mkdir(join(packageDir, "pkg-a"), { recursive: true }); await mkdir(join(packageDir, "pkg-b"), { recursive: true }); await writeFile( join(packageDir, "pkg-a", "package.json"), JSON.stringify({ name: "pkg-a", dependencies: { "pkg-b": "workspace:*", }, }), ); await writeFile( join(packageDir, "pkg-b", "package.json"), JSON.stringify({ name: "pkg-b", dependencies: { "peer-deps": "1.0.0", "fake-peer-deps": "npm:peer-deps@1.0.0", }, peerDependencies: { "no-deps": "1.0.0", }, }), ); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); const out = await stdout.text(); const err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); expect(err).not.toContain("not found"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "4 packages installed", ]); expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([ "fake-peer-deps", "no-deps", "peer-deps", "pkg-a", "pkg-b", ]); expect(await file(join(packageDir, "node_modules", "fake-peer-deps", "package.json")).json()).toEqual({ name: "peer-deps", version: "1.0.0", peerDependencies: { "no-deps": "*", }, } as any); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test("it should not warn when the peer dependency resolution is compatible", async () => { await writeFile( packageJson, JSON.stringify({ name: "compatible-peer-deps", version: "1.0.0", dependencies: { "peer-deps-fixed": "1.0.0", "no-deps": "1.0.0", }, }), ); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "--dev"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); const out = await stdout.text(); const err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); expect(err).not.toContain("not found"); expect(err).not.toContain("incorrect peer dependency"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", expect.stringContaining("+ no-deps@1.0.0"), "+ peer-deps-fixed@1.0.0", "", "2 packages installed", ]); expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["no-deps", "peer-deps-fixed"]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test("it should warn when the peer dependency resolution is incompatible", async () => { await writeFile( packageJson, JSON.stringify({ name: "incompatible-peer-deps", version: "1.0.0", dependencies: { "peer-deps-fixed": "1.0.0", "no-deps": "2.0.0", }, }), ); const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "--dev"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); const out = await stdout.text(); const err = await stderr.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); expect(err).not.toContain("not found"); expect(err).toContain("incorrect peer dependency"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ no-deps@2.0.0", "+ peer-deps-fixed@1.0.0", "", "2 packages installed", ]); expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual(["no-deps", "peer-deps-fixed"]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test("it should install in such a way that two identical packages with different peer dependencies are different instances", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "provides-peer-deps-1-0-0": "1.0.0", "provides-peer-deps-2-0-0": "1.0.0", }, }), ); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); expect(err).not.toContain("not found"); expect(err).not.toContain("incorrect peer dependency"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ provides-peer-deps-1-0-0@1.0.0", "+ provides-peer-deps-2-0-0@1.0.0", "", "5 packages installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); await writeFile( join(packageDir, "test.js"), `console.log( require("provides-peer-deps-1-0-0").dependencies["peer-deps"] === require("provides-peer-deps-2-0-0").dependencies["peer-deps"] ); console.log( Bun.deepEquals(require("provides-peer-deps-1-0-0"), { name: "provides-peer-deps-1-0-0", version: "1.0.0", dependencies: { "peer-deps": { name: "peer-deps", version: "1.0.0", peerDependencies: { "no-deps": { name: "no-deps", version: "1.0.0", }, }, }, "no-deps": { name: "no-deps", version: "1.0.0", }, }, }) ); console.log( Bun.deepEquals(require("provides-peer-deps-2-0-0"), { name: "provides-peer-deps-2-0-0", version: "1.0.0", dependencies: { "peer-deps": { name: "peer-deps", version: "1.0.0", peerDependencies: { "no-deps": { name: "no-deps", version: "2.0.0", }, }, }, "no-deps": { name: "no-deps", version: "2.0.0", }, }, }) );`, ); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "test.js"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); err = await stderr.text(); out = await stdout.text(); expect(out).toBe("true\ntrue\nfalse\n"); expect(err).toBeEmpty(); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test("it should install in such a way that two identical packages with the same peer dependencies are the same instances (simple)", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "provides-peer-deps-1-0-0": "1.0.0", "provides-peer-deps-1-0-0-too": "1.0.0", }, }), ); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); expect(err).not.toContain("not found"); expect(err).not.toContain("incorrect peer dependency"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ provides-peer-deps-1-0-0@1.0.0", "+ provides-peer-deps-1-0-0-too@1.0.0", "", "4 packages installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); await writeFile( join(packageDir, "test.js"), `console.log( require("provides-peer-deps-1-0-0").dependencies["peer-deps"] === require("provides-peer-deps-1-0-0-too").dependencies["peer-deps"] );`, ); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "test.js"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); err = await stderr.text(); out = await stdout.text(); expect(out).toBe("true\n"); expect(err).toBeEmpty(); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test("it should install in such a way that two identical packages with the same peer dependencies are the same instances (complex)", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "forward-peer-deps": "1.0.0", "forward-peer-deps-too": "1.0.0", "no-deps": "1.0.0", }, }), ); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); expect(err).not.toContain("not found"); expect(err).not.toContain("incorrect peer dependency"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ forward-peer-deps@1.0.0", "+ forward-peer-deps-too@1.0.0", expect.stringContaining("+ no-deps@1.0.0"), "", "4 packages installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); await writeFile( join(packageDir, "test.js"), `console.log( require("forward-peer-deps").dependencies["peer-deps"] === require("forward-peer-deps-too").dependencies["peer-deps"] );`, ); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "test.js"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); err = await stderr.text(); out = await stdout.text(); expect(out).toBe("true\n"); expect(err).toBeEmpty(); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test("it shouldn't deduplicate two packages with similar peer dependencies but different names", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "peer-deps": "1.0.0", "peer-deps-too": "1.0.0", "no-deps": "1.0.0", }, }), ); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); expect(err).not.toContain("not found"); expect(err).not.toContain("incorrect peer dependency"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", expect.stringContaining("+ no-deps@1.0.0"), "+ peer-deps@1.0.0", "+ peer-deps-too@1.0.0", "", "3 packages installed", ]); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); await writeFile(join(packageDir, "test.js"), `console.log(require('peer-deps') === require('peer-deps-too'));`); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "test.js"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); err = await stderr.text(); out = await stdout.text(); expect(out).toBe("false\n"); expect(err).toBeEmpty(); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); test("it should reinstall and rebuild dependencies deleted by the user on the next install", async () => { await writeFile( packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "no-deps-scripted": "1.0.0", "one-dep-scripted": "1.5.0", }, trustedDependencies: ["no-deps-scripted", "one-dep-scripted"], }), ); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "--dev"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); expect(err).not.toContain("not found"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ no-deps-scripted@1.0.0", "+ one-dep-scripted@1.5.0", "", "4 packages installed", ]); expect(await exists(join(packageDir, "node_modules/one-dep-scripted/success.txt"))).toBeTrue(); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); await rm(join(packageDir, "node_modules/one-dep-scripted"), { recursive: true, force: true }); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "--dev"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, })); err = await stderr.text(); out = await stdout.text(); expect(err).not.toContain("Saved lockfile"); expect(err).not.toContain("error:"); expect(err).not.toContain("not found"); expect(await exists(join(packageDir, "node_modules/one-dep-scripted/success.txt"))).toBeTrue(); expect(await exited).toBe(0); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); }); }); test("tarball `./` prefix, duplicate directory with file, and empty directory", async () => { await write( packageJson, JSON.stringify({ name: "foo", dependencies: { "tarball-without-package-prefix": "1.0.0", }, }), ); // Entries in this tarball: // // ./ // ./package1000.js // ./package2/ // ./package3/ // ./package4/ // ./package.json // ./package/ // ./package1000/ // ./package/index.js // ./package4/package5/ // ./package4/package.json // ./package3/package6/ // ./package3/package6/index.js // ./package2/index.js // package3/ // package3/package6/ // package3/package6/index.js // // The directory `package3` is added twice, but because one doesn't start // with `./`, it is stripped from the path and a copy of `package6` is placed // at the root of the output directory. Also `package1000` is not included in // the output because it is an empty directory. await runBunInstall(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); const prefix = join(packageDir, "node_modules", "tarball-without-package-prefix"); const results = await Promise.all([ file(join(prefix, "package.json")).json(), file(join(prefix, "package1000.js")).text(), file(join(prefix, "package", "index.js")).text(), file(join(prefix, "package2", "index.js")).text(), file(join(prefix, "package3", "package6", "index.js")).text(), file(join(prefix, "package4", "package.json")).json(), exists(join(prefix, "package4", "package5")), exists(join(prefix, "package1000")), file(join(prefix, "package6", "index.js")).text(), ]); expect(results).toEqual([ { name: "tarball-without-package-prefix", version: "1.0.0", }, "hi", "ooops", "ooooops", "oooooops", { "name": "tarball-without-package-prefix", "version": "2.0.0", }, false, false, "oooooops", ]); expect(await file(join(packageDir, "node_modules", "tarball-without-package-prefix", "package.json")).json()).toEqual( { name: "tarball-without-package-prefix", version: "1.0.0", }, ); }); describe("outdated", () => { const edgeCaseTests = [ { description: "normal dep, smaller than column title", packageJson: { dependencies: { "no-deps": "1.0.0", }, }, }, { description: "normal dep, larger than column title", packageJson: { dependencies: { "prereleases-1": "1.0.0-future.1", }, }, }, { description: "dev dep, smaller than column title", packageJson: { devDependencies: { "no-deps": "1.0.0", }, }, }, { description: "dev dep, larger than column title", packageJson: { devDependencies: { "prereleases-1": "1.0.0-future.1", }, }, }, { description: "peer dep, smaller than column title", packageJson: { peerDependencies: { "no-deps": "1.0.0", }, }, }, { description: "peer dep, larger than column title", packageJson: { peerDependencies: { "prereleases-1": "1.0.0-future.1", }, }, }, { description: "optional dep, smaller than column title", packageJson: { optionalDependencies: { "no-deps": "1.0.0", }, }, }, { description: "optional dep, larger than column title", packageJson: { optionalDependencies: { "prereleases-1": "1.0.0-future.1", }, }, }, ]; for (const { description, packageJson } of edgeCaseTests) { test(description, async () => { await write(join(packageDir, "package.json"), JSON.stringify(packageJson)); await runBunInstall(env, packageDir); assertManifestsPopulated(join(packageDir, ".bun-cache"), registryUrl()); const testEnv = { ...env, FORCE_COLOR: "1" }; const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "outdated"], cwd: packageDir, stdout: "pipe", stderr: "pipe", env: testEnv, }); expect(await exited).toBe(0); const err = await stderr.text(); expect(err).not.toContain("error:"); expect(err).not.toContain("panic:"); const out = await stdout.text(); const first = out.slice(0, out.indexOf("\n")); expect(first).toEqual(expect.stringContaining("bun outdated ")); expect(first).toEqual(expect.stringContaining("v1.")); const rest = out.slice(out.indexOf("\n") + 1); expect(rest).toMatchSnapshot(); }); } test("in workspace", async () => { await Promise.all([ write( packageJson, JSON.stringify({ name: "foo", workspaces: ["pkg1"], dependencies: { "no-deps": "1.0.0", }, }), ), write( join(packageDir, "pkg1", "package.json"), JSON.stringify({ name: "pkg1", dependencies: { "a-dep": "1.0.1", }, }), ), ]); await runBunInstall(env, packageDir); let { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "outdated"], cwd: join(packageDir, "pkg1"), stdout: "pipe", stderr: "pipe", env, }); const err = await stderr.text(); expect(err).not.toContain("error:"); expect(err).not.toContain("panic:"); let out = await stdout.text(); expect(out).toContain("a-dep"); expect(out).not.toContain("no-deps"); expect(await exited).toBe(0); ({ stdout, stderr, exited } = spawn({ cmd: [bunExe(), "outdated"], cwd: packageDir, stdout: "pipe", stderr: "pipe", env, })); const err2 = await stderr.text(); expect(err2).not.toContain("error:"); expect(err2).not.toContain("panic:"); let out2 = await stdout.text(); expect(out2).toContain("no-deps"); expect(out2).not.toContain("a-dep"); expect(await exited).toBe(0); }); test("NO_COLOR works", async () => { await write( packageJson, JSON.stringify({ name: "foo", dependencies: { "a-dep": "1.0.1", }, }), ); await runBunInstall(env, packageDir); const testEnv = { ...env, NO_COLOR: "1" }; const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "outdated"], cwd: packageDir, stdout: "pipe", stderr: "pipe", env: testEnv, }); const err = await stderr.text(); expect(err).not.toContain("error:"); expect(err).not.toContain("panic:"); const out = await stdout.text(); expect(out).toContain("a-dep"); const first = out.slice(0, out.indexOf("\n")); expect(first).toEqual(expect.stringContaining("bun outdated ")); expect(first).toEqual(expect.stringContaining("v1.")); const rest = out.slice(out.indexOf("\n") + 1); expect(rest).toMatchSnapshot(); expect(await exited).toBe(0); }); async function setupWorkspace() { await Promise.all([ write( packageJson, JSON.stringify({ name: "foo", workspaces: ["packages/*"], dependencies: { "no-deps": "1.0.0", }, }), ), write( join(packageDir, "packages", "pkg1", "package.json"), JSON.stringify({ name: "pkg1", dependencies: { "a-dep": "1.0.1", }, }), ), write( join(packageDir, "packages", "pkg2", "package.json"), JSON.stringify({ name: "pkg2222222222222", dependencies: { "prereleases-1": "1.0.0-future.1", }, }), ), ]); } async function runBunOutdated(env: any, cwd: string, ...args: string[]): Promise { const { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "outdated", ...args], cwd, stdout: "pipe", stderr: "pipe", env, }); const err = await stderr.text(); expect(err).not.toContain("error:"); expect(err).not.toContain("panic:"); const out = await stdout.text(); const exitCode = await exited; expect(exitCode).toBe(0); return out; } test("--filter with workspace names and paths", async () => { await setupWorkspace(); await runBunInstall(env, packageDir); let out = await runBunOutdated(env, packageDir, "--filter", "*"); expect(out).toContain("foo"); expect(out).toContain("pkg1"); expect(out).toContain("pkg2222222222222"); out = await runBunOutdated(env, join(packageDir, "packages", "pkg1"), "--filter", "./"); expect(out).toContain("pkg1"); expect(out).not.toContain("foo"); expect(out).not.toContain("pkg2222222222222"); // in directory that isn't a workspace out = await runBunOutdated(env, join(packageDir, "packages"), "--filter", "./*", "--filter", "!pkg1"); expect(out).toContain("pkg2222222222222"); expect(out).not.toContain("pkg1"); expect(out).not.toContain("foo"); out = await runBunOutdated(env, join(packageDir, "packages", "pkg1"), "--filter", "../*"); expect(out).not.toContain("foo"); expect(out).toContain("pkg2222222222222"); expect(out).toContain("pkg1"); }); test("dependency pattern args", async () => { await setupWorkspace(); await runBunInstall(env, packageDir); let out = await runBunOutdated(env, packageDir, "no-deps", "--filter", "*"); expect(out).toContain("no-deps"); expect(out).not.toContain("a-dep"); expect(out).not.toContain("prerelease-1"); out = await runBunOutdated(env, packageDir, "a-dep"); expect(out).not.toContain("a-dep"); expect(out).not.toContain("no-deps"); expect(out).not.toContain("prerelease-1"); out = await runBunOutdated(env, packageDir, "*", "--filter", "*"); expect(out).toContain("no-deps"); expect(out).toContain("a-dep"); expect(out).toContain("prereleases-1"); }); test("scoped workspace names", async () => { await Promise.all([ write( packageJson, JSON.stringify({ name: "@foo/bar", workspaces: ["packages/*"], dependencies: { "no-deps": "1.0.0", }, }), ), write( join(packageDir, "packages", "pkg1", "package.json"), JSON.stringify({ name: "@scope/pkg1", dependencies: { "a-dep": "1.0.1", }, }), ), ]); await runBunInstall(env, packageDir); let out = await runBunOutdated(env, packageDir, "--filter", "*"); expect(out).toContain("@foo/bar"); expect(out).toContain("@scope/pkg1"); out = await runBunOutdated(env, packageDir, "--filter", "*", "--filter", "!@foo/*"); expect(out).not.toContain("@foo/bar"); expect(out).toContain("@scope/pkg1"); }); test("catalog dependencies", async () => { await Promise.all([ write( packageJson, JSON.stringify({ name: "catalog-outdated-test", workspaces: { packages: ["packages/*"], catalog: { "no-deps": "1.0.0", }, catalogs: { dev: { "a-dep": "1.0.1", }, }, }, }), ), write( join(packageDir, "packages", "pkg1", "package.json"), JSON.stringify({ name: "pkg1", dependencies: { "no-deps": "catalog:", }, devDependencies: { "a-dep": "catalog:dev", }, }), ), ]); await runBunInstall(env, packageDir); const out = await runBunOutdated(env, packageDir, "--filter", "*"); expect(out).toContain("no-deps"); expect(out).toContain("a-dep"); }); test("--recursive flag for outdated", async () => { // First verify the flag appears in help const { stdout: helpOut, stderr: helpErr, exited: helpExited, } = spawn({ cmd: [bunExe(), "outdated", "--help"], cwd: packageDir, stdout: "pipe", stderr: "pipe", env, }); const help = (await new Response(helpOut).text()) + (await new Response(helpErr).text()); expect(await helpExited).toBe(0); expect(help).toContain("--recursive"); expect(help).toContain("-r"); // Setup workspace await setupWorkspace(); await runBunInstall(env, packageDir); // Test --recursive shows all workspaces const out = await runBunOutdated(env, packageDir, "--recursive"); expect(out).toContain("no-deps"); expect(out).toContain("a-dep"); expect(out).toContain("prereleases-1"); }); test("catalog grouping with multiple workspaces", async () => { await Promise.all([ write( packageJson, JSON.stringify({ name: "root", workspaces: ["packages/*"], catalog: { "no-deps": "1.0.0", }, }), ), write( join(packageDir, "packages", "workspace-a", "package.json"), JSON.stringify({ name: "workspace-a", dependencies: { "no-deps": "catalog:", }, }), ), write( join(packageDir, "packages", "workspace-b", "package.json"), JSON.stringify({ name: "workspace-b", dependencies: { "no-deps": "catalog:", }, }), ), ]); await runBunInstall(env, packageDir); // Test with filter to show workspace column and grouping const out = await runBunOutdated(env, packageDir, "--filter", "*"); // Should show all workspaces with catalog entries expect(out).toContain("workspace-a"); expect(out).toContain("workspace-b"); expect(out).toContain("no-deps"); // The catalog grouping should show which workspaces use it expect(out).toMatch(/catalog.*workspace-a.*workspace-b|workspace-b.*workspace-a/); }); }); // TODO: setup registry to run across multiple test files, then move this and a few other describe // scopes (update, hoisting, ...) to other files // // test/cli/install/registry/bun-install-windowsshim.test.ts: // // This test is to verify that BinLinkingShim.zig creates correct shim files as // well as bun_shim_impl.exe works in various edge cases. There are many fast // paths for many many cases. describe("windows bin linking shim should work", async () => { if (!isWindows) return; const packageDir = tmpdirSync(); await writeFile( join(packageDir, "bunfig.toml"), ` [install] cache = false registry = "http://localhost:${port}/" `, ); await writeFile( join(packageDir, "package.json"), JSON.stringify({ name: "foo", version: "1.0.0", dependencies: { "bunx-bins": "*", }, }), ); console.log(packageDir); var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "install", "--dev"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env, }); var err = await stderr.text(); var out = await stdout.text(); console.log(err); expect(err).toContain("Saved lockfile"); expect(err).not.toContain("error:"); expect(err).not.toContain("panic:"); expect(err).not.toContain("not found"); expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([ expect.stringContaining("bun install v1."), "", "+ bunx-bins@1.0.0", "", "1 package installed", ]); expect(await exited).toBe(0); const temp_bin_dir = join(packageDir, "temp"); mkdirSync(temp_bin_dir); for (let i = 1; i <= 7; i++) { const target = join(temp_bin_dir, "a".repeat(i) + ".exe"); copyFileSync(bunExe(), target); } copyFileSync(join(packageDir, "node_modules\\bunx-bins\\native.exe"), join(temp_bin_dir, "native.exe")); const PATH = process.env.PATH + ";" + temp_bin_dir; const bins = [ { bin: "bin1", name: "bin1" }, { bin: "bin2", name: "bin2" }, { bin: "bin3", name: "bin3" }, { bin: "bin4", name: "bin4" }, { bin: "bin5", name: "bin5" }, { bin: "bin6", name: "bin6" }, { bin: "bin7", name: "bin7" }, { bin: "bin-node", name: "bin-node" }, { bin: "bin-bun", name: "bin-bun" }, { bin: "native", name: "exe" }, { bin: "uses-native", name: `exe ${packageDir}\\node_modules\\bunx-bins\\uses-native.ts` }, ]; for (const { bin, name } of bins) { test(`bun run ${bin} arg1 arg2`, async () => { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "run", bin, "arg1", "arg2"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env: mergeWindowEnvs([env, { PATH: PATH }]), }); expect(stderr).toBeDefined(); const err = await stderr.text(); expect(err.trim()).toBe(""); const out = await stdout.text(); expect(out.trim()).toBe(`i am ${name} arg1 arg2`); expect(await exited).toBe(0); }); } for (const { bin, name } of bins) { test(`bun --bun run ${bin} arg1 arg2`, async () => { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "--bun", "run", bin, "arg1", "arg2"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env: mergeWindowEnvs([env, { PATH: PATH }]), }); expect(stderr).toBeDefined(); const err = await stderr.text(); expect(err.trim()).toBe(""); const out = await stdout.text(); expect(out.trim()).toBe(`i am ${name} arg1 arg2`); expect(await exited).toBe(0); }); } for (const { bin, name } of bins) { test(`bun --bun x ${bin} arg1 arg2`, async () => { var { stdout, stderr, exited } = spawn({ cmd: [bunExe(), "--bun", "x", bin, "arg1", "arg2"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env: mergeWindowEnvs([env, { PATH: PATH }]), }); expect(stderr).toBeDefined(); const err = await stderr.text(); expect(err.trim()).toBe(""); const out = await stdout.text(); expect(out.trim()).toBe(`i am ${name} arg1 arg2`); expect(await exited).toBe(0); }); } for (const { bin, name } of bins) { test(`${bin} arg1 arg2`, async () => { var { stdout, stderr, exited } = spawn({ cmd: [join(packageDir, "node_modules", ".bin", bin + ".exe"), "arg1", "arg2"], cwd: packageDir, stdout: "pipe", stdin: "pipe", stderr: "pipe", env: mergeWindowEnvs([env, { PATH: PATH }]), }); expect(stderr).toBeDefined(); const err = await stderr.text(); expect(err.trim()).toBe(""); const out = await stdout.text(); expect(out.trim()).toBe(`i am ${name} arg1 arg2`); expect(await exited).toBe(0); }); } });