Files
bun-src/test/cli/install/bun-install-lifecycle-scripts.test.ts
2026-08-27 21:09:14 +00:00

4220 lines
144 KiB
TypeScript

import { file, spawn, write } from "bun";
import { afterAll, beforeAll, describe, expect, setDefaultTimeout, test } from "bun:test";
import { exists, mkdir, rm, writeFile } from "fs/promises";
import {
VerdaccioRegistry,
assertManifestsPopulated,
bunEnv as baseEnv,
bunExe,
isLinux,
isWindows,
readdirSorted,
runBunInstall,
} from "harness";
import { join, sep } from "path";
var verdaccio = new VerdaccioRegistry();
setDefaultTimeout(1000 * 60 * 5);
beforeAll(async () => {
await verdaccio.start();
});
afterAll(() => {
verdaccio.stop();
});
function splitErrLines(err: string): string[] {
return err.split(/\r?\n/).filter(s => !s.startsWith("WARNING: ASAN interferes"));
}
// Tests in this file each spawn `bun install` which itself spawns multiple lifecycle
// script subprocesses. Running them all concurrently would fork-bomb the machine, so
// gate concurrency with a small semaphore and give each test its own isolated dir/env.
const MAX_CONCURRENT = 12;
let activeSlots = 0;
const slotWaiters: Array<() => void> = [];
function acquireSlot(): Promise<void> {
if (activeSlots < MAX_CONCURRENT) {
activeSlots++;
return Promise.resolve();
}
return new Promise(resolve => slotWaiters.push(resolve));
}
function releaseSlot(): void {
const next = slotWaiters.shift();
if (next) {
next();
} else {
activeSlots--;
}
}
type TestCtx = {
packageDir: string;
packageJson: string;
env: Record<string, string>;
[Symbol.dispose](): void;
};
async function setupTest(): Promise<TestCtx> {
await acquireSlot();
let released = false;
try {
const { packageDir, packageJson } = await verdaccio.createTestDir({ bunfigOpts: { linker: "hoisted" } });
const env: Record<string, string> = {
...baseEnv,
BUN_INSTALL_CACHE_DIR: join(packageDir, ".bun-cache"),
BUN_TMPDIR: join(packageDir, ".bun-tmp"),
TMPDIR: join(packageDir, ".bun-tmp"),
TEMP: join(packageDir, ".bun-tmp"),
};
return {
packageDir,
packageJson,
env,
[Symbol.dispose]() {
if (!released) {
released = true;
releaseSlot();
}
},
};
} catch (e) {
releaseSlot();
released = true;
throw e;
}
}
// The six multi-install tests below are the longest in the file (2-3 serial `bun install`s
// each, some with cold caches). Declare them first so they start before the ~110 shorter
// tests and overlap with them instead of forming a serial tail at the end of the run.
test.concurrent("ignore-scripts is read from npmrc", async () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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]);
});
test.concurrent("trustedDependencies matches the resolved package name, not the dependency alias", async () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
// A dependent controls the aliases of its own dependencies, so an entry like
// `"esbuild": "npm:[email protected]"` must not inherit lifecycle-script
// trust from `trustedDependencies: ["esbuild"]`. Trust is keyed on the
// resolved package name, never the alias.
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
"esbuild": "npm:[email protected]",
},
trustedDependencies: ["esbuild"],
}),
);
let { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env,
});
let err = await stderr.text();
let out = await stdout.text();
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("error:");
expect(out).toContain("Blocked 1 postinstall");
expect(await exists(join(packageDir, "node_modules", "esbuild", "package.json"))).toBeTrue();
expect(await exists(join(packageDir, "node_modules", "esbuild", "what-bin.txt"))).toBeFalse();
expect(await exited).toBe(0);
// Trusting the *resolved* package name still grants trust to the same
// aliased dependency.
await rm(join(packageDir, "node_modules"), { recursive: true, force: true });
await rm(join(packageDir, "bun.lock"), { force: true });
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
"esbuild": "npm:[email protected]",
},
trustedDependencies: ["uses-what-bin"],
}),
);
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env,
}));
err = await stderr.text();
out = await stdout.text();
expect(err).not.toContain("error:");
expect(out).not.toContain("Blocked");
expect(await exists(join(packageDir, "node_modules", "esbuild", "what-bin.txt"))).toBeTrue();
expect(await exited).toBe(0);
});
test.concurrent(
"trustedDependencies added on a later install still matches the resolved package name, not the dependency alias",
async () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
const dependencies = { "esbuild": "npm:[email protected]" };
await writeFile(packageJson, JSON.stringify({ name: "foo", version: "1.0.0", dependencies }));
let { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env,
});
let err = await stderr.text();
let out = await stdout.text();
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("error:");
expect(out).toContain("Blocked 1 postinstall");
expect(await exists(join(packageDir, "node_modules", "esbuild", "package.json"))).toBeTrue();
expect(await exists(join(packageDir, "node_modules", "esbuild", "what-bin.txt"))).toBeFalse();
expect(await exited).toBe(0);
await writeFile(
packageJson,
JSON.stringify({ name: "foo", version: "1.0.0", dependencies, trustedDependencies: ["esbuild"] }),
);
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env,
}));
err = await stderr.text();
out = await stdout.text();
expect(err).not.toContain("error:");
expect(await exists(join(packageDir, "node_modules", "esbuild", "what-bin.txt"))).toBeFalse();
expect(await exited).toBe(0);
},
);
test.concurrent("node-gyp shim directory added to lifecycle script PATH gets a randomized name", async () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
"no-deps": "1.0.0",
},
scripts: {
postinstall: `${bunExe()} -e 'await Bun.write("path.txt", String(process.env.PATH))'`,
},
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env,
});
const [out, err, exitCode] = await Promise.all([stdout.text(), stderr.text(), exited]);
expect(err).not.toContain("error:");
expect(exitCode).toBe(0);
const pathVar = await file(join(packageDir, "path.txt")).text();
const match = pathVar.match(/\.([0-9a-f]{1,16})-[0-9A-F]{1,16}\.node-gyp/);
expect(match).not.toBeNull();
const derived = BigInt("0x" + match![1]) ^ 12345n;
const nowNs = BigInt(Date.now()) * 1_000_000n;
const distance = derived > nowNs ? derived - nowNs : nowNs - derived;
expect(distance > 21_600_000_000_000n).toBe(true);
});
test.concurrent("default trusted dependencies require the canonical registry tarball URL", async () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
// No `trustedDependencies` in package.json: `electron` is on the default
// trusted list, so the genuine registry package's lifecycle scripts run.
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
"electron": "1.0.0",
},
}),
);
let { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env,
});
let err = await stderr.text();
let out = await stdout.text();
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("error:");
expect(out).not.toContain("Blocked");
expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeTrue();
expect(await exited).toBe(0);
// Tamper with the lockfile: keep the default-trusted name `electron` but
// point its tarball URL at a different package on the same registry. The
// install must still succeed, but the package must no longer inherit the
// default lifecycle-script grant because the URL is not the canonical
// registry tarball for `[email protected]`.
const lockfilePath = join(packageDir, "bun.lock");
const lockfile = await file(lockfilePath).text();
expect(lockfile).toContain("/electron/-/electron-1.0.0.tgz");
await writeFile(
lockfilePath,
lockfile
.replace("/electron/-/electron-1.0.0.tgz", "/all-lifecycle-scripts/-/all-lifecycle-scripts-1.0.0.tgz")
.replace(/"sha512-[^"]+"/, '""'),
);
await rm(join(packageDir, "node_modules"), { 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: "ignore",
stderr: "pipe",
env,
}));
err = await stderr.text();
out = await stdout.text();
expect(err).not.toContain("error:");
// The redirected tarball (all-lifecycle-scripts content) installs under the
// recorded name, but its preinstall/install/postinstall must not run: the
// package no longer inherits default trust from the `electron` name because
// the URL is not the canonical registry tarball for `[email protected]`.
expect(await exists(join(packageDir, "node_modules", "electron", "package.json"))).toBeTrue();
expect(await exists(join(packageDir, "node_modules", "electron", "install.js"))).toBeTrue();
expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse();
expect(await exists(join(packageDir, "node_modules", "electron", "install.txt"))).toBeFalse();
expect(await exists(join(packageDir, "node_modules", "electron", "postinstall.txt"))).toBeFalse();
expect(await exited).toBe(0);
// Tamper again: keep the canonical path for `[email protected]` but point the
// URL at a different origin — a second local server that proxies to the
// real registry. The tarball still downloads and the integrity still
// matches, but the origin is not the configured registry, so the default
// lifecycle-script grant must not apply.
using proxy = Bun.serve({
port: 0,
fetch(req) {
const url = new URL(req.url);
return fetch(`http://localhost:${verdaccio.port}${url.pathname}${url.search}`, {
method: req.method,
headers: req.headers,
});
},
});
const canonicalUrl = `http://localhost:${verdaccio.port}/electron/-/electron-1.0.0.tgz`;
expect(lockfile).toContain(canonicalUrl);
await writeFile(
lockfilePath,
lockfile.replace(canonicalUrl, `http://localhost:${proxy.port}/electron/-/electron-1.0.0.tgz`),
);
await rm(join(packageDir, "node_modules"), { 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: "ignore",
stderr: "pipe",
env,
}));
err = await stderr.text();
out = await stdout.text();
expect(err).not.toContain("error:");
expect(await exists(join(packageDir, "node_modules", "electron", "package.json"))).toBeTrue();
expect(await exists(join(packageDir, "node_modules", "electron", "preinstall.txt"))).toBeFalse();
expect(await exists(join(packageDir, "node_modules", "electron", "install.txt"))).toBeFalse();
expect(await exists(join(packageDir, "node_modules", "electron", "postinstall.txt"))).toBeFalse();
expect(await exited).toBe(0);
});
test.concurrent("binary lockfile trusted dependency entries require an exact name match", async () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
// The binary lockfile (bun.lockb) stores trustedDependencies as truncated
// 32-bit name hashes with no name. A hash-only entry must never grant
// lifecycle-script trust to a different name that happens to collide with
// it. These two distinct names share the truncated hash 0x6c4a82d1 under
// `Wyhash11::hash(0, name) as u32` (same pair as "trustedDependencies entry
// must match by name, not truncated hash" above).
const trustedName = "pkg-xjd";
const colliderName = "pkg-ztd";
await verdaccio.writeBunfig(packageDir, { saveTextLockfile: false, linker: "hoisted" });
const colliderPath = join(packageDir, "collider");
await mkdir(colliderPath, { recursive: true });
await writeFile(
join(colliderPath, "package.json"),
JSON.stringify({
name: colliderName,
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: {
[colliderName]: "file:./collider",
},
trustedDependencies: [trustedName],
}),
);
// First install writes a binary bun.lockb whose trustedDependencies entry
// for `pkg-xjd` is persisted as a hash with no name attached.
let { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env,
});
let err = await stderr.text();
let out = await stdout.text();
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("error:");
expect(out).toContain("Blocked 1 postinstall");
expect(await exists(join(packageDir, "bun.lockb"))).toBeTrue();
expect(await exists(join(packageDir, "node_modules", colliderName, "postinstall-ran.txt"))).toBeFalse();
expect(await exited).toBe(0);
// Reinstall from the binary lockfile. The hash-only entry loaded from disk
// must still not grant trust to the colliding name.
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,
}));
err = await stderr.text();
out = await stdout.text();
expect(err).not.toContain("error:");
expect(out).toContain("Blocked 1 postinstall");
expect(await exists(join(packageDir, "node_modules", colliderName, "postinstall-ran.txt"))).toBeFalse();
expect(await exited).toBe(0);
// A trustedDependencies entry that names the real package keeps working
// across the same binary-lockfile round trip.
await rm(join(packageDir, "node_modules"), { recursive: true, force: true });
await rm(join(packageDir, "bun.lockb"), { force: true });
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
[colliderName]: "file:./collider",
},
trustedDependencies: [colliderName],
}),
);
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env,
}));
err = await stderr.text();
out = await stdout.text();
expect(err).not.toContain("error:");
expect(out).not.toContain("Blocked");
expect(await exists(join(packageDir, "node_modules", colliderName, "postinstall-ran.txt"))).toBeTrue();
expect(await exited).toBe(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,
}));
err = await stderr.text();
out = await stdout.text();
expect(err).not.toContain("error:");
expect(out).not.toContain("Blocked");
expect(await exists(join(packageDir, "node_modules", colliderName, "postinstall-ran.txt"))).toBeTrue();
expect(await exited).toBe(0);
});
test.concurrent(
"lifecycle script trust for file: dependencies is keyed on the dependency alias, not the package's self-declared name",
async () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
// The user vets and trusts the names they wrote in `dependencies` /
// `trustedDependencies`. A folder/tarball/git dependency installed under a
// different alias must not inherit lifecycle-script trust just because the
// package.json inside the dependency declares the trusted name for itself.
const payloadDir = join(packageDir, "payload");
await mkdir(payloadDir, { recursive: true });
await writeFile(
join(payloadDir, "package.json"),
JSON.stringify({
name: "my-native-addon",
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: {
"unrelated-alias": "file:./payload",
},
trustedDependencies: ["my-native-addon"],
}),
);
let { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env,
});
let err = await stderr.text();
let out = await stdout.text();
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("error:");
expect(out).toContain("Blocked 1 postinstall");
expect(await exists(join(packageDir, "node_modules", "unrelated-alias", "package.json"))).toBeTrue();
expect(await exists(join(packageDir, "node_modules", "unrelated-alias", "postinstall-ran.txt"))).toBeFalse();
expect(await exists(join(packageDir, "node_modules", "my-native-addon", "postinstall-ran.txt"))).toBeFalse();
expect(await exited).toBe(0);
// The same folder dependency declared under the alias the user actually
// listed in trustedDependencies still runs its lifecycle scripts.
await rm(join(packageDir, "node_modules"), { recursive: true, force: true });
await rm(join(packageDir, "bun.lock"), { force: true });
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
"my-native-addon": "file:./payload",
},
trustedDependencies: ["my-native-addon"],
}),
);
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env,
}));
err = await stderr.text();
out = await stdout.text();
expect(err).not.toContain("error:");
expect(out).not.toContain("Blocked");
expect(await exists(join(packageDir, "node_modules", "my-native-addon", "postinstall-ran.txt"))).toBeTrue();
expect(await exited).toBe(0);
},
);
test.concurrent(
"trustedDependencies entries for non-npm dependencies only apply to dependencies declared by the root or a workspace",
async () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
// A transitive dependency picks the aliases of its own dependencies. An alias
// that happens to match an entry in the root's `trustedDependencies` must not
// grant lifecycle-script trust to a tarball/git/folder package the root never
// declared itself.
const tarballUrl = `http://localhost:${verdaccio.port}/electron/-/electron-1.0.0.tgz`;
const middleDir = join(packageDir, "middle");
await mkdir(middleDir, { recursive: true });
await writeFile(
join(middleDir, "package.json"),
JSON.stringify({
name: "middle",
version: "1.0.0",
dependencies: {
"trusted-native-addon": tarballUrl,
},
}),
);
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
"middle": "file:./middle",
},
trustedDependencies: ["trusted-native-addon"],
}),
);
let { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env,
});
let err = await stderr.text();
let out = await stdout.text();
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("error:");
// The remote tarball was introduced by `middle`, not by the root, so its
// preinstall must stay blocked even though the alias matches an entry in the
// root's trustedDependencies.
expect(out).toContain("Blocked 1 postinstall");
expect(await exited).toBe(0);
expect(await exists(join(packageDir, "node_modules", "trusted-native-addon", "preinstall.txt"))).toBeFalse();
expect(
await exists(
join(packageDir, "node_modules", "middle", "node_modules", "trusted-native-addon", "preinstall.txt"),
),
).toBeFalse();
// The same tarball declared by the root itself under the trusted alias still
// runs its lifecycle scripts.
await rm(join(packageDir, "node_modules"), { recursive: true, force: true });
await rm(join(packageDir, "bun.lock"), { force: true });
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
"trusted-native-addon": tarballUrl,
},
trustedDependencies: ["trusted-native-addon"],
}),
);
({ stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env,
}));
err = await stderr.text();
out = await stdout.text();
expect(err).not.toContain("error:");
expect(out).not.toContain("Blocked");
expect(await exited).toBe(0);
expect(await exists(join(packageDir, "node_modules", "trusted-native-addon", "preinstall.txt"))).toBeTrue();
},
);
// waiter thread is only a thing on Linux.
for (const forceWaiterThread of isLinux ? [false, true] : [false]) {
describe.concurrent("lifecycle scripts" + (forceWaiterThread ? " (waiter thread)" : ""), async () => {
test("root package with all lifecycle scripts", async () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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."),
"",
"+ [email protected]",
"",
"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."),
"",
"+ [email protected]",
"",
"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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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."),
"",
"+ [email protected]",
"",
"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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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("+ [email protected]"),
"",
"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("+ [email protected]"),
"",
"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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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."),
"",
"+ [email protected]",
"",
// @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."),
"",
"+ [email protected]",
"",
"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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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:[email protected]",
},
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."),
"",
"+ [email protected]",
"+ [email protected]",
"",
"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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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."),
"",
"+ [email protected]",
"",
"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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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."),
"",
"+ [email protected]",
"",
"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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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."),
"",
"+ [email protected]",
"",
"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 = 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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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."),
"",
"+ [email protected]",
"",
"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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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."),
"",
"+ [email protected]",
"",
"1 package installed",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
expect(await exists(join(packageDir, "build.node"))).toBeTrue();
});
test("auto node-gyp script does not drop the package's own postinstall", async () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
// Local mock node-gyp so the injected `node-gyp rebuild` succeeds without
// touching the registry.
await mkdir(join(packageDir, "node-gyp-pkg"), { recursive: true });
await writeFile(
join(packageDir, "node-gyp-pkg", "package.json"),
JSON.stringify({
name: "node-gyp",
version: "1.0.0",
bin: { "node-gyp": "./node-gyp.js" },
}),
);
await writeFile(
join(packageDir, "node-gyp-pkg", "node-gyp.js"),
`#!/usr/bin/env node\nrequire("fs").writeFileSync("build.node", "built");\n`,
);
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
"node-gyp": "file:./node-gyp-pkg",
},
scripts: {
postinstall: `${bunExe()} -e "require('fs').writeFileSync('postinstall.txt', 'ran')"`,
prepare: `${bunExe()} -e "require('fs').writeFileSync('prepare.txt', 'ran')"`,
postprepare: `${bunExe()} -e "require('fs').writeFileSync('postprepare.txt', 'ran')"`,
},
}),
);
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();
const out = await stdout.text();
expect(err).not.toContain("not found");
expect(err).not.toContain("error:");
expect(out).toContain("1 package installed");
expect({
// injected `node-gyp rebuild`
"build.node": await exists(join(packageDir, "build.node")),
// the package's own hooks must still run
"postinstall": await exists(join(packageDir, "postinstall.txt")),
"prepare": await exists(join(packageDir, "prepare.txt")),
"postprepare": await exists(join(packageDir, "postprepare.txt")),
}).toEqual({
"build.node": true,
"postinstall": true,
"prepare": true,
"postprepare": true,
});
expect(await exited).toBe(0);
});
for (const script of ["install", "preinstall"]) {
test(`does not add auto node-gyp script when ${script} script exists`, async () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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."),
"",
"+ [email protected]",
"",
"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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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 = 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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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."),
"",
"+ [email protected]",
"",
"2 packages installed",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
});
async function createPackagesWithScripts(
packageDir: string,
packageJson: string,
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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
const scripts = {
"preinstall": `${bunExe()} -e 'Bun.sleepSync(500)'`,
};
const dependenciesList = await createPackagesWithScripts(packageDir, packageJson, 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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
const dependenciesList = await createPackagesWithScripts(packageDir, packageJson, 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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
// this should install `what-bin` in two places:
//
// - node_modules/.bin/[email protected]
// - node_modules/uses-what-bin/node_modules/.bin/[email protected]
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("+ [email protected]"),
"+ [email protected]",
"",
"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(
"[email protected]",
);
expect(
await file(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin", "what-bin.js")).text(),
).toContain("[email protected]");
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 = 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(
"[email protected]",
);
expect(
await file(join(packageDir, "node_modules", "uses-what-bin", "node_modules", "what-bin", "what-bin.js")).text(),
).toContain("[email protected]");
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("+ [email protected]"),
expect.stringContaining("+ [email protected]"),
"",
"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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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."),
"",
"+ [email protected]",
"",
"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("bun pm ls --trusted uses default trusted list when trustedDependencies is not set", async () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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",
"no-deps": "1.0.0",
},
}),
);
{
const { 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("error:");
expect(err).toContain("Saved lockfile");
expect(await exited).toBe(0);
}
// Without `trustedDependencies` in package.json, `--trusted` falls back to
// the default trusted list. `electron` is on it, `no-deps` is not.
{
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "pm", "ls", "--trusted"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
const out = await stdout.text();
expect(await stderr.text()).toBe("");
expect(out).toBe(`${packageDir} node_modules (2)
└── [email protected]
`);
expect(await exited).toBe(0);
}
// With `trustedDependencies` set, the default list is ignored.
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.2.3",
dependencies: {
"electron": "1.0.0",
"no-deps": "1.0.0",
},
trustedDependencies: ["no-deps"],
}),
);
{
const { 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("error:");
expect(await exited).toBe(0);
}
{
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "pm", "ls", "--trusted"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
const out = await stdout.text();
expect(await stderr.text()).toBe("");
expect(out).toBe(`${packageDir} node_modules (2)
└── [email protected]
`);
expect(await exited).toBe(0);
}
});
test("default trusted dependencies should not be used of trustedDependencies is populated", async () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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."),
"",
"+ [email protected]",
expect.stringContaining("+ [email protected]"),
"",
"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."),
"",
"+ [email protected]",
expect.stringContaining("+ [email protected]"),
"",
"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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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."),
"",
"+ [email protected]",
expect.stringContaining("+ [email protected]"),
"",
"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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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("trustedDependencies entry must match by name, not truncated hash", async () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
// Trusted-dependency lookups are keyed by `Wyhash11::hash(0, name) as u32`
// (the legacy 32-byte-round wyhash used by `String.Builder.string_hash`,
// NOT `Bun.hash.wyhash`). These two distinct names share the truncated
// 32-bit hash 0x6c4a82d1; they were found by an offline birthday search
// over `pkg-<base36>` candidates against that exact hash function.
const trustedName = "pkg-xjd";
const colliderName = "pkg-ztd";
const colliderPath = join(packageDir, "collider");
await mkdir(colliderPath, { recursive: true });
await writeFile(
join(colliderPath, "package.json"),
JSON.stringify({
name: colliderName,
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: {
[colliderName]: "file:./collider",
},
// Trusts a *different* name whose truncated hash collides with the
// collider's. The collider's postinstall must NOT run.
trustedDependencies: [trustedName],
}),
);
let { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "install"],
cwd: packageDir,
stdout: "pipe",
stdin: "ignore",
stderr: "pipe",
env: testEnv,
});
let err = await stderr.text();
let out = await stdout.text();
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("error:");
expect(out).toContain("Blocked 1 postinstall");
expect(await exited).toBe(0);
expect(await exists(join(packageDir, "node_modules", colliderName))).toBeTrue();
expect(await exists(join(packageDir, "node_modules", colliderName, "postinstall-ran.txt"))).toBeFalse();
// Trusting the collider by its exact name still grants trust to the same
// package. Start from a clean slate so this is a plain install with a
// matching trustedDependencies entry.
await rm(join(packageDir, "node_modules"), { recursive: true, force: true });
await rm(join(packageDir, "bun.lock"), { force: true });
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
version: "1.0.0",
dependencies: {
[colliderName]: "file:./collider",
},
trustedDependencies: [colliderName],
}),
);
({ 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("error:");
expect(out).not.toContain("Blocked");
expect(await exited).toBe(0);
expect(await exists(join(packageDir, "node_modules", colliderName, "postinstall-ran.txt"))).toBeTrue();
});
test("will run default trustedDependencies after install that didn't include them", async () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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",
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."),
"",
"+ [email protected]",
"",
"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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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:[email protected]",
},
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();
});
test("must not trust a truncated-hash collision in the trusted subtree", async () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
// `--trust` collects the trusted subtree into a set keyed by
// `Wyhash11::hash(0, name) as u32` (the legacy 32-byte-round wyhash
// used by `String.Builder.string_hash`). "pkg-jodsufb" was found by an
// offline targeted search over `pkg-<base36>` candidates so that its
// truncated hash equals hash32("uses-what-bin") == 0x9f4d06e5.
const colliderName = "pkg-jodsufb";
const colliderPath = join(packageDir, "collider2");
await mkdir(colliderPath, { recursive: true });
await writeFile(
join(colliderPath, "package.json"),
JSON.stringify({
name: colliderName,
version: "1.0.0",
scripts: {
postinstall: `${bunExe()} -e "require('fs').writeFileSync('postinstall-ran.txt', 'ran')"`,
},
}),
);
await writeFile(
packageJson,
JSON.stringify({
name: "foo",
dependencies: {
[colliderName]: "file:./collider2",
},
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "i", "--trust", "[email protected]"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env: testEnv,
});
const err = await stderr.text();
const out = await stdout.text();
expect(err).toContain("Saved lockfile");
expect(err).not.toContain("error:");
expect(await exited).toBe(0);
// The package that was actually trusted ran its scripts...
expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue();
// ...the hash-colliding package did not.
expect(await exists(join(packageDir, "node_modules", colliderName, "postinstall-ran.txt"))).toBeFalse();
// Only the name passed to --trust is written back to package.json.
const pkgJson = await file(packageJson).json();
expect(pkgJson.trustedDependencies).toEqual(["uses-what-bin"]);
// The colliding alias is not persisted into the lockfile's
// trustedDependencies either.
const lockfile = await file(join(packageDir, "bun.lock")).text();
const trusted = lockfile.match(/"trustedDependencies":\s*\[([^\]]*)\]/);
expect(trusted).not.toBeNull();
expect(trusted![1]).toContain('"uses-what-bin"');
expect(trusted![1]).not.toContain(colliderName);
});
for (const linker of ["hoisted", "isolated"]) {
test(`only trusts packages resolved inside the trusted subtree, not same-named dependencies elsewhere (${linker})`, async () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
const localPath = join(packageDir, "local-what-bin");
await mkdir(localPath, { recursive: true });
await Promise.all([
writeFile(
join(localPath, "package.json"),
JSON.stringify({
name: "what-bin",
version: "9.9.9",
scripts: {
postinstall: `${bunExe()} -e "require('fs').writeFileSync('postinstall-ran.txt', 'ran')"`,
},
}),
),
writeFile(
packageJson,
JSON.stringify({
name: "foo",
dependencies: {
"what-bin": "file:./local-what-bin",
},
}),
),
]);
const { stderr, exited } = spawn({
cmd: [bunExe(), "i", `--linker=${linker}`, "--trust", "[email protected]"],
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(await exited).toBe(0);
expect(await exists(join(packageDir, "node_modules", "uses-what-bin", "what-bin.txt"))).toBeTrue();
expect(await exists(join(packageDir, "node_modules", "what-bin", "postinstall-ran.txt"))).toBeFalse();
expect(await file(join(packageDir, "node_modules", "what-bin", "package.json")).json()).toMatchObject({
name: "what-bin",
version: "9.9.9",
});
const pkgJson = await file(packageJson).json();
expect(pkgJson.trustedDependencies).toEqual(["uses-what-bin"]);
const lockfile = await file(join(packageDir, "bun.lock")).text();
const trusted = lockfile.match(/"trustedDependencies":\s*\[([^\]]*)\]/);
expect(trusted).not.toBeNull();
expect(trusted![1]).toContain('"uses-what-bin"');
expect(trusted![1]).not.toContain('"what-bin"');
});
}
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: pkgJsonFixture } of trustTests) {
test(label, async () => {
using ctx = await setupTest();
const { packageDir, env } = ctx;
const testEnv = forceWaiterThread ? { ...env, BUN_FEATURE_FLAG_FORCE_WAITER_THREAD: "1" } : env;
await writeFile(join(packageDir, "package.json"), JSON.stringify(pkgJsonFixture));
let { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "i", "--trust", "[email protected]"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env: testEnv,
});
let err = 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 [email protected]",
"",
"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 = 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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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", "[email protected]"],
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
env: testEnv,
});
const err = 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 [email protected]",
"",
"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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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 = 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 [email protected]",
"",
"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).not.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 [email protected]",
"",
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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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 = 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("+ [email protected]"),
"",
"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 = 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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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 = 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("+ [email protected]"),
"",
"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 = 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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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 = 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."),
"",
"+ [email protected]",
"",
"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 = 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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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 = 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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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 = 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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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 = 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("+ [email protected]"),
"",
"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 = 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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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 = 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("+ [email protected]"),
expect.stringContaining("+ [email protected]"),
"",
"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 = 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 = 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 = 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 = 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("+ [email protected]"),
"",
"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 = 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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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.concurrent("stdout/stderr is inherited from root scripts during install", async () => {
test("without packages", async () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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 = 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 () => {
using ctx = await setupTest();
const { packageDir, packageJson, env } = ctx;
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 = 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("+ [email protected]"),
"",
"1 package installed",
]);
expect(await exited).toBe(0);
assertManifestsPopulated(join(packageDir, ".bun-cache"), verdaccio.registryUrl());
});
});
}