mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
## Summary - When running `bun <file>` on a file with an unsupported type (e.g., `.css`, `.yaml`, `.toml`), Bun now shows a helpful error message instead of the misleading "File not found" - Tracks when a file is resolved but has a loader that can't be run directly - Shows the actual file path and file type in the error message **Before:** ``` error: File not found "test.css" ``` **After:** ``` error: Cannot run "/path/to/test.css" note: Bun cannot run css files directly ``` ## Test plan - [x] Added regression test in `test/regression/issue/1365.test.ts` - [x] Test verifies unsupported files show "Cannot run" error - [x] Test verifies nonexistent files still show "File not found" - [x] Test fails with `USE_SYSTEM_BUN=1` and passes with debug build Fixes #1365 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
1010 lines
34 KiB
TypeScript
1010 lines
34 KiB
TypeScript
import { $ } from "bun";
|
|
import { describe, expect, it } from "bun:test";
|
|
import { chmodSync } from "fs";
|
|
import { bunEnv as bunEnv_, bunExe, isWindows, tempDir, tempDirWithFiles } from "harness";
|
|
import { join } from "path";
|
|
|
|
const bunEnv = {
|
|
...bunEnv_,
|
|
BUN_INTERNAL_SUPPRESS_CRASH_IN_BUN_RUN: "1",
|
|
};
|
|
|
|
describe.concurrent("bun run", () => {
|
|
for (let withRun of [false, true]) {
|
|
describe(withRun ? "bun run" : "bun", () => {
|
|
describe("should work with .", () => {
|
|
it("respecting 'main' field and allowing trailing commas/comments in package.json", async () => {
|
|
using dir = tempDir("bun-run-main", {
|
|
"test.js": "console.log('Hello, world!');",
|
|
"package.json": `{
|
|
// single-line comment
|
|
"name": "test",
|
|
/** even multi-line comment!!
|
|
* such feature much compatible very ecosystem
|
|
*/
|
|
"version": "0.0.0",
|
|
"main": "test.js",
|
|
}`,
|
|
});
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), ...(withRun ? ["run"] : []), "."].filter(Boolean),
|
|
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("Hello, world!\n");
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
it("falling back to index", async () => {
|
|
using dir = tempDir("bun-run-index", {
|
|
"index.ts": "console.log('Hello, world!');",
|
|
"package.json": JSON.stringify({
|
|
name: "test",
|
|
version: "0.0.0",
|
|
}),
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), ...(withRun ? ["run"] : []), "."].filter(Boolean),
|
|
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("Hello, world!\n");
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
it("invalid tsconfig.json is ignored", async () => {
|
|
using dir = tempDir("bun-run-tsconfig", {
|
|
"package.json": JSON.stringify({
|
|
name: "test",
|
|
version: "0.0.0",
|
|
scripts: {
|
|
"boop": "echo hi",
|
|
},
|
|
}),
|
|
"tsconfig.json": "!!!bad!!!",
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "--silent", ...(withRun ? ["run"] : []), "boop"].filter(Boolean),
|
|
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.replaceAll("\r\n", "\n")).toBe("hi\n");
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
it("--silent omits error messages", async () => {
|
|
using dir = tempDir("bun-run-silent", {});
|
|
const exe = isWindows ? "bun.exe" : "bun";
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "run", "--silent", exe, "doesnotexist"],
|
|
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).not.toEndWith(`error: "${exe}" exited with code 1\n`);
|
|
expect(stdout).toBe("");
|
|
expect(exitCode).toBe(1);
|
|
});
|
|
|
|
it("no --silent includes error messages", async () => {
|
|
using dir = tempDir("bun-run-nosilent", {});
|
|
const exe = isWindows ? "bun.exe" : "bun";
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "run", exe, "doesnotexist"],
|
|
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).toEndWith(`error: "${exe}" exited with code 1\n`);
|
|
expect(exitCode).toBe(1);
|
|
});
|
|
|
|
it.skipIf(isWindows)("exit code message works above 128", async () => {
|
|
using dir = tempDir("bun-run-exitcode", {});
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "run", "bash", "-c", "ulimit -c 0; exit 200"],
|
|
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).toStartWith('error: "bash" exited with code 200');
|
|
expect(exitCode).toBe(200);
|
|
});
|
|
|
|
describe.each(["--silent", "not silent"])("%s", silentOption => {
|
|
const silent = silentOption === "--silent";
|
|
it.skipIf(isWindows)("exit signal works", async () => {
|
|
using dir = tempDir("bun-run-signal", {});
|
|
{
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), ...(silent ? ["--silent"] : []), "run", "bash", "-c", "ulimit -c 0; kill -4 $$"].filter(
|
|
Boolean,
|
|
),
|
|
cwd: String(dir),
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
proc.stdout.text(),
|
|
proc.stderr.text(),
|
|
proc.exited,
|
|
]);
|
|
|
|
if (silent) {
|
|
expect(stderr).toBe("");
|
|
} else {
|
|
expect(stderr).toContain("bash");
|
|
expect(stderr).toContain("SIGILL");
|
|
}
|
|
|
|
expect(proc.signalCode).toBe("SIGILL");
|
|
// exitCode is null or 128+signal depending on context
|
|
expect(exitCode === null || exitCode === 132).toBe(true);
|
|
}
|
|
{
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), ...(silent ? ["--silent"] : []), "run", "bash", "-c", "ulimit -c 0; kill -9 $$"],
|
|
cwd: String(dir),
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
proc.stdout.text(),
|
|
proc.stderr.text(),
|
|
proc.exited,
|
|
]);
|
|
|
|
if (silent) {
|
|
expect(stderr).toBe("");
|
|
} else {
|
|
expect(stderr).toContain("bash");
|
|
expect(stderr).toContain("SIGKILL");
|
|
}
|
|
expect(proc.signalCode).toBe("SIGKILL");
|
|
// exitCode is null or 128+signal depending on context
|
|
expect(exitCode === null || exitCode === 137).toBe(true);
|
|
}
|
|
});
|
|
});
|
|
|
|
for (let withLogLevel of [true, false]) {
|
|
it(
|
|
"valid tsconfig.json with invalid extends doesn't crash" + (withLogLevel ? " (log level debug)" : ""),
|
|
async () => {
|
|
using dir = tempDir("bun-run-tsconfig-extends", {
|
|
"package.json": JSON.stringify({
|
|
name: "test",
|
|
version: "0.0.0",
|
|
scripts: {},
|
|
}),
|
|
"tsconfig.json": JSON.stringify(
|
|
{
|
|
extends: "!!!bad!!!",
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
"index.js": "console.log('hi')",
|
|
...(withLogLevel ? { "bunfig.toml": `logLevel = "debug"` } : {}),
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
// TODO: figure out why -c is necessary here.
|
|
cmd: [
|
|
bunExe(),
|
|
...(withRun ? ["run"] : []),
|
|
"-c=" + join(String(dir), "bunfig.toml"),
|
|
"./index.js",
|
|
].filter(Boolean),
|
|
cwd: String(dir),
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([
|
|
proc.stdout.text(),
|
|
proc.stderr.text(),
|
|
proc.exited,
|
|
]);
|
|
|
|
if (withLogLevel) {
|
|
expect(stderr.trim()).toContain("ENOENT loading tsconfig.json extends");
|
|
} else {
|
|
expect(stderr.trim()).not.toContain("ENOENT loading tsconfig.json extends");
|
|
}
|
|
|
|
expect(stdout).toBe("hi\n");
|
|
expect(exitCode).toBe(0);
|
|
},
|
|
);
|
|
}
|
|
|
|
it("falling back to index with no package.json", async () => {
|
|
using dir = tempDir("bun-run-nopkg", {
|
|
"index.ts": "console.log('Hello, world!');",
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), ...(withRun ? ["run"] : []), "."].filter(Boolean),
|
|
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("Hello, world!\n");
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
it("should not passthrough script arguments to pre- or post- scripts", async () => {
|
|
using dir = tempDir("bun-run-prepost", {
|
|
"package.json": JSON.stringify({
|
|
scripts: {
|
|
premyscript: "echo pre",
|
|
myscript: "echo main",
|
|
postmyscript: "echo post",
|
|
},
|
|
}),
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "run", "--silent", "myscript", "-a", "-b", "-c"].filter(Boolean),
|
|
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.replaceAll("\r\n", "\n")).toBe("pre\n" + "main -a -b -c\n" + "post\n");
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
it("should show the correct working directory when run with --cwd", async () => {
|
|
using dir = tempDir("bun-run-cwd", {
|
|
"subdir/test.js": `console.log(process.cwd());`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "run", "--cwd", "subdir", "test.js"],
|
|
cwd: String(dir),
|
|
stdin: "ignore",
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
env: {
|
|
...bunEnv,
|
|
BUN_INSTALL_CACHE_DIR: join(String(dir), ".cache"),
|
|
},
|
|
});
|
|
|
|
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
|
|
|
|
expect(stdout).toMatch(/subdir/);
|
|
// The exit code will not be 1 if it panics.
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
it("DCE annotations are respected", async () => {
|
|
using dir = tempDir("test", {
|
|
"index.ts": `
|
|
/* @__PURE__ */ console.log("Hello, world!");
|
|
`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "run", "index.ts"],
|
|
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("");
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
it("--ignore-dce-annotations ignores DCE annotations", async () => {
|
|
using dir = tempDir("test", {
|
|
"index.ts": `
|
|
/* @__PURE__ */ console.log("Hello, world!");
|
|
`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "--ignore-dce-annotations", "run", "index.ts"],
|
|
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("Hello, world!\n");
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
it("$npm_command is accurate", async () => {
|
|
using dir = tempDir("bun-run-npm-command", {
|
|
"package.json": `{
|
|
"scripts": {
|
|
"sample": "echo $npm_command",
|
|
},
|
|
}
|
|
`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "run", "sample"],
|
|
cwd: String(dir),
|
|
stdin: "ignore",
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
env: bunEnv,
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(stderr).toBe(`$ echo $npm_command\n`);
|
|
expect(stdout).toBe(`run-script\n`);
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
it("$npm_lifecycle_event is accurate", async () => {
|
|
using dir = tempDir("bun-run-npm-lifecycle", {
|
|
"package.json": `{
|
|
"scripts": {
|
|
"presample": "echo $npm_lifecycle_event",
|
|
"sample": "echo $npm_lifecycle_event",
|
|
"postsample": "echo $npm_lifecycle_event",
|
|
},
|
|
}
|
|
`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "run", "sample"],
|
|
cwd: String(dir),
|
|
stdin: "ignore",
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
env: bunEnv,
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
// prettier-ignore
|
|
expect(stderr).toBe(`$ echo $npm_lifecycle_event\n$ echo $npm_lifecycle_event\n$ echo $npm_lifecycle_event\n`);
|
|
expect(stdout).toBe(`presample\nsample\npostsample\n`);
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
it("$npm_package_config_* works", async () => {
|
|
using dir = tempDir("bun-run-npm-config", {
|
|
"package.json": `{
|
|
"config": {
|
|
"foo": "bar"
|
|
},
|
|
"scripts": {
|
|
"sample": "echo $npm_package_config_foo",
|
|
},
|
|
}
|
|
`,
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "run", "sample"],
|
|
cwd: String(dir),
|
|
stdin: "ignore",
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
env: bunEnv,
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(stderr).toBe(`$ echo $npm_package_config_foo\n`);
|
|
expect(stdout).toBe(`bar\n`);
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
it("does not crash after spawning with $ variable", async () => {
|
|
using dir = tempDir("bun-run-dollar", {
|
|
"package.json": JSON.stringify({
|
|
scripts: {
|
|
debug: "bun index.js $hi",
|
|
},
|
|
}),
|
|
});
|
|
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "run", "debug"],
|
|
cwd: String(dir),
|
|
stdin: "ignore",
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
env: bunEnv,
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(stdout).toBe("");
|
|
expect(stderr).toBe(
|
|
'$ bun index.js $hi\nerror: Module not found "index.js"\nerror: script "debug" exited with code 1\n',
|
|
);
|
|
expect(exitCode).toBe(1);
|
|
});
|
|
|
|
it("should pass arguments correctly in scripts", async () => {
|
|
using dir = tempDir("test", {
|
|
"package.json": JSON.stringify({
|
|
workspaces: ["a", "b"],
|
|
scripts: { "root_script": "bun index.ts" },
|
|
}),
|
|
"index.ts": `for(const arg of Bun.argv) console.log(arg);`,
|
|
"a/package.json": JSON.stringify({ name: "a", scripts: { echo2: "echo" } }),
|
|
"b/package.json": JSON.stringify({ name: "b", scripts: { echo2: "npm run echo3", echo3: "echo" } }),
|
|
});
|
|
|
|
{
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "run", "root_script", "$HOME (!)", "argument two"].filter(Boolean),
|
|
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('$ bun index.ts "\\$HOME (!)" "argument two"\n');
|
|
expect(stdout).toEndWith("\n$HOME (!)\nargument two\n");
|
|
expect(exitCode).toBe(0);
|
|
}
|
|
{
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "--filter", "*", "echo2", "$HOME (!)", "argument two"].filter(Boolean),
|
|
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.split("\n").sort().join("\n")).toBe(
|
|
[
|
|
"a echo2: $HOME (!) argument two",
|
|
"a echo2: Exited with code 0",
|
|
'b echo2: $ echo "\\$HOME (!)" "argument two"',
|
|
"b echo2: $HOME (!) argument two",
|
|
"b echo2: Exited with code 0",
|
|
"",
|
|
]
|
|
.sort()
|
|
.join("\n"),
|
|
);
|
|
expect(exitCode).toBe(0);
|
|
}
|
|
});
|
|
|
|
const cases = [
|
|
["yarn run", "run"],
|
|
["yarn add", "passthrough"],
|
|
["yarn audit", "passthrough"],
|
|
["yarn -abcd run", "passthrough"],
|
|
["yarn info", "passthrough"],
|
|
["yarn generate-lock-entry", "passthrough"],
|
|
["yarn", "run"],
|
|
["npm run", "run"],
|
|
["npx", "x"],
|
|
["pnpm run", "run"],
|
|
["pnpm dlx", "x"],
|
|
["pnpx", "x"],
|
|
];
|
|
describe("should handle run case", () => {
|
|
for (const ccase of cases) {
|
|
it(ccase[0], async () => {
|
|
using dir = tempDir("test", {
|
|
"package.json": JSON.stringify({
|
|
scripts: {
|
|
"root_script": ` ${ccase[0]} target_script% `,
|
|
"target_script%": " echo target_script ",
|
|
},
|
|
}),
|
|
});
|
|
{
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "root_script"],
|
|
cwd: String(dir),
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
if (ccase[1] === "run") {
|
|
expect(stderr).toMatch(/^\$ bun(-debug)? run target_script% \n\$ echo target_script \n/);
|
|
expect(stdout).toEndWith("target_script\n");
|
|
expect(exitCode).toBe(0);
|
|
} else if (ccase[1] === "x") {
|
|
expect(stderr).toMatch(
|
|
/^\$ bun(-debug)? x target_script% \nerror: unrecognised dependency format: target_script%/,
|
|
);
|
|
expect(exitCode).toBe(1);
|
|
} else {
|
|
expect(stderr).toStartWith(`$ ${ccase[0]} target_script% \n`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
describe("'bun run' priority", async () => {
|
|
// priority:
|
|
// - 1: run script with matching name
|
|
// - 2: load module and run that module
|
|
// - 3: execute a node_modules/.bin/<X> command
|
|
// - 4: ('run' only): execute a system command, like 'ls'
|
|
const dir = tempDirWithFiles("test", {
|
|
"test": { "index.js": "console.log('test/index.js');" },
|
|
"build": { "script.js": "console.log('build/script.js');" },
|
|
"consume": { "index.js": "console.log('consume/index.js');" },
|
|
"index.js": "console.log('index.js')",
|
|
"main.js": "console.log('main.js')",
|
|
"typescript.ts": "console.log('typescript.ts')",
|
|
"sample.js": "console.log('sample.js')",
|
|
"noext": "console.log('noext')",
|
|
"folderandfile": { "index.js": "console.log('folderandfile/index.js')" },
|
|
"folderandfile.js": "console.log('folderandfile.js')",
|
|
"shellscript.sh": "echo shellscript.sh",
|
|
".secretscript.js": "console.log('.secretscript.js')",
|
|
"package.json": JSON.stringify({
|
|
scripts: {
|
|
"build": "echo scripts/build",
|
|
"test": "echo scripts/test",
|
|
"sample.js": "echo scripts/sample.js",
|
|
"§'.js": 'echo "scripts/§\'.js"',
|
|
"test.todo": "echo scripts/test.todo",
|
|
"/absolute": "echo DO_NOT_RUN",
|
|
"./relative": "echo DO_NOT_RUN",
|
|
},
|
|
main: "main.js",
|
|
}),
|
|
"nx.json": JSON.stringify({}),
|
|
"§'.js": 'console.log("§\'.js")',
|
|
"node_modules": {
|
|
".bin": {
|
|
"confabulate": `#!${bunExe()}\nconsole.log("node_modules/.bin/confabulate")`,
|
|
"nx": `#!${bunExe()}\nconsole.log("node_modules/.bin/nx")`,
|
|
},
|
|
},
|
|
"no_run_json.json": JSON.stringify({}),
|
|
});
|
|
chmodSync(dir + "/node_modules/.bin/confabulate", 0o755);
|
|
chmodSync(dir + "/node_modules/.bin/nx", 0o755);
|
|
|
|
const commands: {
|
|
command: string[];
|
|
req_run?: boolean;
|
|
stdout: string;
|
|
stderr?: string | RegExp;
|
|
exitCode?: number;
|
|
}[] = [
|
|
{ command: ["test"], stdout: "scripts/test", stderr: "$ echo scripts/test", req_run: true },
|
|
{ command: ["build"], stdout: "scripts/build", stderr: "$ echo scripts/build", req_run: true },
|
|
{ command: ["consume"], stdout: "consume/index.js", stderr: "" },
|
|
|
|
{ command: ["test/index"], stdout: "test/index.js", stderr: "" },
|
|
{ command: ["test/index.js"], stdout: "test/index.js", stderr: "" },
|
|
{ command: ["build/script"], stdout: "build/script.js", stderr: "" },
|
|
{ command: ["build/script.js"], stdout: "build/script.js", stderr: "" },
|
|
{ command: ["consume/index"], stdout: "consume/index.js", stderr: "" },
|
|
{ command: ["consume/index.js"], stdout: "consume/index.js", stderr: "" },
|
|
|
|
{ command: ["./test"], stdout: "test/index.js", stderr: "" },
|
|
{ command: ["./build"], stdout: "", stderr: /error: Module not found "\.(\/|\\|\\\\)build"|EACCES/, exitCode: 1 },
|
|
{ command: ["./consume"], stdout: "consume/index.js", stderr: "" },
|
|
|
|
{ command: ["index.js"], stdout: "index.js", stderr: "" },
|
|
{ command: ["./index.js"], stdout: "index.js", stderr: "" },
|
|
{ command: ["index"], stdout: "index.js", stderr: "" },
|
|
{ command: ["./index"], stdout: "index.js", stderr: "" },
|
|
|
|
{ command: ["."], stdout: "main.js", stderr: "" },
|
|
{ command: ["./"], stdout: "main.js", stderr: "" },
|
|
|
|
{ command: ["typescript.ts"], stdout: "typescript.ts", stderr: "" },
|
|
{ command: ["./typescript.ts"], stdout: "typescript.ts", stderr: "" },
|
|
{ command: ["typescript.js"], stdout: "typescript.ts", stderr: "" },
|
|
{ command: ["./typescript.js"], stdout: "typescript.ts", stderr: "" },
|
|
{ command: ["typescript"], stdout: "typescript.ts", stderr: "" },
|
|
{ command: ["./typescript"], stdout: "typescript.ts", stderr: "" },
|
|
|
|
{ command: ["sample.js"], stdout: "scripts/sample.js", stderr: "$ echo scripts/sample.js", req_run: true },
|
|
{ command: ["sample.js"], stdout: "sample.js", stderr: "", req_run: false },
|
|
{ command: ["./sample.js"], stdout: "sample.js", stderr: "" },
|
|
{ command: ["sample"], stdout: "sample.js", stderr: "" },
|
|
{ command: ["./sample"], stdout: "sample.js", stderr: "" },
|
|
|
|
{ command: ["test.todo"], stdout: "scripts/test.todo", stderr: "$ echo scripts/test.todo" },
|
|
|
|
{ command: ["§'.js"], stdout: "scripts/§'.js", stderr: '$ echo "scripts/§\'.js"', req_run: true },
|
|
{ command: ["§'.js"], stdout: "§'.js", stderr: "", req_run: false },
|
|
{ command: ["./§'.js"], stdout: "§'.js", stderr: "" },
|
|
{ command: ["§'"], stdout: "§'.js", stderr: "" },
|
|
{ command: ["./§'"], stdout: "§'.js", stderr: "" },
|
|
|
|
{ command: ["noext"], stdout: "noext", stderr: "" },
|
|
{ command: ["./noext"], stdout: "noext", stderr: "" },
|
|
|
|
{ command: ["folderandfile"], stdout: "folderandfile.js", stderr: "" },
|
|
{ command: ["./folderandfile"], stdout: "folderandfile.js", stderr: "" },
|
|
{ command: ["folderandfile.js"], stdout: "folderandfile.js", stderr: "" },
|
|
{ command: ["./folderandfile.js"], stdout: "folderandfile.js", stderr: "" },
|
|
...(isWindows
|
|
? [] // on windows these ones run "folderandfile.js" but the absolute path ones run "folderandfile/index.js"
|
|
: [
|
|
{ command: ["folderandfile/"], stdout: "folderandfile/index.js", stderr: "" },
|
|
{ command: ["./folderandfile/"], stdout: "folderandfile/index.js", stderr: "" },
|
|
]),
|
|
{ command: ["folderandfile/index"], stdout: "folderandfile/index.js", stderr: "" },
|
|
{ command: ["./folderandfile/index"], stdout: "folderandfile/index.js", stderr: "" },
|
|
{ command: ["folderandfile/index.js"], stdout: "folderandfile/index.js", stderr: "" },
|
|
{ command: ["./folderandfile/index.js"], stdout: "folderandfile/index.js", stderr: "" },
|
|
{ command: [dir + "/folderandfile"], stdout: "folderandfile.js", stderr: "" },
|
|
{ command: [dir + "/folderandfile/"], stdout: "folderandfile/index.js", stderr: "" },
|
|
|
|
{ command: ["shellscript.sh"], stdout: "shellscript.sh", stderr: "" },
|
|
{ command: ["./shellscript.sh"], stdout: "shellscript.sh", stderr: "" },
|
|
|
|
{ command: [".secretscript.js"], stdout: ".secretscript.js", stderr: "" },
|
|
{ command: ["./.secretscript"], stdout: ".secretscript.js", stderr: "" },
|
|
{ command: [dir + "/.secretscript"], stdout: ".secretscript.js", stderr: "" },
|
|
|
|
{
|
|
command: ["no_run_json"],
|
|
stdout: "",
|
|
stderr: /error: Cannot run ".*no_run_json\.json"|EACCES/,
|
|
exitCode: 1,
|
|
},
|
|
{
|
|
command: ["no_run_json.json"],
|
|
stdout: "",
|
|
stderr: /error: Cannot run ".*no_run_json\.json"|EACCES/,
|
|
exitCode: 1,
|
|
},
|
|
{
|
|
command: ["./no_run_json"],
|
|
stdout: "",
|
|
stderr: /error: Cannot run ".*no_run_json\.json"|EACCES/,
|
|
exitCode: 1,
|
|
},
|
|
|
|
{
|
|
command: ["/absolute"],
|
|
stdout: "",
|
|
stderr: /error: Module not found "(\/|\\|\\\\)absolute"|EACCES/,
|
|
exitCode: 1,
|
|
},
|
|
{
|
|
command: ["./relative"],
|
|
stdout: "",
|
|
stderr: /error: Module not found ".(\/|\\|\\\\)relative"|EACCES/,
|
|
exitCode: 1,
|
|
},
|
|
|
|
...(isWindows
|
|
? [
|
|
// TODO: node_modules command
|
|
// TODO: system command
|
|
]
|
|
: [
|
|
// node_modules command
|
|
{ command: ["confabulate"], stdout: "node_modules/.bin/confabulate", stderr: "" },
|
|
{ command: ["nx"], stdout: "node_modules/.bin/nx", stderr: "" },
|
|
|
|
// system command
|
|
{ command: ["echo", "abc"], stdout: "abc", stderr: "", req_run: true },
|
|
{ command: ["echo", "abc"], stdout: "", exitCode: 1, req_run: false },
|
|
]),
|
|
|
|
// TODO: test preloads (https://bun.sh/docs/runtime/bunfig#preload), test $npm_lifecycle_event
|
|
// TODO: test with path overrides in tsconfig.json
|
|
];
|
|
if (isWindows) {
|
|
for (const cmd of [...commands]) {
|
|
if (cmd.command[0].includes("/")) {
|
|
commands.push({
|
|
...cmd,
|
|
command: [cmd.command[0].replaceAll("/", "\\"), ...cmd.command.slice(1)],
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const cmd of commands) {
|
|
for (const flag of [[], ["--bun"]]) {
|
|
for (const postflag of cmd.req_run === true ? [["run"]] : cmd.req_run === false ? [[]] : [[], ["run"]]) {
|
|
const full_command = [...flag, ...postflag, ...cmd.command];
|
|
it("bun " + full_command.join(" "), async () => {
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), ...full_command],
|
|
cwd: dir,
|
|
env: { ...bunEnv, BUN_DEBUG_QUIET_LOGS: "1" },
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
if (cmd.stderr != null && typeof cmd.stderr !== "string") expect(stderr).toMatch(cmd.stderr);
|
|
expect({
|
|
...(cmd.stderr != null && typeof cmd.stderr === "string" ? { stderr: stderr.trim() } : {}),
|
|
stdout: stdout.trim(),
|
|
exitCode,
|
|
}).toStrictEqual({
|
|
...(cmd.stderr != null && typeof cmd.stderr === "string" ? { stderr: cmd.stderr } : {}),
|
|
stdout: cmd.stdout,
|
|
exitCode: cmd.exitCode ?? 0,
|
|
});
|
|
});
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
it("should run from stdin", async () => {
|
|
const res = await $`echo "console.log('hello')" | bun run -`.text();
|
|
expect(res).toBe(`hello\n`);
|
|
});
|
|
|
|
describe.todo("run from stdin", async () => {
|
|
// TODO: write this test
|
|
// note limit of around 1gb when running from stdin
|
|
// - which says 'catch return false'
|
|
});
|
|
|
|
describe("should run scripts from the project root (#16169)", async () => {
|
|
const dir = tempDirWithFiles("test", {
|
|
"run_here": {
|
|
"myscript.ts": "console.log('successful run')",
|
|
"package.json": JSON.stringify({
|
|
scripts: { "sample": "pwd", "runscript": "bun myscript.ts" },
|
|
}),
|
|
"dont_run_in_here": {
|
|
"runme.ts": "console.log('do run this script')",
|
|
},
|
|
},
|
|
});
|
|
|
|
it("outside", async () => {
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "run", "sample"],
|
|
cwd: dir + "/run_here",
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
|
|
|
|
expect(stdout).toContain("run_here");
|
|
expect(stdout).not.toContain("dont_run_in_here");
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
it("inside", async () => {
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "run", "sample"],
|
|
cwd: dir + "/run_here/dont_run_in_here",
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
|
|
|
|
expect(stdout).toContain("run_here");
|
|
expect(stdout).not.toContain("dont_run_in_here");
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
it("inside --shell=bun", async () => {
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "--shell=bun", "run", "sample"],
|
|
cwd: dir + "/run_here/dont_run_in_here",
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
|
|
|
|
expect(stdout).toContain("run_here");
|
|
expect(stdout).not.toContain("dont_run_in_here");
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
it("inside script", async () => {
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "run", "runme.ts"],
|
|
cwd: dir + "/run_here/dont_run_in_here",
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
|
|
|
|
expect(stdout).toContain("do run this script");
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
it("inside wrong script", async () => {
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "run", "myscript.ts"],
|
|
cwd: dir + "/run_here/dont_run_in_here",
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
if (stderr.includes("myscript.ts") && stderr.includes("EACCES")) {
|
|
// for some reason on musl, the run_here folder is in $PATH
|
|
// 'error: Failed to run "myscript.ts" due to:\nEACCES: run_here/myscript.ts: Permission denied (posix_spawn())'
|
|
} else {
|
|
expect(stderr).toBe('error: Module not found "myscript.ts"\n');
|
|
}
|
|
expect(exitCode).toBe(1);
|
|
});
|
|
|
|
it("outside 2", async () => {
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "runscript"],
|
|
cwd: dir + "/run_here",
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
|
|
|
|
expect(stdout).toBe("successful run\n");
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
it("inside 2", async () => {
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "runscript"],
|
|
cwd: dir + "/run_here/dont_run_in_here",
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
|
|
|
|
expect(stdout).toBe("successful run\n");
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("run main within monorepo", async () => {
|
|
const dir = tempDirWithFiles("test", {
|
|
"package.json": JSON.stringify({
|
|
name: "monorepo_root",
|
|
main: "monorepo_root.ts",
|
|
workspaces: ["packages/*"],
|
|
}),
|
|
"monorepo_root.ts": "console.log('monorepo_root')",
|
|
"packages": {
|
|
"package_a": {
|
|
"package.json": JSON.stringify({ name: "package_a", main: "package_a.ts" }),
|
|
"package_a.ts": "console.log('package_a')",
|
|
},
|
|
"package_b": {
|
|
"package.json": JSON.stringify({ name: "package_b" }),
|
|
},
|
|
},
|
|
});
|
|
|
|
it("should run main from monorepo root", async () => {
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "."],
|
|
cwd: dir,
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
|
|
|
|
expect(stdout).toBe("monorepo_root\n");
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
it("should run package_a from package_a", async () => {
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "."],
|
|
cwd: dir + "/packages/package_a",
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]);
|
|
|
|
expect(stdout).toBe("package_a\n");
|
|
expect(exitCode).toBe(0);
|
|
});
|
|
|
|
it("should fail from package_b", async () => {
|
|
await using proc = Bun.spawn({
|
|
cmd: [bunExe(), "."],
|
|
cwd: dir + "/packages/package_b",
|
|
env: bunEnv,
|
|
stdout: "pipe",
|
|
stderr: "pipe",
|
|
});
|
|
|
|
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
|
|
|
expect(exitCode).toBe(1);
|
|
});
|
|
});
|
|
});
|