Files
bun.sh/test/cli/install/bun-install-registry.test.ts
pfg d2201eb1fe Rewrite test/describe, add test.concurrent (#22534)
# bun test

Fixes #8768, Fixes #14624, Fixes #20100, Fixes #19875, Fixes #14135,
Fixes #20980, Fixes #21830, Fixes #5738, Fixes #19758, Fixes #12782,
Fixes #5585, Fixes #9548, Might fix 5996

# New features:

## Concurrent tests

Concurrent tests allow running multiple async tests at the same time.

```ts
// concurrent.test.ts
test.concurrent("this takes a while 1", async () => {
  await Bun.sleep(1000);
});
test.concurrent("this takes a while 2", async () => {
  await Bun.sleep(1000);
});
test.concurrent("this takes a while 3", async () => {
  await Bun.sleep(1000);
});
```

Without `.concurrent`, this test file takes 3 seconds to run because
each one has to wait for the one before it to finish before it can
start.

With `.concurrent`, this file takes 1 second because all three sleeps
can run at once.

```
$> bun-after test concurrent
concurrent.test.js:
✓ this takes a while 1 [1005.36ms]
✓ this takes a while 2 [1012.51ms]
✓ this takes a while 3 [1013.15ms]

 3 pass
 0 fail
Ran 3 tests across 1 file. [1081.00ms]
```

To run all tests as concurrent, pass the `--concurrent` flag when
running tests.

Limitations:

- concurrent tests cannot attribute `expect()` call counts to the test,
meaning `expect.assertions()` does not function
- concurrent tests cannot use `toMatchSnapshot`. `toMatchInlineSnapshot`
is still supported.
- `beforeAll`/`afterAll` will never be executed concurrently.
`beforeEach`/`afterEach` will.

## Chaining

Chaining multiple describe/test qualifiers is now allowed. Previously,
it would fail.

```ts
// chaining-test-qualifiers.test.ts
test.failing.each([1, 2, 3])("each %i", async i => {
  throw new Error(i);
});
```

```
$> bun-after test chaining-test-qualifiers
a.test.js:
✓ each 1
✓ each 2
✓ each 3
```

# Breaking changes:

## Describe ordering

Previously, describe callbacks were called immediately. Now, they are
deferred until the outer callback has finished running. The previous
order matched Jest. The new order is similar to Vitest, but does not
match exactly.

```ts
// describe-ordering.test.ts
describe("outer", () => {
  console.log("outer before");
  describe("inner", () => {
    console.log("inner");
  });
  console.log("outer after");
});
```

Before, this would print

```
$> bun-before test describe-ordering
outer before
inner
outer after
```

Now, this will print

```
$> bun-after test describe-ordering
outer before
outer after
inner
```

## Test ordering

Describes are no longer always called before tests. They are now in
order.

```ts
// test-ordering.test.ts
test("one", () => {});
describe("scope", () => {
  test("two", () => {});
});
test("three", () => {});
```

Before, this would print

```
$> bun-before test test-ordering
✓ scope > two
✓ one
✓ three
```

Now, this will print

```
$> bun-after test test-ordering
✓ one
✓ scope > two
✓ three
```

## Preload hooks

Previously, beforeAll in a preload ran before the first file and
afterAll ran after the last file. Now, beforeAll will run at the start
of each file and afterAll will run at the end of each file. This
behaviour matches Jest and Vitest.

```ts
// preload.ts
beforeAll(() => console.log("preload: beforeAll"));
afterAll(() => console.log("preload: afterAll"));
```

```ts
// preload-ordering-1.test.ts
test("demonstration file 1", () => {});
```

```ts
// preload-ordering-2.test.ts
test("demonstration file 2", () => {});
```

```
$> bun-before test --preload=./preload preload-ordering
preload-ordering-1.test.ts:
preload: beforeAll
✓ demonstration file 1

preload-ordering-2.test.ts:
✓ demonstration file 2
preload: afterAll
```

```
$> bun-after test --preload=./preload preload-ordering
preload-ordering-1.test.ts:
preload: beforeAll
✓ demonstration file 1
preload: afterAll

preload-ordering-2.test.ts:
preload: beforeAll
✓ demonstration file 2
preload: afterAll
```

## Describe failures

Current behaviour is that when an error is thrown inside a describe
callback, none of the tests declared there will run. Now, describes
declared inside will also not run. The new behaviour matches the
behaviour of Jest and Vitest.

```ts
// describe-failures.test.ts
describe("erroring describe", () => {
  test("this test does not run because its describe failed", () => {
    expect(true).toBe(true);
  });
  describe("inner describe", () => {
    console.log("does the inner describe callback get called?");
    test("does the inner test run?", () => {
      expect(true).toBe(true);
    });
  });
  throw new Error("uh oh!");
});
```

Before, the inner describe callback would be called and the inner test
would run, although the outer test would not:

```
$> bun-before test describe-failures
describe-failures.test.ts:
does the inner describe callback get called?

# Unhandled error between tests
-------------------------------
11 |   throw new Error("uh oh!");
             ^
error: uh oh!
-------------------------------

✓ erroring describe > inner describe > does the inner test run?

 1 pass
 0 fail
 1 error
 1 expect() calls
Ran 1 test across 1 file.
Exited with code [1]
```

Now, the inner describe callback is not called at all.

```
$> bun-after test describe-failures
describe-failures.test.ts:

# Unhandled error between tests
-------------------------------
11 |   throw new Error("uh oh!");
             ^
error: uh oh!
-------------------------------


 0 pass
 0 fail
 1 error
Ran 0 tests across 1 file.
Exited with code [1]
```

## Hook failures

Previously, a beforeAll failure would skip subsequent beforeAll()s, the
test, and the afterAll. Now, a beforeAll failure skips any subsequent
beforeAll()s and the test, but not the afterAll.

```js
beforeAll(() => {
  throw new Error("before all: uh oh!");
});
test("my test", () => {
  console.log("my test");
});
afterAll(() => console.log("after all"));
```

```
$> bun-before test hook-failures
Error: before all: uh oh!

$> bun-after test hook-failures
Error: before all: uh oh!
after all
```

Previously, an async beforeEach failure would still allow the test to
run. Now, an async beforeEach failure will prevent the test from running

```js
beforeEach(() => {
  await 0;
  throw "uh oh!";
});
it("the test", async () => {
  console.log("does the test run?");
});
```

```
$> bun-before test async-beforeeach-failure
does the test run?
error: uh oh!
uh oh!
✗ the test

$> bun-after test async-beforeeach-failure
error: uh oh!
uh oh!
✗ the test
```

## Hook timeouts

Hooks will now time out, and can have their timeout configured in an
options parameter

```js
beforeAll(async () => {
  await Bun.sleep(1000);
}, 500);
test("my test", () => {
  console.log("ran my test");
});
```

```
$> bun-before test hook-timeouts
ran my test
Ran 1 test across 1 file. [1011.00ms]

$> bun-after test hook-timeouts
✗ my test [501.15ms]
  ^ a beforeEach/afterEach hook timed out for this test.
```

## Hook execution order

beforeAll will now execute before the tests in the scope, rather than
immediately when it is called.

```ts
describe("d1", () => {
  beforeAll(() => {
    console.log("<d1>");
  });
  test("test", () => {
    console.log("  test");
  });
  afterAll(() => {
    console.log("</d1>");
  });
});
describe("d2", () => {
  beforeAll(() => {
    console.log("<d2>");
  });
  test("test", () => {
    console.log("  test");
  });
  afterAll(() => {
    console.log("</d2>");
  });
});
```

```
$> bun-before test ./beforeall-ordering.test.ts
<d1>
<d2>
  test
</d1>
  test
</d2>

$> bun-after test ./beforeall-ordering.test.ts
<d1>
  test
</d1>
<d2>
  test
</d2>
```

## test inside test

test() inside test() now errors rather than silently failing. Support
for this may be added in the future.

```ts
test("outer", () => {
    console.log("outer");
    test("inner", () => {
        console.log("inner");
    });
});
```

```
$> bun-before test
outer
✓ outer [0.06ms]

 1 pass
 0 fail
Ran 1 test across 1 file. [8.00ms]

$> bun-after test
outer
1 | test("outer", () => {
2 |     console.log("outer");
3 |     test("inner", () => {
        ^
error: Cannot call test() inside a test. Call it inside describe() instead.
✗ outer [0.71ms]

 0 pass
 1 fail
```

## afterAll inside test

afterAll inside a test is no longer allowed

```ts
test("test 1", () => {
  afterAll(() => console.log("afterAll"));
  console.log("test 1");
});
test("test 2", () => {
  console.log("test 2");
});
```

```
$> bun-before
test 1
✓ test 1 [0.05ms]
test 2
✓ test 2
afterAll

$> bun-after
error: Cannot call afterAll() inside a test. Call it inside describe() instead.
✗ test 1 [1.00ms]
test 2
✓ test 2 [0.20ms]
```

# Only inside only

Previously, an outer 'describe.only' would run all tests inside it even
if there was an inner 'test.only'. Now, only the innermost only tests
are executed.

```ts
describe.only("outer", () => {
    test("one", () => console.log("should not run"));
    test.only("two", () => console.log("should run"));
});
```

```
$> bun-before test
should not run
should run

$> bun-after test
should run
```

With no inner only, the outer only will still run all tests:

```ts
describe.only("outer", () => {
    test("test 1", () => console.log("test 1 runs"));
    test("test 2", () => console.log("test 2 runs"));
});
```

# Potential follow-up work

- [ ] for concurrent tests, display headers before console.log messages
saying which test it is for
  - this will need async context or similar
- refActiveExecutionEntry should also be able to know the current test
even in test.concurrent
- [ ] `test("rerun me", () => { console.log("run one time!"); });`
`--rerun-each=3` <- this runs the first and third time but not the
second time. fix.
- [ ] should to cache the JSValue created from
DoneCallback.callAsFunction
- [ ] implement retry and rerun params for tests.
- [ ] Remove finalizer on ScopeFunctions.zig by storing the data in 3
jsvalues passed in bind rather than using a custom class. We should also
migrate off of the ClassGenerator for ScopeFunctions
- [ ] support concurrent limit, how many concurrent tests are allowed to
run at a time. ie `--concurrent-limit=25`
- [ ] flag to run tests in random order
- [ ] `test.failing` should have its own style in the same way
`test.todo` passing marks as 'todo' insetead of 'passing'. right now
it's `✓` which is confusing.
- [ ] remove all instances of bun.jsc.Jest.Jest.current
  - [ ] test options should be in BunTestRoot
- [ ] we will need one global still, stored in the globalobject/vm/?.
but it should not be a Jest instance.
- [ ] consider allowing test() inside test(), as well as afterEach and
afterAll. could even allow describe() too. to do this we would switch
from indices to pointers and they would be in a linked list. they would
be allocated in memorypools for perf/locality. some special
consideration is needed for making sure repeated tests lose their
temporary items. this could also improve memory usage soomewhat.
- [ ] consider using a jsc Bound Function rather than CallbackWithArgs.
bound functions allow adding arguments and they are only one value for
GC instead of many. and this removes our unnecessary three copies.
- [ ] eliminate Strong.Safe. we should be using a C++ class instead.
- [ ] consider modifying the junit reporter to print the whole describe
tree at the end instead of trying to output as test results come in. and
move it into its own file.
- [ ] expect_call_count/expect_assertions is confusing. rename to
`expect_calls`, `assert_expect_calls`. or something.
- [ ] Should make line_no be an enum with a none option and a function
to get if line nombers are enabled
- [ ] looks like we don't need to use file_id anymore (remove
`bun.jsc.Jest.Jest.runner.?.getOrPutFile(file_path).file_id;`, store the
file path directly)
- [ ] 'dot' test reporter like vitest?
- [ ] `test.failing.if(false)` errors because it can't replace mode
'failing' with mode 'skip'. this should probably be allowed instead.
- [ ] trigger timeout termination exception for `while(true) {}`
- [ ] clean up unused callbacks. as soon as we advance to the next
execution group, we can fully clean out the previous one. sometimes
within an execution sequence we can do the same.
  - clean by swapping held values with undefined
