313 lines
13 KiB
TypeScript
313 lines
13 KiB
TypeScript
import { spawnSync } from "bun";
|
|
import { describe, expect, test } from "bun:test";
|
|
import { bunEnv, bunExe, isDebug, isWindows, tempDir } from "harness";
|
|
import fs from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
describe("bun", () => {
|
|
describe("NO_COLOR", () => {
|
|
for (const value of ["1", "0", "foo", " "]) {
|
|
test(`respects NO_COLOR=${JSON.stringify(value)} to disable color`, () => {
|
|
const { stdout } = spawnSync({
|
|
cmd: [bunExe()],
|
|
env: {
|
|
NO_COLOR: value,
|
|
},
|
|
});
|
|
expect(stdout.toString()).not.toMatch(/\u001b\[\d+m/);
|
|
});
|
|
}
|
|
for (const value of ["", undefined]) {
|
|
// TODO: need a way to fake a tty in order to test this,
|
|
// and cannot use FORCE_COLOR since that will always override NO_COLOR.
|
|
test.todo(`respects NO_COLOR=${JSON.stringify(value)} to enable color`, () => {
|
|
const { stdout } = spawnSync({
|
|
cmd: [bunExe()],
|
|
env:
|
|
value === undefined
|
|
? {}
|
|
: {
|
|
NO_COLOR: value,
|
|
},
|
|
});
|
|
expect(stdout.toString()).toMatch(/\u001b\[\d+m/);
|
|
});
|
|
}
|
|
});
|
|
|
|
describe("revision", () => {
|
|
test("revision generates version numbers correctly", () => {
|
|
var { stdout, exitCode } = Bun.spawnSync({
|
|
cmd: [bunExe(), "--version"],
|
|
env: bunEnv,
|
|
stderr: "inherit",
|
|
});
|
|
var version = stdout.toString().trim();
|
|
|
|
var { stdout, exitCode } = Bun.spawnSync({
|
|
cmd: [bunExe(), "--revision"],
|
|
env: bunEnv,
|
|
stderr: "inherit",
|
|
});
|
|
var revision = stdout.toString().trim();
|
|
|
|
expect(exitCode).toBe(0);
|
|
expect(revision).toStartWith(version);
|
|
// https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
|
|
expect(revision).toMatch(
|
|
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/,
|
|
);
|
|
});
|
|
});
|
|
describe("getcompletes", () => {
|
|
test("getcompletes should not panic and should not be empty", () => {
|
|
const { stdout, exitCode } = spawnSync({
|
|
cmd: [bunExe(), "getcompletes"],
|
|
env: bunEnv,
|
|
});
|
|
expect(exitCode).toBe(0);
|
|
expect(stdout.toString()).not.toBeEmpty();
|
|
});
|
|
|
|
// https://github.com/oven-sh/bun/issues/30086
|
|
test("getcompletes keeps scripts whose names start with 'pre'/'post' when no sibling script exists", () => {
|
|
using dir = tempDir("getcompletes-pre-post", {
|
|
"package.json": JSON.stringify({
|
|
name: "test",
|
|
scripts: {
|
|
// standalone scripts — nothing named `ttier`, `pare-release`, `gres`, `css`, `view`
|
|
"prettier": "echo prettier",
|
|
"prettier:fix": "echo prettier:fix",
|
|
"prepare-release": "echo prepare-release",
|
|
"postgres": "echo postgres",
|
|
"postcss": "echo postcss",
|
|
"preview": "echo preview",
|
|
// plain scripts
|
|
"build": "echo build",
|
|
"dev": "echo dev",
|
|
"lint": "echo lint",
|
|
"lint:fix": "echo lint:fix",
|
|
"fix": "echo fix",
|
|
"test": "echo test",
|
|
// real lifecycle hooks — these SHOULD be hidden (sibling exists)
|
|
"prebuild": "echo prebuild",
|
|
"postbuild": "echo postbuild",
|
|
"pretest": "echo pretest",
|
|
},
|
|
}),
|
|
});
|
|
|
|
for (const filter of ["s", "i", "r", "g", "z"]) {
|
|
const { stdout, exitCode } = spawnSync({
|
|
cmd: [bunExe(), "getcompletes", filter],
|
|
env: bunEnv,
|
|
cwd: String(dir),
|
|
});
|
|
const lines = stdout
|
|
.toString()
|
|
.split("\n")
|
|
.map(l => l.split("\t")[0]) // "z" filter emits "name\tdescription"
|
|
.filter(Boolean);
|
|
|
|
// standalone pre/post-prefixed scripts must be present
|
|
expect(lines).toContain("prettier");
|
|
expect(lines).toContain("prettier:fix");
|
|
expect(lines).toContain("prepare-release");
|
|
expect(lines).toContain("postgres");
|
|
expect(lines).toContain("postcss");
|
|
expect(lines).toContain("preview");
|
|
|
|
// real npm lifecycle hooks (sibling `build`/`test` exists) must still be hidden
|
|
expect(lines).not.toContain("prebuild");
|
|
expect(lines).not.toContain("postbuild");
|
|
expect(lines).not.toContain("pretest");
|
|
|
|
expect(exitCode).toBe(0);
|
|
}
|
|
});
|
|
});
|
|
// On Windows `bun completions` installs bunx as a hardlink (or a .cmd shim) instead of a symlink.
|
|
describe.skipIf(isWindows)("completions", () => {
|
|
const bunxName = isDebug ? "bunx-debug" : "bunx";
|
|
|
|
test("installs a bunx symlink to the executable, falling back through the install directories", async () => {
|
|
using dir = tempDir("completions-bunx", {
|
|
"bin": {},
|
|
"empty-path": {},
|
|
"install": { bin: {} },
|
|
"home-empty": {},
|
|
"home-bun": { ".bun": { bin: {} } },
|
|
"home-local": { ".local": { bin: {} } },
|
|
});
|
|
// Run a private copy of the executable so that the first candidate location, the executable's
|
|
// own directory, is inside the temporary directory. A hardlink avoids copying the binary; fall
|
|
// back to a copy when the temporary directory is on another filesystem.
|
|
const exe = join(String(dir), "bin", "bun");
|
|
try {
|
|
fs.linkSync(fs.realpathSync(bunExe()), exe);
|
|
} catch {
|
|
fs.copyFileSync(bunExe(), exe);
|
|
}
|
|
// The link is created against the resolved executable path.
|
|
const exeRealpath = fs.realpathSync(exe);
|
|
|
|
async function installBunx(env: Record<string, string | undefined>) {
|
|
await using proc = Bun.spawn({
|
|
cmd: [exe, "completions"],
|
|
// No bunx on PATH, so the symlink gets installed. No SHELL, so the command stops right
|
|
// after that step instead of writing shell completions.
|
|
env: { ...bunEnv, PATH: join(String(dir), "empty-path"), SHELL: undefined, BUN_INSTALL: undefined, ...env },
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
expect(stdout).toBe("");
|
|
expect(stderr).toContain("Unknown or unsupported shell");
|
|
expect(exitCode).toBe(1);
|
|
}
|
|
|
|
// 1. Next to the executable.
|
|
await installBunx({ HOME: join(String(dir), "home-empty") });
|
|
expect(fs.readlinkSync(join(String(dir), "bin", bunxName))).toBe(exeRealpath);
|
|
|
|
// That link now exists, so every following run falls through to the next location.
|
|
// 2. $BUN_INSTALL/bin
|
|
await installBunx({ HOME: join(String(dir), "home-empty"), BUN_INSTALL: join(String(dir), "install") });
|
|
expect(fs.readlinkSync(join(String(dir), "install", "bin", bunxName))).toBe(exeRealpath);
|
|
|
|
// 3. $HOME/.bun/bin
|
|
await installBunx({ HOME: join(String(dir), "home-bun") });
|
|
expect(fs.readlinkSync(join(String(dir), "home-bun", ".bun", "bin", bunxName))).toBe(exeRealpath);
|
|
|
|
// 4. $HOME/.local/bin, once $HOME/.bun/bin does not exist.
|
|
await installBunx({ HOME: join(String(dir), "home-local") });
|
|
expect(fs.readlinkSync(join(String(dir), "home-local", ".local", "bin", bunxName))).toBe(exeRealpath);
|
|
});
|
|
});
|
|
describe("--help preserves <placeholder> text", () => {
|
|
const env = { ...bunEnv, NO_COLOR: "1" };
|
|
const usage: [string, string][] = [
|
|
["install", "bun install [flags] <name>@<version>"],
|
|
["add", "bun add [flags] <package><@version>"],
|
|
["remove", "bun remove [flags] [<packages>]"],
|
|
["update", "bun update [flags] <name>@<version>"],
|
|
["link", "bun link [flags] [<packages>]"],
|
|
["patch", "bun patch [flags or options] <package>@<version>"],
|
|
["patch-commit", "bun patch-commit [flags or options] <directory>"],
|
|
["info", "bun info [flags] <package>[@<version>]"],
|
|
];
|
|
test.concurrent.each(usage)("bun %s --help usage line", async (cmd, expected) => {
|
|
await using proc = Bun.spawn({ cmd: [bunExe(), cmd, "--help"], env, stderr: "pipe" });
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
const line = (stdout + stderr).split(/\r?\n/).find(l => l.startsWith("Usage:")) ?? "";
|
|
expect(line).toBe(`Usage: ${expected}`);
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
const flags: [string, string, string][] = [
|
|
["audit", "--audit-level", "greater than or equal to <level> (low,"],
|
|
["test", "--rerun-each", "Re-run each test file <NUMBER> times"],
|
|
["test", "--bail", "Exit the test suite after <NUMBER> failures"],
|
|
["build", "--allow-unresolved", "Use '<empty>' for opaque specifiers"],
|
|
["add", "-F, --filter", "Add the package(s) to the matching workspaces instead of the current package"],
|
|
["add", "--catalog", 'depend on it as "catalog:" (use --catalog=NAME for a named catalog)'],
|
|
["remove", "-F, --filter", "Remove the package(s) from the matching workspaces instead of the current package"],
|
|
["install", "-F, --filter", "Install packages for the matching workspaces"],
|
|
["install", "--catalog", 'depend on it as "catalog:" (use --catalog=NAME for a named catalog)'],
|
|
["audit", "--ignore", "Ignore advisories by GHSA or numeric advisory ID (repeatable)"],
|
|
["audit", "-L, --latest", "Also apply fixes your declared ranges exclude, rewriting package.json"],
|
|
["update", "-p, --production", "Only update dependencies and optionalDependencies (alias: --prod)"],
|
|
["install", "-p, --production", "Don't install devDependencies"],
|
|
];
|
|
test.concurrent.each(flags)("bun %s --help keeps placeholder in %s description", async (cmd, flag, expected) => {
|
|
await using proc = Bun.spawn({ cmd: [bunExe(), cmd, "--help"], env, stderr: "pipe" });
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
const line = (stdout + stderr).split(/\r?\n/).find(l => l.includes(flag)) ?? "";
|
|
expect(line).toContain(expected);
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
test("bun add --help usage line is intact with FORCE_COLOR=1", async () => {
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "add", "--help"],
|
|
env: { ...bunEnv, NO_COLOR: undefined, FORCE_COLOR: "1" },
|
|
stderr: "pipe",
|
|
});
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
const out = stdout + stderr;
|
|
// <blue>\<package\><r> renders to \x1b[34m<package>\x1b[0m, not \x1b[34m\x1b[0m
|
|
expect(out).toContain("\x1b[34m<package>\x1b[0m");
|
|
// raw tag markup must not leak through
|
|
expect(out).not.toContain("<blue>");
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("--help lists package manager commands and examples", () => {
|
|
const env = { ...bunEnv, NO_COLOR: "1" };
|
|
const lines: [string, string[], RegExp[]][] = [
|
|
[
|
|
"bun --help",
|
|
[],
|
|
[
|
|
/^ {2}dedupe +Remove duplicate versions from the lockfile$/m,
|
|
/^ {2}prune +Remove packages that are not in the lockfile from node_modules$/m,
|
|
],
|
|
],
|
|
[
|
|
"bun pm --help",
|
|
["pm"],
|
|
[
|
|
/^ {2}bun pm ls +list the dependency tree according to the current lockfile$/m,
|
|
/^ {2}bun pm licenses +list installed packages grouped by license$/m,
|
|
],
|
|
],
|
|
[
|
|
"bun add --help",
|
|
["add"],
|
|
[
|
|
/^ {2}Add a dependency to a specific workspace in a monorepo\n {2}bun add zod --filter api$/m,
|
|
/^ {2}Add to the workspace catalog instead of pinning a version\n {2}bun add --catalog react\n {2}bun add --catalog=testing vitest$/m,
|
|
],
|
|
],
|
|
[
|
|
"bun audit --help",
|
|
["audit"],
|
|
[
|
|
/^ {2}bun audit fix upgrades vulnerable packages to the lowest safe version that still satisfies every dependent's range\.$/m,
|
|
/^ {2}Upgrade vulnerable packages in bun\.lock and node_modules; package\.json is only changed when an exact pin has to be bumped\.\n {2}bun audit fix$/m,
|
|
/^ {2}Show what bun audit fix would change without changing anything\.\n {2}bun audit fix --dry-run$/m,
|
|
/^ {2}Also apply fixes that your package\.json ranges exclude, rewriting those ranges\.\n {2}bun audit fix --latest$/m,
|
|
],
|
|
],
|
|
];
|
|
test.concurrent.each(lines)("%s", async (_, args, expected) => {
|
|
await using proc = Bun.spawn({ cmd: [bunExe(), ...args, "--help"], env, stderr: "pipe" });
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
const out = (stdout + stderr).replaceAll("\r\n", "\n");
|
|
for (const re of expected) expect(out).toMatch(re);
|
|
expect(out).not.toContain("bun list ");
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("test command line arguments", () => {
|
|
test("test --config, issue #4128", () => {
|
|
const path = `${tmpdir()}/bunfig-${Date.now()}.toml`;
|
|
fs.writeFileSync(path, "[debug]");
|
|
|
|
const p = Bun.spawnSync({
|
|
cmd: [bunExe(), "--config=" + path],
|
|
env: {},
|
|
stderr: "inherit",
|
|
});
|
|
try {
|
|
expect(p.exitCode).toBe(0);
|
|
} finally {
|
|
fs.unlinkSync(path);
|
|
}
|
|
});
|
|
});
|
|
});
|