import { SyncSubprocess } from "bun";
import { describe, expect, test } from "bun:test";
import { rmSync, writeFileSync } from "fs";
import { bunEnv, bunExe, isWindows, tempDir, tmpdirSync } from "harness";
import { tmpdir } from "os";
import { join, sep } from "path";
test.concurrent.each([
{ label: "block function", source: "{ function f() {} console.log(f.name); }", expected: "f" },
{ label: "arrow initializer", source: "{ const f = () => {}; console.log(f.name); }", expected: "f" },
{ label: "async arrow initializer", source: "{ const f = async () => {}; console.log(f.name); }", expected: "f" },
{ label: "function initializer", source: "{ let f = function() {}; console.log(f.name); }", expected: "f" },
{ label: "generator initializer", source: "{ const f = function*() {}; console.log(f.name); }", expected: "f" },
{ label: "class initializer", source: "{ const C = class {}; console.log(C.name); }", expected: "C" },
{
label: "explicit function name",
source: "{ const alias = function original() {}; console.log(alias.name); }",
expected: "original",
},
{
label: "explicit class name",
source: "{ const alias = class Original {}; console.log(alias.name); }",
expected: "Original",
},
{
label: "mutable function binding",
source: '{ function f() { return f; } const original = f; f = "rebound"; console.log(original()); }',
expected: "rebound",
},
{
label: "mutable class binding",
source:
'{ let C = class { static current() { return C; } }; const original = C; C = "rebound"; console.log(original.current()); }',
expected: "rebound",
},
{
label: "recursive block function",
source: "{ function f(n) { return n === 0 ? 1 : n * f(n - 1); } console.log(f(5)); }",
expected: "120",
},
{
label: "direct eval binding access",
source: `{ function f() { return 7; } console.log(eval("f.name + ':' + f()")); }`,
expected: "f:7",
},
])("eval preserves $label semantics", async ({ source, expected }) => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", source],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect({ stdout, stderr, exitCode }).toEqual({ stdout: expected + "\n", stderr: "", exitCode: 0 });
});
for (const flag of ["-e", "--print"]) {
describe(`bun ${flag}`, () => {
test("it works", async () => {
const input = flag === "--print" ? '"hello world"' : 'console.log("hello world")';
let { stdout } = Bun.spawnSync({
cmd: [bunExe(), flag, input],
env: bunEnv,
});
expect(stdout.toString("utf8")).toEqual("hello world\n");
});
test("import, tsx, require in esm, import.meta", async () => {
const ref = await import("react");
const input =
flag === "--print"
? 'import {version} from "react"; console.log(JSON.stringify({version,file:import.meta.path,require:require("react").version})); world'
: 'import {version} from "react"; console.log(JSON.stringify({version,file:import.meta.path,require:require("react").version})); console.log(world);';
let { stdout } = Bun.spawnSync({
cmd: [bunExe(), flag, input],
env: bunEnv,
});
const json = {
version: ref.version,
file: join(process.cwd(), "[eval]"),
require: ref.version,
};
expect(stdout.toString("utf8")).toEqual(JSON.stringify(json) + "\nworld\n");
});
test("error has source map info 1", async () => {
let { stderr } = Bun.spawnSync({
cmd: [bunExe(), flag, '(throw new Error("hi" as 2))'],
env: bunEnv,
});
expect(stderr.toString("utf8")).toInclude('"hi" as 2');
expect(stderr.toString("utf8")).toInclude("Unexpected throw");
});
test("process.argv", async () => {
function testProcessArgv(args: string[], expected: string[]) {
const input = flag === "--print" ? "process.argv" : "console.log(process.argv)";
let { stdout, stderr, exitCode } = Bun.spawnSync({
cmd: [bunExe(), flag, input, ...args],
env: bunEnv,
});
expect(stderr.toString("utf8")).toBe("");
expect(JSON.parse(stdout.toString("utf8"))).toEqual(expected);
expect(exitCode).toBe(0);
}
// replace the trailin
const exe = isWindows ? bunExe().replaceAll("/", "\\") : bunExe();
testProcessArgv([], [exe]);
testProcessArgv(["abc", "def"], [exe, "abc", "def"]);
testProcessArgv(["--", "abc", "def"], [exe, "abc", "def"]);
// testProcessArgv(["--", "abc", "--", "def"], [exe, "abc", "--", "def"]);
});
test("process._eval", async () => {
const code = flag === "--print" ? "process._eval" : "console.log(process._eval)";
const { stdout } = Bun.spawnSync({
cmd: [bunExe(), flag, code],
env: bunEnv,
});
expect(stdout.toString("utf8")).toEqual(code + "\n");
});
// The eval source is UTF-8; reading it back as Latin-1 turns every
// multi-byte character into mojibake. The expected text is compared here
// in the parent -- comparing inside the child would pass either way, since
// a Latin-1-decoded source corrupts the literal and process._eval alike.
test("process._eval round-trips multi-byte UTF-8", async () => {
const marker = "/* 한글-🎉-café */";
const code = (flag === "--print" ? "process._eval" : "console.log(process._eval)") + ` ${marker}`;
const { stdout } = Bun.spawnSync({
cmd: [bunExe(), flag, code],
env: bunEnv,
});
expect(stdout.toString("utf8")).toEqual(code + "\n");
});
test("does not crash in non-latin1 directory", async () => {
const dir = join(tmpdirSync(), "eval-test-开始学习");
await Bun.write(join(dir, "index.js"), "console.log('hello world')");
const { stdout, stderr, exitCode } = Bun.spawnSync({
cmd: [bunExe(), flag, "import './index.js'"],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
stdin: "ignore",
});
expect(stderr.toString("utf8")).toBe("");
expect(stdout.toString("utf8")).toEqual("hello world\n" + (flag === "--print" ? "undefined\n" : ""));
expect(exitCode).toBe(0);
});
});
}
describe("--print for cjs/esm", () => {
test("eval result between esm imports", async () => {
let cwd = tmpdirSync();
writeFileSync(join(cwd, "foo.js"), "'foo'");
writeFileSync(join(cwd, "bar.js"), "'bar'");
let { stdout, stderr, exitCode } = Bun.spawnSync({
cmd: [bunExe(), "--print", 'import "./foo.js"; 123; import "./bar.js"'],
cwd: cwd,
env: bunEnv,
});
expect(stderr.toString("utf8")).toBe("");
expect(stdout.toString("utf8")).toEqual("123\n");
expect(exitCode).toBe(0);
rmSync(cwd, { recursive: true, force: true });
});
// https://github.com/oven-sh/bun/issues/30207
describe.each([
{ expr: "(await 1) + 1", expected: "2" },
{ expr: 'await Promise.resolve("hello") + " world"', expected: "hello world" },
{ expr: "(await 1) + (await 2)", expected: "3" },
// no top-level await — still returns the expression value.
{ expr: "1 + 1", expected: "2" },
])("bun -p $expr", ({ expr, expected }) => {
test(`→ ${expected}`, async () => {
const { stdout, stderr, exitCode } = Bun.spawnSync({
cmd: [bunExe(), "-p", expr],
env: bunEnv,
});
expect(stderr.toString("utf8")).toBe("");
expect(stdout.toString("utf8")).toBe(`${expected}\n`);
expect(exitCode).toBe(0);
});
});
test("forced cjs", async () => {
let { stdout, stderr, exitCode } = Bun.spawnSync({
cmd: [bunExe(), "--print", "module.exports; 123"],
env: bunEnv,
});
expect(stderr.toString("utf8")).toBe("");
expect(stdout.toString("utf8")).toEqual("123\n");
expect(exitCode).toBe(0);
});
test("module, exports, require, __filename, __dirname", async () => {
let { stdout, stderr, exitCode } = Bun.spawnSync({
cmd: [
bunExe(),
"--print",
`
console.log(typeof module, typeof exports, typeof require, typeof __filename, typeof __dirname); 123
`,
],
env: bunEnv,
});
expect(stderr.toString("utf8")).toBe("");
expect(stdout.toString("utf8")).toEqual("object object function string string\n123\n");
expect(exitCode).toBe(0);
});
test("module._compile is require('module').prototype._compile", async () => {
const { stdout, exitCode } = Bun.spawnSync({
cmd: [bunExe(), "-p", "module._compile === require('module').prototype._compile"],
env: bunEnv,
});
expect(stdout.toString()).toBe("true\n");
expect(exitCode).toBe(0);
});
// The hoist patterns in [install] are regexes. Compiling them while
// bunfig.toml loaded initialized JSC before `bun -p` could turn on eval
// mode, so the script's completion value was dropped and every --print
// printed undefined.
describe.each(["hoistPattern", "publicHoistPattern"])("with install.%s in bunfig.toml", key => {
test.concurrent.each([
{ expr: "Math.max(1, 9)", expected: "9" },
{ expr: "[1, 2].map(x => x * 2)", expected: "[ 2, 4 ]" },
{ expr: "1 + 1", expected: "2" },
{ expr: "(await 1) + 1", expected: "2" },
])("bun -p $expr", async ({ expr, expected }) => {
using dir = tempDir("eval-install-pattern", {
"bunfig.toml": `[install]\n${key} = ["*eslint*", "!eslint-plugin-*"]\n`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "-p", expr],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout).toBe(`${expected}\n`);
expect(exitCode).toBe(0);
});
});
});
function group(run: (code: string) => SyncSubprocess<"pipe", "inherit">) {
test("it works", async () => {
const { stdout } = run('console.log("hello world")');
expect(stdout.toString("utf8")).toEqual("hello world\n");
});
test("it gets a correct specifer", async () => {
const { stdout } = run("console.log(import.meta.path)");
expect(stdout.toString("utf8")).toEndWith(sep + "[stdin]\n");
});
test("it can require", async () => {
const { stdout } = run(`
const process = require("node:process");
console.log(process.platform);
`);
expect(stdout.toString("utf8")).toEqual(process.platform + "\n");
});
test("it can import", async () => {
const { stdout } = run(`
import * as process from "node:process";
console.log(process.platform);
`);
expect(stdout.toString("utf8")).toEqual(process.platform + "\n");
});
test("process.argv", async () => {
const { stdout } = run("console.log(process.argv)");
const exe = isWindows ? bunExe().replaceAll("/", "\\") : bunExe();
expect(JSON.parse(stdout.toString("utf8"))).toEqual([exe, "-"]);
});
test("process._eval", async () => {
const code = "console.log(process._eval)";
const { stdout } = run(code);
// the file piping one on windows can include extra carriage returns
if (isWindows) {
expect(stdout.toString("utf8")).toInclude(code);
} else {
expect(stdout.toString("utf8")).toEqual(code + "\n");
}
});
}
describe("bun run - < file-path.js", () => {
function run(code: string) {
// bash only supports / as path separator
const file = join(tmpdir(), "bun-run-eval-test.js").replaceAll("\\", "/");
require("fs").writeFileSync(file, code);
try {
let result;
if (process.platform === "win32") {
result = Bun.spawnSync(["powershell", "-c", `Get-Content ${file} | ${bunExe()} run -`], {
env: bunEnv,
stderr: "inherit",
});
} else {
result = Bun.spawnSync(["bash", "-c", `${bunExe()} run - < ${file}`], {
env: bunEnv,
stderr: "inherit",
});
}
if (!result.success) {
queueMicrotask(() => {
throw new Error("bun run - < file-path.js failed");
});
}
return result;
} finally {
try {
require("fs").unlinkSync(file);
} catch (e) {}
}
}
group(run);
});
describe("echo | bun run -", () => {
function run(code: string) {
const result = Bun.spawnSync([bunExe(), "run", "-"], {
env: bunEnv,
stdin: Buffer.from(code),
stderr: "inherit",
});
if (!result.success) {
queueMicrotask(() => {
throw new Error("bun run - failed");
});
}
return result;
}
group(run);
});
test("process._eval (undefined for normal run)", async () => {
const cwd = tmpdirSync();
const file = join(cwd, "test.js");
writeFileSync(file, "console.log(typeof process._eval)");
const { stdout } = Bun.spawnSync({
cmd: [bunExe(), "run", file],
cwd: cwd,
env: bunEnv,
});
expect(stdout.toString("utf8")).toEqual("undefined\n");
rmSync(cwd, { recursive: true, force: true });
});
test("uncaught error from a CommonJS-sniffed eval entry reports and exits 1", async () => {
// The presence of require() makes the eval source evaluate as CommonJS,
// which used to swallow a top-level throw entirely: no stderr output and
// exit code 0.
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", `require("assert"); throw new Error("eval-cjs-uncaught");`],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("eval-cjs-uncaught");
expect(stdout).toBe("");
expect(exitCode).toBe(1);
});
test("uncaught error from a CommonJS-sniffed stdin entry reports and exits 1", async () => {
await using proc = Bun.spawn({
cmd: [bunExe(), "-"],
env: bunEnv,
stdin: Buffer.from(`require("assert"); throw new Error("stdin-cjs-uncaught");`),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("stdin-cjs-uncaught");
expect(stdout).toBe("");
expect(exitCode).toBe(1);
});