- [ ] structure cache for performance for donecallback/scopefunctions
- [ ] consider migrating CallbackWithArgs to be a bound function. the
length of the bound function can exclude the specified args.
- [ ] setting both result and maybe_skip is not ideal, maybe there
should be a function to do both at once?
- [ ] try using a linked list rather than arraylist for describe/test
children, see how it affects performance
- [ ] consider a memory pool for describescope/executionentry. test if
it improves performance.
- [ ] consider making RefDataValue methods return the reason for failure
rather than ?value. that way we can improve error messages. the reason
could be a string or it could be a defined error set
- [ ] instead of 'description orelse (unnamed)', let's have description
default to 'unnamed' and not free it if it === the global that defines
that
- [ ] Add a phase before ordering results that inherits properties to
the parents. (eg inherit only from the child and inherit has_callback
from the child. and has_callback can be on describe/test individually
rather than on base). then we won't have that happening in an init()
function (terrible!)
- [ ] this test was incidentally passing because resolves.pass() wasn't
waiting for promise
  ```
  test("fetching with Request object - issue #1527", async () => {
    const server = createServer((req, res) => {
      res.end();
    }).listen(0);
    try {
      await once(server, "listening");

      const body = JSON.stringify({ foo: "bar" });
const request = new Request(`http://localhost:${server.address().port}`,
{
        method: "POST",
        body,
      });

      expect(fetch(request)).resolves.pass();
    } finally {
      server.closeAllConnections();
    }
  });
  ```
- [ ] the error "expect.assertions() is not supported in the describe
phase, in concurrent tests, between tests, or after test execution has
completed" is not very good. we should be able to identify which of
those it is and print the right error for the context
- [ ] consider: instead of storing weak pointers to BunTest, we can
instead give the instance an id and check that it is correct when
getting the current bun test instance from the ref
- [ ] auto_killer: add three layers of auto_killer:
  - preload (includes file & test)
  - file (includes test)
  - test
- that way at the end of the test, we kill the test processes. at the
end of the file, we kill the file processes. at the end of all, we kill
anything remaining.

AsyncLocalStorage

- store active_id & refdatavalue. active_id is a replacement for the
above weak pointers thing. refdatavalue is for determining which test it
is. this probably fits in 2×u64
- use for auto_killer so timeouts can kill even in concurrent tests

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2025-09-20 00:35:42 -07:00

8721 lines
254 KiB
TypeScript

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<string, string> = {};
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({ saveTextLockfile: false }));
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<string> {
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<Response> {
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"],
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", "-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", "-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",
...(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,
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,
"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<string> {
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);
});
}
});