Files
bun.sh/test/cli/install/bun-install-lifecycle-scripts.test.ts
robobun 7dcd49f832 fix(install): only apply default trusted dependencies to npm packages (#25163)
## Summary
- The default trusted dependencies list should only apply to packages
installed from npm
- Non-npm sources (file:, link:, git:, github:) now require explicit
trustedDependencies
- This prevents malicious packages from spoofing trusted names through
local paths or git repos

## Test plan
- [x] Added test: file: dependency named "esbuild" does NOT auto-run
postinstall scripts
- [x] Added test: file: dependency runs scripts when explicitly added to
trustedDependencies
- [x] Verified tests fail with system bun (old behavior) and pass with
new build
- [x] Build compiles successfully

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
2025-12-11 17:44:41 -08:00

3091 lines
105 KiB
TypeScript

import { file, spawn, write } from "bun";
import { afterAll, beforeAll, beforeEach, describe, expect, setDefaultTimeout, test } from "bun:test";
import { exists, mkdir, rm, writeFile } from "fs/promises";
import {
VerdaccioRegistry,
assertManifestsPopulated,
bunExe,
bunEnv as env,
isLinux,
isWindows,
readdirSorted,
runBunInstall,
stderrForInstall,
} from "harness";
import { join, sep } from "path";
var verdaccio = new VerdaccioRegistry();
var packageDir: string;
var packageJson: string;
beforeAll(async () => {
setDefaultTimeout(1000 * 60 * 5);
await verdaccio.start();
});
afterAll(() => {
verdaccio.stop();
});
function splitErrLines(err: string): string[] {
return err.split(/\r?\n/).filter(s => !s.startsWith("WARNING: ASAN interferes"));
}
beforeEach(async () => {
({ packageDir, packageJson } = await verdaccio.createTestDir({ bunfigOpts: { linker: "hoisted" } }));
env.BUN_INSTALL_CACHE_DIR = join(packageDir, ".bun-cache");
env.BUN_TMPDIR = env.TMPDIR = env.TEMP = join(packageDir, ".bun-tmp");
});
// waiter thread is only a thing on Linux.
for (const forceWaiterThread of isLinux ? [false, true] : [false]) {
describe("lifecycle scripts" + (forceWaiterThread ? " (waiter thread)" : ""), async () => {
test("root package with all lifecycle scripts", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
const writeScript = async (name: string) => {
const contents = `
import { writeFileSync, existsSync, rmSync } from "fs";
import { join } from "path";
const file = join(import.meta.dir, "${name}.txt");
if (existsSync(file)) {
rmSync(file);
writeFileSync(file, "${name} exists!");
} else {
writeFileSync(file, "${name}!");
}
`;
await writeFile(join(packageDir, `${name}.js`), contents);
};
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
scripts: {
preinstall: `${bunExe()} preinstall.js`,
install: `${bunExe()} install.js`,
postinstall: `${bunExe()} postinstall.js`,
preprepare: `${bunExe()} preprepare.js`,
prepare: `${bunExe()} prepare.js`,
postprepare: `${bunExe()} postprepare.js`,
},
}),
);
await writeScript("preinstall");
await writeScript("install");
await writeScript("postinstall");
await writeScript("preprepare");
await writeScript("prepare");
await writeScript("postprepare");
var { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
var err = await stderr.text();
var out = await stdout.text();
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await exists(join(packageDir, "preinstall.txt"))).toBeTrue();
expect(await exists(join(packageDir, "install.txt"))).toBeTrue();
expect(await exists(join(packageDir, "postinstall.txt"))).toBeTrue();
expect(await exists(join(packageDir, "preprepare.txt"))).toBeTrue();
expect(await exists(join(packageDir, "prepare.txt"))).toBeTrue();
expect(await exists(join(packageDir, "postprepare.txt"))).toBeTrue();
expect(await file(join(packageDir, "preinstall.txt")).text()).toBe("preinstall!");
expect(await file(join(packageDir, "install.txt")).text()).toBe("install!");
expect(await file(join(packageDir, "postinstall.txt")).text()).toBe("postinstall!");
expect(await file(join(packageDir, "preprepare.txt")).text()).toBe("preprepare!");
expect(await file(join(packageDir, "prepare.txt")).text()).toBe("prepare!");
expect(await file(join(packageDir, "postprepare.txt")).text()).toBe("postprepare!");
// add a dependency with all lifecycle scripts
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
scripts: {
preinstall: `${bunExe()} preinstall.js`,
install: `${bunExe()} install.js`,
postinstall: `${bunExe()} postinstall.js`,
preprepare: `${bunExe()} preprepare.js`,
prepare: `${bunExe()} prepare.js`,
postprepare: `${bunExe()} postprepare.js`,
},
dependencies: {
"all-lifecycle-scripts": "1.0.0",
},
trustedDependencies: ["all-lifecycle-scripts"],
}),
);
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
}));
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."),
"",
"+ all-lifecycle-scripts@1.0.0",
"",
"1 package installed",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await file(join(packageDir, "preinstall.txt")).text()).toBe("preinstall exists!");
expect(await file(join(packageDir, "install.txt")).text()).toBe("install exists!");
expect(await file(join(packageDir, "postinstall.txt")).text()).toBe("postinstall exists!");
expect(await file(join(packageDir, "preprepare.txt")).text()).toBe("preprepare exists!");
expect(await file(join(packageDir, "prepare.txt")).text()).toBe("prepare exists!");
expect(await file(join(packageDir, "postprepare.txt")).text()).toBe("postprepare exists!");
const depDir = join(packageDir, "node_modules", "all-lifecycle-scripts");
expect(await exists(join(depDir, "preinstall.txt"))).toBeTrue();
expect(await exists(join(depDir, "install.txt"))).toBeTrue();
expect(await exists(join(depDir, "postinstall.txt"))).toBeTrue();
expect(await exists(join(depDir, "preprepare.txt"))).toBeFalse();
expect(await exists(join(depDir, "prepare.txt"))).toBeTrue();
expect(await exists(join(depDir, "postprepare.txt"))).toBeFalse();
expect(await file(join(depDir, "preinstall.txt")).text()).toBe("preinstall!");
expect(await file(join(depDir, "install.txt")).text()).toBe("install!");
expect(await file(join(depDir, "postinstall.txt")).text()).toBe("postinstall!");
expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!");
await rm(join(packageDir, "preinstall.txt"));
await rm(join(packageDir, "install.txt"));
await rm(join(packageDir, "postinstall.txt"));
await rm(join(packageDir, "preprepare.txt"));
await rm(join(packageDir, "prepare.txt"));
await rm(join(packageDir, "postprepare.txt"));
await rm(join(packageDir, "node_modules"), { recursive: true, force: true });
await rm(join(packageDir, "bun.lock"));
// all at once
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
}));
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
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."),
"",
"+ all-lifecycle-scripts@1.0.0",
"",
"1 package installed",
]);
expect(await file(join(packageDir, "preinstall.txt")).text()).toBe("preinstall!");
expect(await file(join(packageDir, "install.txt")).text()).toBe("install!");
expect(await file(join(packageDir, "postinstall.txt")).text()).toBe("postinstall!");
expect(await file(join(packageDir, "preprepare.txt")).text()).toBe("preprepare!");
expect(await file(join(packageDir, "prepare.txt")).text()).toBe("prepare!");
expect(await file(join(packageDir, "postprepare.txt")).text()).toBe("postprepare!");
expect(await file(join(depDir, "preinstall.txt")).text()).toBe("preinstall!");
expect(await file(join(depDir, "install.txt")).text()).toBe("install!");
expect(await file(join(depDir, "postinstall.txt")).text()).toBe("postinstall!");
expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!");
});
test("workspace lifecycle scripts", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
workspaces: ["packages/*"],
scripts: {
preinstall: `touch preinstall.txt`,
install: `touch install.txt`,
postinstall: `touch postinstall.txt`,
preprepare: `touch preprepare.txt`,
prepare: `touch prepare.txt`,
postprepare: `touch postprepare.txt`,
},
}),
);
await mkdir(join(packageDir, "packages", "pkg1"), { recursive: true });
await writeFile(
join(packageDir, "packages", "pkg1", "package.json"),
JSON.stringify({
name: "pkg1",
version: "1.0.0",
scripts: {
preinstall: `touch preinstall.txt`,
install: `touch install.txt`,
postinstall: `touch postinstall.txt`,
preprepare: `touch preprepare.txt`,
prepare: `touch prepare.txt`,
postprepare: `touch postprepare.txt`,
},
}),
);
await mkdir(join(packageDir, "packages", "pkg2"), { recursive: true });
await writeFile(
join(packageDir, "packages", "pkg2", "package.json"),
JSON.stringify({
name: "pkg2",
version: "1.0.0",
scripts: {
preinstall: `touch preinstall.txt`,
install: `touch install.txt`,
postinstall: `touch postinstall.txt`,
preprepare: `touch preprepare.txt`,
prepare: `touch prepare.txt`,
postprepare: `touch postprepare.txt`,
},
}),
);
var { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
var err = await stderr.text();
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
expect(err).toContain("Saved lockfile");
var out = await stdout.text();
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"2 packages installed",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await exists(join(packageDir, "preinstall.txt"))).toBeTrue();
expect(await exists(join(packageDir, "install.txt"))).toBeTrue();
expect(await exists(join(packageDir, "postinstall.txt"))).toBeTrue();
expect(await exists(join(packageDir, "preprepare.txt"))).toBeTrue();
expect(await exists(join(packageDir, "prepare.txt"))).toBeTrue();
expect(await exists(join(packageDir, "postprepare.txt"))).toBeTrue();
expect(await exists(join(packageDir, "packages", "pkg1", "preinstall.txt"))).toBeTrue();
expect(await exists(join(packageDir, "packages", "pkg1", "install.txt"))).toBeTrue();
expect(await exists(join(packageDir, "packages", "pkg1", "postinstall.txt"))).toBeTrue();
expect(await exists(join(packageDir, "packages", "pkg1", "preprepare.txt"))).toBeFalse();
expect(await exists(join(packageDir, "packages", "pkg1", "prepare.txt"))).toBeTrue();
expect(await exists(join(packageDir, "packages", "pkg1", "postprepare.txt"))).toBeFalse();
expect(await exists(join(packageDir, "packages", "pkg2", "preinstall.txt"))).toBeTrue();
expect(await exists(join(packageDir, "packages", "pkg2", "install.txt"))).toBeTrue();
expect(await exists(join(packageDir, "packages", "pkg2", "postinstall.txt"))).toBeTrue();
expect(await exists(join(packageDir, "packages", "pkg2", "preprepare.txt"))).toBeFalse();
expect(await exists(join(packageDir, "packages", "pkg2", "prepare.txt"))).toBeTrue();
expect(await exists(join(packageDir, "packages", "pkg2", "postprepare.txt"))).toBeFalse();
});
test("dependency lifecycle scripts run before root lifecycle scripts", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
const script = '[[ -f "./node_modules/uses-what-bin-slow/what-bin.txt" ]]';
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
"uses-what-bin-slow": "1.0.0",
},
trustedDependencies: ["uses-what-bin-slow"],
scripts: {
install: script,
postinstall: script,
preinstall: script,
prepare: script,
postprepare: script,
preprepare: script,
},
}),
);
// uses-what-bin-slow will wait one second then write a file to disk. The root package should wait for
// for this to happen before running its lifecycle scripts.
var { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
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(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
});
test("install a dependency with lifecycle scripts, then add to trusted dependencies and install again", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
"all-lifecycle-scripts": "1.0.0",
},
trustedDependencies: [],
}),
);
var { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
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."),
"",
"+ all-lifecycle-scripts@1.0.0",
"",
"1 package installed",
"",
"Blocked 3 postinstalls. Run `bun pm untrusted` for details.",
"",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
const depDir = join(packageDir, "node_modules", "all-lifecycle-scripts");
expect(await exists(join(depDir, "preinstall.txt"))).toBeFalse();
expect(await exists(join(depDir, "install.txt"))).toBeFalse();
expect(await exists(join(depDir, "postinstall.txt"))).toBeFalse();
expect(await exists(join(depDir, "preprepare.txt"))).toBeFalse();
expect(await exists(join(depDir, "prepare.txt"))).toBeTrue();
expect(await exists(join(depDir, "postprepare.txt"))).toBeFalse();
expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!");
// add to trusted dependencies
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
"all-lifecycle-scripts": "1.0.0",
},
trustedDependencies: ["all-lifecycle-scripts"],
}),
);
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
}));
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("Checked 1 install across 2 packages (no changes)"),
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await file(join(depDir, "preinstall.txt")).text()).toBe("preinstall!");
expect(await file(join(depDir, "install.txt")).text()).toBe("install!");
expect(await file(join(depDir, "postinstall.txt")).text()).toBe("postinstall!");
expect(await file(join(depDir, "prepare.txt")).text()).toBe("prepare!");
expect(await exists(join(depDir, "preprepare.txt"))).toBeFalse();
expect(await exists(join(depDir, "postprepare.txt"))).toBeFalse();
});
test("adding a package without scripts to trustedDependencies", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
"what-bin": "1.0.0",
},
trustedDependencies: ["what-bin"],
}),
);
var { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
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("+ what-bin@1.0.0"),
"",
"1 package installed",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]);
const what_bin_bins = !isWindows ? ["what-bin"] : ["what-bin.bunx", "what-bin.exe"];
// prettier-ignore
expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins);
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
}));
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."),
"",
"Checked 1 install across 2 packages (no changes)",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
await rm(join(packageDir, "node_modules"), { recursive: true, force: true });
await rm(join(packageDir, "bun.lock"));
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: { "what-bin": "1.0.0" },
}),
);
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
}));
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("+ what-bin@1.0.0"),
"",
"1 package installed",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]);
expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins);
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
}));
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."),
"",
"Checked 1 install across 2 packages (no changes)",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]);
expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins);
// add it to trusted dependencies
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
"what-bin": "1.0.0",
},
trustedDependencies: ["what-bin"],
}),
);
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
}));
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."),
"",
"Checked 1 install across 2 packages (no changes)",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await readdirSorted(join(packageDir, "node_modules"))).toEqual([".bin", "what-bin"]);
expect(await readdirSorted(join(packageDir, "node_modules", ".bin"))).toEqual(what_bin_bins);
});
test("lifecycle scripts run if node_modules is deleted", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
"lifecycle-postinstall": "1.0.0",
},
trustedDependencies: ["lifecycle-postinstall"],
}),
);
var { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
var err = await stderr.text();
var out = await stdout.text();
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"+ lifecycle-postinstall@1.0.0",
"",
// @ts-ignore
"1 package installed",
]);
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
expect(await exists(join(packageDir, "node_modules", "lifecycle-postinstall", "postinstall.txt"))).toBeTrue();
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
await rm(join(packageDir, "node_modules"), { force: true, recursive: true });
await rm(join(packageDir, ".bun-cache"), { recursive: true, force: true });
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
}));
err = await stderr.text();
out = await stdout.text();
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"+ lifecycle-postinstall@1.0.0",
"",
"1 package installed",
]);
expect(err).not.toContain("Saved lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
expect(await exists(join(packageDir, "node_modules", "lifecycle-postinstall", "postinstall.txt"))).toBeTrue();
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
});
test("INIT_CWD is set to the correct directory", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
scripts: {
install: "bun install.js",
},
dependencies: {
"lifecycle-init-cwd": "1.0.0",
"another-init-cwd": "npm:lifecycle-init-cwd@1.0.0",
},
trustedDependencies: ["lifecycle-init-cwd", "another-init-cwd"],
}),
);
await writeFile(
join(packageDir, "install.js"),
`
const fs = require("fs");
const path = require("path");
fs.writeFileSync(
path.join(__dirname, "test.txt"),
process.env.INIT_CWD || "does not exist"
);
`,
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
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."),
"",
"+ another-init-cwd@1.0.0",
"+ lifecycle-init-cwd@1.0.0",
"",
"1 package installed",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await file(join(packageDir, "test.txt")).text()).toBe(packageDir);
expect(await file(join(packageDir, "node_modules/lifecycle-init-cwd/test.txt")).text()).toBe(packageDir);
expect(await file(join(packageDir, "node_modules/another-init-cwd/test.txt")).text()).toBe(packageDir);
});
test("failing lifecycle script should print output", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
"lifecycle-failing-postinstall": "1.0.0",
},
trustedDependencies: ["lifecycle-failing-postinstall"],
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
const err = await stderr.text();
expect(err).toContain("hello");
expect(await exited).toBe(1);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
const out = await stdout.text();
expect(out).toEqual(expect.stringContaining("bun install v1."));
});
test("failing root lifecycle script should print output correctly", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "fooooooooo",
version: "1.0.0",
scripts: {
preinstall: `${bunExe()} -e "throw new Error('Oops!')"`,
},
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
env: testEnv,
});
expect(await exited).toBe(1);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await stdout.text()).toEqual(expect.stringContaining("bun install v1."));
const err = await stderr.text();
expect(err).toContain("error: Oops!");
expect(err).toContain('error: preinstall script from "fooooooooo" exited with 1');
});
test("exit 0 in lifecycle scripts works", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
scripts: {
postinstall: "exit 0",
prepare: "exit 0",
postprepare: "exit 0",
},
}),
);
var { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
const err = await stderr.text();
expect(err).toContain("No packages! Deleted empty 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("done"),
"",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
});
test("--ignore-scripts should skip lifecycle scripts", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
dependencies: {
"lifecycle-failing-postinstall": "1.0.0",
},
trustedDependencies: ["lifecycle-failing-postinstall"],
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install", "--ignore-scripts"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env: testEnv,
});
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("hello");
const out = await stdout.text();
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"+ lifecycle-failing-postinstall@1.0.0",
"",
"1 package installed",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
});
test("it should add `node-gyp rebuild` as the `install` script when `install` and `postinstall` don't exist and `binding.gyp` exists in the root of the package", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
"binding-gyp-scripts": "1.5.0",
},
trustedDependencies: ["binding-gyp-scripts"],
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
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."),
"",
"+ binding-gyp-scripts@1.5.0",
"",
"2 packages installed",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await exists(join(packageDir, "node_modules/binding-gyp-scripts/build.node"))).toBeTrue();
});
test("automatic node-gyp scripts should not run for untrusted dependencies, and should run after adding to `trustedDependencies`", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
const packageJSON: any = {
name: "foo",
version: "1.0.0",
dependencies: {
"binding-gyp-scripts": "1.5.0",
},
};
await writeFile(packageJson, JSON.stringify(packageJSON));
var { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
let 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\]$/m, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"+ binding-gyp-scripts@1.5.0",
"",
"2 packages installed",
"",
"Blocked 1 postinstall. Run `bun pm untrusted` for details.",
"",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await exists(join(packageDir, "node_modules", "binding-gyp-scripts", "build.node"))).toBeFalse();
packageJSON.trustedDependencies = ["binding-gyp-scripts"];
await writeFile(packageJson, JSON.stringify(packageJSON));
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
}));
err = stderrForInstall(await stderr.text());
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
expect(err).not.toContain("warn:");
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await exists(join(packageDir, "node_modules", "binding-gyp-scripts", "build.node"))).toBeTrue();
});
test("automatic node-gyp scripts work in package root", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
"node-gyp": "1.5.0",
},
}),
);
await writeFile(join(packageDir, "binding.gyp"), "");
var { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
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."),
"",
"+ node-gyp@1.5.0",
"",
"1 package installed",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await exists(join(packageDir, "build.node"))).toBeTrue();
await rm(join(packageDir, "build.node"));
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
}));
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await exists(join(packageDir, "build.node"))).toBeTrue();
});
test("auto node-gyp scripts work when scripts exists other than `install` and `preinstall`", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
"node-gyp": "1.5.0",
},
scripts: {
postinstall: "exit 0",
prepare: "exit 0",
postprepare: "exit 0",
},
}),
);
await writeFile(join(packageDir, "binding.gyp"), "");
var { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
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."),
"",
"+ node-gyp@1.5.0",
"",
"1 package installed",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await exists(join(packageDir, "build.node"))).toBeTrue();
});
for (const script of ["install", "preinstall"]) {
test(`does not add auto node-gyp script when ${script} script exists`, async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
const packageJSON: any = {
name: "foo",
version: "1.0.0",
dependencies: {
"node-gyp": "1.5.0",
},
scripts: {
[script]: "exit 0",
},
};
await writeFile(packageJson, JSON.stringify(packageJSON));
await writeFile(join(packageDir, "binding.gyp"), "");
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
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."),
"",
"+ node-gyp@1.5.0",
"",
"1 package installed",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await exists(join(packageDir, "build.node"))).toBeFalse();
});
}
test("git dependencies also run `preprepare`, `prepare`, and `postprepare` scripts", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
"lifecycle-install-test": "dylan-conway/lifecycle-install-test#3ba6af5b64f2d27456e08df21d750072dffd3eee",
},
}),
);
var { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
let 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\]$/m, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"+ lifecycle-install-test@github:dylan-conway/lifecycle-install-test#3ba6af5",
"",
"1 package installed",
"",
"Blocked 6 postinstalls. Run `bun pm untrusted` for details.",
"",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preprepare.txt"))).toBeFalse();
expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "prepare.txt"))).toBeFalse();
expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postprepare.txt"))).toBeFalse();
expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preinstall.txt"))).toBeFalse();
expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "install.txt"))).toBeFalse();
expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postinstall.txt"))).toBeFalse();
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
"lifecycle-install-test": "dylan-conway/lifecycle-install-test#3ba6af5b64f2d27456e08df21d750072dffd3eee",
},
trustedDependencies: ["lifecycle-install-test"],
}),
);
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
}));
err = stderrForInstall(await stderr.text());
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
expect(err).not.toContain("warn:");
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preprepare.txt"))).toBeTrue();
expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "prepare.txt"))).toBeTrue();
expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postprepare.txt"))).toBeTrue();
expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "preinstall.txt"))).toBeTrue();
expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "install.txt"))).toBeTrue();
expect(await exists(join(packageDir, "node_modules", "lifecycle-install-test", "postinstall.txt"))).toBeTrue();
});
test("root lifecycle scripts should wait for dependency lifecycle scripts", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
"uses-what-bin-slow": "1.0.0",
},
trustedDependencies: ["uses-what-bin-slow"],
scripts: {
install: '[[ -f "./node_modules/uses-what-bin-slow/what-bin.txt" ]]',
},
}),
);
// Package `uses-what-bin-slow` has an install script that will sleep for 1 second
// before writing `what-bin.txt` to disk. The root package has an install script that
// checks if this file exists. If the root package install script does not wait for
// the other to finish, it will fail.
var { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
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."),
"",
"+ uses-what-bin-slow@1.0.0",
"",
"2 packages installed",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
});
async function createPackagesWithScripts(
packagesCount: number,
scripts: Record<string, string>,
): Promise<string[]> {
const dependencies: Record<string, string> = {};
const dependenciesList: string[] = [];
async function iterate(i) {
const packageName: string = "stress-test-package-" + i;
const packageVersion = "1.0." + i;
dependencies[packageName] = "file:./" + packageName;
dependenciesList[i] = packageName;
const packagePath = join(packageDir, packageName);
await Bun.write(
join(packagePath, "package.json"),
JSON.stringify({
name: packageName,
version: packageVersion,
scripts,
}),
);
}
await Promise.all(Array.from({ length: packagesCount }, (_, i) => iterate(i)));
await writeFile(
packageJson,
JSON.stringify({
name: "stress-test",
version: "1.0.0",
dependencies,
trustedDependencies: dependenciesList,
}),
);
return dependenciesList;
}
test("reach max concurrent scripts", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
const scripts = {
"preinstall": `${bunExe()} -e 'Bun.sleepSync(500)'`,
};
const dependenciesList = await createPackagesWithScripts(4, scripts);
var { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install", "--concurrent-scripts=2"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
const [err, out, exitCode] = await Promise.all([stderr.text(), stdout.text(), exited]);
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
expect(out).not.toContain("Blocked");
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
...dependenciesList.map(dep => `+ ${dep}@${dep}`),
"",
"4 packages installed",
]);
expect(exitCode).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
});
test("stress test", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
const dependenciesList = await createPackagesWithScripts(500, {
"postinstall": `${bunExe()} --version`,
});
// the script is quick, default number for max concurrent scripts
var { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
const [err, out, exitCode] = await Promise.all([stderr.text(), stdout.text(), exited]);
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
expect(out).not.toContain("Blocked");
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
...dependenciesList.map(dep => `+ ${dep}@${dep}`).sort((a, b) => a.localeCompare(b)),
"",
"500 packages installed",
]);
expect(exitCode).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
});
test("it should install and use correct binary version", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
// this should install `what-bin` in two places:
//
// - node_modules/.bin/what-bin@1.5.0
// - node_modules/uses-what-bin/node_modules/.bin/what-bin@1.0.0
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
"uses-what-bin": "1.0.0",
"what-bin": "1.5.0",
},
}),
);
var { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
var err = await stderr.text();
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
var 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"),
"+ what-bin@1.5.0",
"",
"3 packages installed",
"",
"Blocked 1 postinstall. Run `bun pm untrusted` for details.",
"",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await file(join(packageDir, "node_modules", "what-bin", "what-bin.js")).text()).toContain(
"what-bin@1.5.0",
);
expect(
await file(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin", "what-bin.js")).text(),
).toContain("what-bin@1.0.0");
await rm(join(packageDir, "node_modules"), { recursive: true, force: true });
await rm(join(packageDir, "bun.lock"));
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
"uses-what-bin": "1.5.0",
"what-bin": "1.0.0",
},
scripts: {
install: "what-bin",
},
trustedDependencies: ["uses-what-bin"],
}),
);
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
}));
err = stderrForInstall(await stderr.text());
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
expect(err).not.toContain("warn:");
expect(await exited).toBe(0);
const firstLockfile = await (
await file(join(packageDir, "bun.lock")).text()
).replaceAll(/localhost:\d+/g, "localhost:1234");
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await file(join(packageDir, "node_modules", "what-bin", "what-bin.js")).text()).toContain(
"what-bin@1.0.0",
);
expect(
await file(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin", "what-bin.js")).text(),
).toContain("what-bin@1.5.0");
await rm(join(packageDir, "node_modules"), { recursive: true, force: true });
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
}));
out = await stdout.text();
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("+ uses-what-bin@1.5.0"),
expect.stringContaining("+ what-bin@1.0.0"),
"",
"3 packages installed",
]);
expect(await exited).toBe(0);
const secondLockfile = await (
await file(join(packageDir, "bun.lock")).text()
).replaceAll(/localhost:\d+/g, "localhost:1234");
expect(firstLockfile).toEqual(secondLockfile);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
});
test("node-gyp should always be available for lifecycle scripts", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
scripts: {
install: "node-gyp --version",
},
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
const err = await stderr.text();
expect(err).not.toContain("Saved lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
const out = await stdout.text();
// if node-gyp isn't available, it would return a non-zero exit code
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
});
// if this test fails, `electron` might be removed from the default list
test("default trusted dependencies should work", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.2.3",
dependencies: {
"electron": "1.0.0",
},
}),
);
var { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
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."),
"",
"+ electron@1.0.0",
"",
"1 package installed",
]);
expect(out).not.toContain("Blocked");
expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue();
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
});
test("default trusted dependencies should not be used of trustedDependencies is populated", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.2.3",
dependencies: {
"uses-what-bin": "1.0.0",
// fake electron package because it's in the default trustedDependencies list
"electron": "1.0.0",
},
}),
);
var { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
// electron lifecycle scripts should run, uses-what-bin scripts should not run
var err = await stderr.text();
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
var out = await stdout.text();
expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"+ electron@1.0.0",
expect.stringContaining("+ uses-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"), verdaccio.registryUrl());
expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse();
expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue();
await rm(join(packageDir, "node_modules"), { recursive: true, force: true });
await rm(join(packageDir, ".bun-cache"), { recursive: true, force: true });
await rm(join(packageDir, "bun.lock"));
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.2.3",
dependencies: {
"uses-what-bin": "1.0.0",
"electron": "1.0.0",
},
trustedDependencies: ["uses-what-bin"],
}),
);
// now uses-what-bin scripts should run and electron scripts should not run.
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
}));
err = await stderr.text();
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
out = await stdout.text();
expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"+ electron@1.0.0",
expect.stringContaining("+ uses-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"), verdaccio.registryUrl());
expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue();
expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse();
});
test("does not run any scripts if trustedDependencies is an empty list", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.2.3",
dependencies: {
"uses-what-bin": "1.0.0",
"electron": "1.0.0",
},
trustedDependencies: [],
}),
);
var { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
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\]$/m, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"+ electron@1.0.0",
expect.stringContaining("+ uses-what-bin@1.0.0"),
"",
"3 packages installed",
"",
"Blocked 2 postinstalls. Run `bun pm untrusted` for details.",
"",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse();
expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse();
});
test("default trusted dependencies should only apply to npm packages, not file: dependencies", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
// Create a file: dependency named "esbuild" (which is in the default trusted dependencies list)
// with a postinstall script that would fail if it ran
const esbuildPath = join(packageDir, "local-esbuild");
await mkdir(esbuildPath, { recursive: true });
await writeFile(
join(esbuildPath, "package.json"),
JSON.stringify({
name: "esbuild",
version: "1.0.0",
scripts: {
postinstall: "exit 1",
},
}),
);
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
// file: dependency named "esbuild" - should NOT use default trusted list
esbuild: "file:./local-esbuild",
},
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
const err = await stderr.text();
const out = await stdout.text();
// The install should succeed because the postinstall script should NOT run
// (file: dependencies don't use default trusted list, even if name matches)
// The postinstall is blocked (not trusted), so we expect the "Blocked" message
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."),
"",
"+ esbuild@local-esbuild",
"",
"1 package installed",
"",
"Blocked 1 postinstall. Run `bun pm untrusted` for details.",
"",
]);
expect(await exited).toBe(0);
});
test("file: dependency with default trusted name should run scripts when explicitly added to trustedDependencies", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
// Create a file: dependency named "esbuild" with a postinstall script that creates a marker file
const esbuildPath = join(packageDir, "local-esbuild");
await mkdir(esbuildPath, { recursive: true });
await writeFile(
join(esbuildPath, "package.json"),
JSON.stringify({
name: "esbuild",
version: "1.0.0",
scripts: {
postinstall: `${bunExe()} -e "require('fs').writeFileSync('postinstall-ran.txt', 'ran')"`,
},
}),
);
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
esbuild: "file:./local-esbuild",
},
// Explicitly trust the file: dependency
trustedDependencies: ["esbuild"],
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
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\]$/m, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"+ esbuild@local-esbuild",
"",
"1 package installed",
"",
]);
expect(out).not.toContain("Blocked");
expect(await exited).toBe(0);
// The postinstall script should have run because we explicitly trusted it
expect(await exists(join(packageDir, "node_modules", "esbuild", "postinstall-ran.txt"))).toBeTrue();
});
test("will run default trustedDependencies after install that didn't include them", async () => {
await verdaccio.writeBunfig(packageDir, { saveTextLockfile: false, linker: "hoisted" });
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.2.3",
dependencies: {
electron: "1.0.0",
},
trustedDependencies: ["blah"],
}),
);
// first install does not run electron scripts
var { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
var err = await stderr.text();
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
var out = await stdout.text();
expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"+ electron@1.0.0",
"",
"1 package installed",
"",
"Blocked 1 postinstall. Run `bun pm untrusted` for details.",
"",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse();
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.2.3",
dependencies: {
electron: "1.0.0",
},
}),
);
// The electron scripts should run now because it's in default trusted dependencies.
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
}));
err = await stderr.text();
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
out = await stdout.text();
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"Checked 1 install across 2 packages (no changes)",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue();
});
describe("--trust", async () => {
test("unhoisted untrusted scripts, none at root node_modules", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await Promise.all([
write(
packageJson,
JSON.stringify({
name: "foo",
dependencies: {
// prevents real `uses-what-bin` from hoisting to root
"uses-what-bin": "npm:a-dep@1.0.3",
},
workspaces: ["pkg1"],
}),
),
write(
join(packageDir, "pkg1", "package.json"),
JSON.stringify({
name: "pkg1",
dependencies: {
"uses-what-bin": "1.0.0",
},
}),
),
]);
await runBunInstall(testEnv, packageDir);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
const results = await Promise.all([
exists(join(packageDir, "node_modules", "pkg1", "node_modules", "uses-what-bin")),
exists(join(packageDir, "node_modules", "pkg1", "node_modules", "uses-what-bin", "what-bin.txt")),
]);
expect(results).toEqual([true, false]);
const { stderr, exited } = spawn({
cmd: [bunExe(), "pm", "trust", "--all"],
cwd: packageDir,
stdout: "ignore",
stderr: "pipe",
env: testEnv,
});
const err = await stderr.text();
expect(err).not.toContain("error:");
expect(await exited).toBe(0);
expect(
await exists(join(packageDir, "node_modules", "pkg1", "node_modules", "uses-what-bin", "what-bin.txt")),
).toBeTrue();
});
const trustTests = [
{
label: "only name",
packageJson: {
name: "foo",
},
},
{
label: "empty dependencies",
packageJson: {
name: "foo",
dependencies: {},
},
},
{
label: "populated dependencies",
packageJson: {
name: "foo",
dependencies: {
"uses-what-bin": "1.0.0",
},
},
},
{
label: "empty trustedDependencies",
packageJson: {
name: "foo",
trustedDependencies: [],
},
},
{
label: "populated dependencies, empty trustedDependencies",
packageJson: {
name: "foo",
dependencies: {
"uses-what-bin": "1.0.0",
},
trustedDependencies: [],
},
},
{
label: "populated dependencies and trustedDependencies",
packageJson: {
name: "foo",
dependencies: {
"uses-what-bin": "1.0.0",
},
trustedDependencies: ["uses-what-bin"],
},
},
{
label: "empty dependencies and trustedDependencies",
packageJson: {
name: "foo",
dependencies: {},
trustedDependencies: [],
},
},
];
for (const { label, packageJson } of trustTests) {
test(label, async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(join(packageDir, "package.json"), JSON.stringify(packageJson));
let { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "i", "--trust", "uses-what-bin@1.0.0"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env: testEnv,
});
let err = stderrForInstall(await stderr.text());
expect(err).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.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun add v1."),
"",
"installed uses-what-bin@1.0.0",
"",
"2 packages installed",
]);
expect(await exited).toBe(0);
expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue();
expect(await file(join(packageDir, "package.json")).json()).toEqual({
name: "foo",
dependencies: {
"uses-what-bin": "1.0.0",
},
trustedDependencies: ["uses-what-bin"],
});
// another install should not error with json SyntaxError
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "i"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env: testEnv,
}));
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:");
out = await stdout.text();
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"Checked 2 installs across 3 packages (no changes)",
]);
expect(await exited).toBe(0);
});
}
describe("packages without lifecycle scripts", async () => {
test("initial install", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "i", "--trust", "no-deps@1.0.0"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env: testEnv,
});
const err = stderrForInstall(await stderr.text());
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
expect(err).not.toContain("warn:");
const out = await stdout.text();
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun add v1."),
"",
"installed no-deps@1.0.0",
"",
"1 package installed",
]);
expect(await exited).toBe(0);
expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeTrue();
expect(await file(packageJson).json()).toEqual({
name: "foo",
dependencies: {
"no-deps": "1.0.0",
},
});
});
test("already installed", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
}),
);
let { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "i", "no-deps"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env: testEnv,
});
let err = stderrForInstall(await stderr.text());
expect(err).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.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun add v1."),
"",
"installed no-deps@2.0.0",
"",
"1 package installed",
]);
expect(await exited).toBe(0);
expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeTrue();
expect(await file(packageJson).json()).toEqual({
name: "foo",
dependencies: {
"no-deps": "^2.0.0",
},
});
// oops, I wanted to run the lifecycle scripts for no-deps, I'll install
// again with --trust.
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "i", "--trust", "no-deps"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env: testEnv,
}));
// oh, I didn't realize no-deps doesn't have
// any lifecycle scripts. It shouldn't automatically add to
// trustedDependencies.
err = await stderr.text();
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
out = await stdout.text();
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun add v1."),
"",
"installed no-deps@2.0.0",
"",
expect.stringContaining("done"),
"",
]);
expect(await exited).toBe(0);
expect(await exists(join(packageDir, "node_modules", "no-deps"))).toBeTrue();
expect(await file(packageJson).json()).toEqual({
name: "foo",
dependencies: {
"no-deps": "^2.0.0",
},
});
});
});
});
describe("updating trustedDependencies", async () => {
test("existing trustedDependencies, unchanged trustedDependencies", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
trustedDependencies: ["uses-what-bin"],
dependencies: {
"uses-what-bin": "1.0.0",
},
}),
);
let { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "i"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env: testEnv,
});
let err = stderrForInstall(await stderr.text());
expect(err).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.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
expect.stringContaining("+ uses-what-bin@1.0.0"),
"",
"2 packages installed",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue();
expect(await file(packageJson).json()).toEqual({
name: "foo",
dependencies: {
"uses-what-bin": "1.0.0",
},
trustedDependencies: ["uses-what-bin"],
});
// no changes, lockfile shouldn't be saved
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "i"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env: testEnv,
}));
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:");
out = await stdout.text();
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"Checked 2 installs across 3 packages (no changes)",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
});
test("existing trustedDependencies, removing trustedDependencies", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
trustedDependencies: ["uses-what-bin"],
dependencies: {
"uses-what-bin": "1.0.0",
},
}),
);
let { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "i"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env: testEnv,
});
let err = stderrForInstall(await stderr.text());
expect(err).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.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
expect.stringContaining("+ uses-what-bin@1.0.0"),
"",
"2 packages installed",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue();
expect(await file(packageJson).json()).toEqual({
name: "foo",
dependencies: {
"uses-what-bin": "1.0.0",
},
trustedDependencies: ["uses-what-bin"],
});
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
dependencies: {
"uses-what-bin": "1.0.0",
},
}),
);
// this script should not run because uses-what-bin is no longer in trustedDependencies
await rm(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"), { force: true });
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "i"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env: testEnv,
}));
err = stderrForInstall(await stderr.text());
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
expect(err).not.toContain("warn:");
out = await stdout.text();
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"Checked 2 installs across 3 packages (no changes)",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await file(packageJson).json()).toEqual({
name: "foo",
dependencies: {
"uses-what-bin": "1.0.0",
},
});
expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse();
});
test("non-existent trustedDependencies, then adding it", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
dependencies: {
"electron": "1.0.0",
},
}),
);
let { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "i"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env: testEnv,
});
let err = stderrForInstall(await stderr.text());
expect(err).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.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"+ electron@1.0.0",
"",
"1 package installed",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue();
expect(await file(packageJson).json()).toEqual({
name: "foo",
dependencies: {
"electron": "1.0.0",
},
});
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
trustedDependencies: ["electron"],
dependencies: {
"electron": "1.0.0",
},
}),
);
await rm(join(packageDir, "node_modules", "electron", "preinstall.txt"), { force: true });
// lockfile should save evenn though there are no changes to trustedDependencies due to
// the default list
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "i"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env: testEnv,
}));
err = stderrForInstall(await stderr.text());
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
expect(err).not.toContain("warn:");
out = await stdout.text();
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
"Checked 1 install across 2 packages (no changes)",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue();
});
});
test("node -p should work in postinstall scripts", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
scripts: {
postinstall: `node -p "require('fs').writeFileSync('postinstall.txt', 'postinstall')"`,
},
}),
);
const originalPath = env.PATH;
env.PATH = "";
let { stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
env.PATH = originalPath;
let err = stderrForInstall(await stderr.text());
expect(err).toContain("No packages! Deleted empty lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
expect(err).not.toContain("warn:");
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await exists(join(packageDir, "postinstall.txt"))).toBeTrue();
});
test("ensureTempNodeGypScript works", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
scripts: {
preinstall: "node-gyp --version",
},
}),
);
const originalPath = env.PATH;
env.PATH = "";
let { stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env,
});
env.PATH = originalPath;
let err = stderrForInstall(await stderr.text());
expect(err).toContain("No packages! Deleted empty lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
expect(err).not.toContain("warn:");
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
});
test("bun pm trust and untrusted on missing package", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
dependencies: {
"uses-what-bin": "1.5.0",
},
}),
);
let { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "i"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
env: testEnv,
});
let err = stderrForInstall(await stderr.text());
expect(err).toContain("Saved lockfile");
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.5.0"),
"",
"2 packages installed",
"",
"Blocked 1 postinstall. Run `bun pm untrusted` for details.",
"",
]);
expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeFalse();
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
// remove uses-what-bin from node_modules, bun pm trust and untrusted should handle missing package
await rm(join(packageDir, "node_modules", "uses-what-bin"), { recursive: true, force: true });
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "pm", "untrusted"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
env: testEnv,
}));
err = stderrForInstall(await stderr.text());
expect(err).toContain("bun pm untrusted");
expect(err).not.toContain("error:");
expect(err).not.toContain("warn:");
out = await stdout.text();
expect(out).toContain("Found 0 untrusted dependencies with scripts");
expect(await exited).toBe(0);
({ stderr, exited } = spawn({
cmd: [bunExe(), "pm", "trust", "uses-what-bin"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
env: testEnv,
}));
expect(await exited).toBe(1);
err = await stderr.text();
expect(err).toContain("bun pm trust");
expect(err).toContain("0 scripts ran");
expect(err).toContain("uses-wha");
});
describe("add trusted, delete, then add again", async () => {
// when we change bun install to delete dependencies from node_modules
// for both cases, we need to update this test
for (const withRm of [true, false]) {
test(withRm ? "withRm" : "withoutRm", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await verdaccio.writeBunfig(packageDir, { saveTextLockfile: false, linker: "hoisted" });
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
dependencies: {
"no-deps": "1.0.0",
"uses-what-bin": "1.0.0",
},
}),
);
let { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
env: testEnv,
});
let err = stderrForInstall(await stderr.text());
expect(err).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.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"",
expect.stringContaining("+ no-deps@1.0.0"),
expect.stringContaining("+ uses-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"), verdaccio.registryUrl());
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: testEnv,
}));
err = stderrForInstall(await stderr.text());
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();
expect(await file(packageJson).json()).toEqual({
name: "foo",
dependencies: {
"no-deps": "1.0.0",
"uses-what-bin": "1.0.0",
},
trustedDependencies: ["uses-what-bin"],
});
// now remove and install again
if (withRm) {
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "rm", "uses-what-bin"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
env: testEnv,
}));
err = stderrForInstall(await stderr.text());
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
expect(err).not.toContain("warn:");
out = await stdout.text();
expect(out).toContain("1 package removed");
expect(out).toContain("uses-what-bin");
expect(await exited).toBe(0);
}
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
dependencies: {
"no-deps": "1.0.0",
},
}),
);
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
env: testEnv,
}));
err = stderrForInstall(await stderr.text());
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
expect(err).not.toContain("warn:");
out = await stdout.text();
let expected = withRm
? ["", "Checked 1 install across 2 packages (no changes)"]
: ["", expect.stringContaining("1 package removed")];
expected = [expect.stringContaining("bun install v1."), ...expected];
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual(expected);
expect(await exited).toBe(0);
expect(await exists(join(packageDir, "node_modules", "uses-what-bin"))).toBe(!withRm);
// add again, bun pm untrusted should report it as untrusted
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
dependencies: {
"no-deps": "1.0.0",
"uses-what-bin": "1.0.0",
},
}),
);
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "i"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
env: testEnv,
}));
err = stderrForInstall(await stderr.text());
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
expect(err).not.toContain("warn:");
out = await stdout.text();
expected = withRm
? [
"",
expect.stringContaining("+ uses-what-bin@1.0.0"),
"",
"1 package installed",
"",
"Blocked 1 postinstall. Run `bun pm untrusted` for details.",
"",
]
: ["", expect.stringContaining("Checked 3 installs across 4 packages (no changes)"), ""];
expected = [expect.stringContaining("bun install v1."), ...expected];
expect(out.replace(/\s*\[[0-9\.]+m?s\]$/m, "").split(/\r?\n/)).toEqual(expected);
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "pm", "untrusted"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
env: testEnv,
}));
err = stderrForInstall(await stderr.text());
expect(err).not.toContain("error:");
expect(err).not.toContain("warn:");
out = await stdout.text();
expect(out).toContain("./node_modules/uses-what-bin @1.0.0".replaceAll("/", sep));
expect(await exited).toBe(0);
});
}
});
describe.if(!forceWaiterThread || process.platform === "linux")("does not use 100% cpu", async () => {
test("install", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
scripts: {
preinstall: `${bunExe()} -e 'Bun.sleepSync(1000)'`,
},
}),
);
const proc = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "ignore",
stderr: "ignore",
stdin: "ignore",
env: testEnv,
});
expect(await proc.exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(proc.resourceUsage()?.cpuTime.total).toBeLessThan(750_000);
});
// https://github.com/oven-sh/bun/issues/11252
test.todoIf(isWindows)("bun pm trust", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
const dep = isWindows ? "uses-what-bin-slow-window" : "uses-what-bin-slow";
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
[dep]: "1.0.0",
},
}),
);
var { exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "ignore",
stderr: "ignore",
env: testEnv,
});
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await exists(join(packageDir, "node_modules", dep, "what-bin.txt"))).toBeFalse();
const proc = spawn({
cmd: [bunExe(), "pm", "trust", "--all"],
cwd: packageDir,
stdout: "ignore",
stderr: "ignore",
env: testEnv,
});
expect(await proc.exited).toBe(0);
expect(await exists(join(packageDir, "node_modules", dep, "what-bin.txt"))).toBeTrue();
expect(proc.resourceUsage()?.cpuTime.total).toBeLessThan(750_000 * (isWindows ? 5 : 1));
});
});
});
describe("stdout/stderr is inherited from root scripts during install", async () => {
test("without packages", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
const exe = bunExe().replace(/\\/g, "\\\\");
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.2.3",
scripts: {
"preinstall": `${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`,
"install": `${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`,
"prepare": `${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`,
},
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
env: testEnv,
});
const err = stderrForInstall(await stderr.text());
expect(err).not.toContain("error:");
expect(err).not.toContain("warn:");
expect(splitErrLines(err)).toEqual([
"No packages! Deleted empty lockfile",
"",
`$ ${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`,
"preinstall stderr 🍦",
`$ ${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`,
`$ ${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`,
"",
]);
const out = await stdout.text();
expect(out.split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"install stdout 🚀",
"prepare stdout done ✅",
"",
expect.stringContaining("done"),
"",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
});
test("with a package", async () => {
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
const exe = bunExe().replace(/\\/g, "\\\\");
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.2.3",
scripts: {
"preinstall": `${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`,
"install": `${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`,
"prepare": `${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`,
},
dependencies: {
"no-deps": "1.0.0",
},
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
env: testEnv,
});
const err = stderrForInstall(await stderr.text());
expect(err).not.toContain("error:");
expect(err).not.toContain("warn:");
expect(splitErrLines(err)).toEqual([
"Resolving dependencies",
expect.stringContaining("Resolved, downloaded and extracted "),
"Saved lockfile",
"",
`$ ${exe} -e 'process.stderr.write("preinstall stderr 🍦\\n")'`,
"preinstall stderr 🍦",
`$ ${exe} -e 'process.stdout.write("install stdout 🚀\\n")'`,
`$ ${exe} -e 'Bun.sleepSync(200); process.stdout.write("prepare stdout done ✅\\n")'`,
"",
]);
const out = await stdout.text();
expect(out.replace(/\s*\[[0-9\.]+m?s\]\s*$/, "").split(/\r?\n/)).toEqual([
expect.stringContaining("bun install v1."),
"install stdout 🚀",
"prepare stdout done ✅",
"",
expect.stringContaining("+ no-deps@1.0.0"),
"",
"1 package installed",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
});
});
}
test("ignore-scripts is read from npmrc", async () => {
await Promise.all([
write(
packageJson,
JSON.stringify({
name: "foo",
version: "1.2.3",
dependencies: {
"uses-what-bin": "1.0.0",
},
scripts: {
postinstall: `${bunExe()} -e 'await Bun.write("postinstall.txt", "postinstall!!")'`,
},
trustedDependencies: ["uses-what-bin"],
}),
),
write(join(packageDir, ".npmrc"), "ignore-scripts=true"),
]);
async function checkScripts(): Promise<boolean[]> {
return Promise.all([
exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt")),
exists(join(packageDir, "postinstall.txt")),
]);
}
await runBunInstall(env, packageDir);
expect(await checkScripts()).toEqual([false, false]);
await write(join(packageDir, ".npmrc"), "ignore-scripts=false");
await runBunInstall(env, packageDir, { savesLockfile: false });
expect(await checkScripts()).toEqual([false, true]);
await Promise.all([
rm(join(packageDir, "postinstall.txt")),
rm(join(packageDir, "node_modules"), { recursive: true, force: true }),
]);
expect(await checkScripts()).toEqual([false, false]);
await runBunInstall(env, packageDir, { savesLockfile: false });
expect(await checkScripts()).toEqual([true, true]);
});