4220 lines
144 KiB
TypeScript
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());
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|