Skip to content

cli: report Cannot run for a path to a data file, for every spelling (bun ./x.css) - #43480

Open
robobun wants to merge 4 commits into
mainfrom
robobun/c050f633/explicit-path-unrunnable-loader
Open

robobun wants to merge 4 commits into
mainfrom
robobun/c050f633/explicit-path-unrunnable-loader

Conversation

@robobun

@robobun robobun commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • bun ./x.css and bun /abs/x.css print nothing and exit 0. bun x.css prints error: Cannot run "/tmp/norun/x.css" and note: Bun cannot run css files directly, then exits 1. Every data loader does the same.
  • The cause is maybe_open_with_bun_js (src/runtime/cli/run_command.rs), the fast path for a target that starts with . or is absolute. It boots every existing file. The resolve block of exec_with_cfg checks the loader first. Fix bun run folder #15117 (Bun 1.2.0) dropped that check here.
  • bun sub/tool.css executes the file as a binary if it has the executable bit. On Windows it fails with EFTYPE from uv_spawn.

Fix

  • Both paths share entry_point_loader (the lookup order of the VM's loader_for_path) and can_run_entry_point. The fast path reports through the shared failure tail nothing_ran and passes its loader on.
  • With a preload, the fast path boots as before, because a plugin can turn the file into code. test/cli/run/preload-test.test.js covers that and is no longer a todo.
  • The binary lookup does not execute a data file that a target with a directory names.
  • Verified: test/cli/install/bun-run.test.ts on Linux and Windows x64. 26 new tests fail on the released binary.

Background

  • RunCommand::exec_with_cfg handles bun <target> and bun run <target>. It tries the fast path, a package.json script, module resolution, then a binary.
  • A loader says how Bun reads a file. can_be_run_by_bun() covers JS, TS, JSX, TSX, wasm and shell. HTML starts the dev server and Markdown renders. Every other loader only exports data.
  • A preload (--preload, bunfig preload) runs before the entry point. It can register a plugin whose onLoad replaces the contents of a file.
Notes

Before and after (Linux x64, files in /tmp/norun, output and exit code, no preload)

command 1.1.43 1.2.0 1.3.5 1.4.3-canary (367d939) this PR
bun x.css File not found, 1 nothing, 0 File not found, 1 Cannot run, 1 Cannot run, 1
bun ./x.css File not found, 1 nothing, 0 nothing, 0 nothing, 0 Cannot run, 1
bun x.json nothing, 0 nothing, 0 Module not found, 1 Cannot run, 1 Cannot run, 1
bun ./x.json nothing, 0 nothing, 0 nothing, 0 nothing, 0 Cannot run, 1
bun sub/tool.css (executable bit) runs the file Cannot run, 1
bun sub\style.css (Windows) EFTYPE ... (uv_spawn()), 1 Cannot run, 1

History

What the review of the first version changed

The first version refused every data file on the fast path. The review found these cases, and each now has a test:

  • A preload plugin. bun --preload ./plugin.ts ./entry.yaml runs the plugin's code on the base, and test/cli/run/preload-test.test.js ("as entry point") documents that for a .txt file. The first version refused it. Now the fast path refuses only when there is no preload.
  • ".sh" = "sh" in bunfig, or --loader .sh:sh. Both store the sh loader as the file loader, so the check refused bun ./script.sh. boot runs every .sh path in the Bun shell, so can_run_entry_point accepts a .sh path first. This also repairs bun run script.sh with that mapping, which the base refuses with Bun cannot run file files directly.
  • An extension mapped to or from md. The fast path checked one loader and booted with none. With ".txt" = "md", bun ./notes.txt printed nothing and bun notes.txt rendered. With ".md" = "ts", bun ./x.md rendered the source and bun run x.md ran it. boot_and_handle_error now takes the loader from its caller, and its fallback to the default loader is gone.
  • X.CSS, data.sqlite, note.file. The VM also reads an extension in upper case, and a loader name as an extension. The CLI did not, so both spellings printed nothing. entry_point_loader now ends with Loader::from_string, as loader_for_path does. README.MD now renders.
  • bun sub/tool.css. For a target with a directory, which() does not search $PATH. It looks at that path only. The lookup now skips a file that can_run_entry_point refuses. A bare name still goes to node_modules/.bin (bun nx next to nx.json), and on Windows bun run sub/tool still runs sub\tool.cmd next to sub\tool.json.

Known gaps that this PR does not close

  1. With a preload and no plugin that takes the file, bun ./x.css still prints nothing and exits 0. Only the VM knows whether a plugin takes the entry point, so a report for that case needs a check in the module loader.
  2. bun run --loader .txt:ts ./code.txt in a project whose bunfig has a [loader] table reports Bun cannot run text files directly. On the base it prints nothing. In both cases the cause is that the late bunfig load replaces the --loader flags, so the VM gets the text loader too. bunfig: keep CLI flags ahead of bunfig.toml when it is loaded after argv (bun run) #38599 fixes that load order.
  3. The node shim (exec_as_if_node) still prints nothing and exits 0 for node ./x.css. Node runs .json and .node entry points and parses every other extension as JavaScript, so the Cannot run rule does not fit there as it is. Make the entry point of the node shim the main module #43409 also moves the resolve of the shim's entry point, and a loader check belongs after that resolve.
  4. bun ./X.HTML now reaches the HTML entry point code, which accepts only a .html name and throws No HTML files found matching. An extension that bunfig maps to html does the same on the base. Before, X.HTML printed nothing.

What else changes

  • bun words.ts (no ./) also takes the fast path, because .ts runs by default. With ".ts" = "text" in bunfig it printed nothing and exited 0, and bun run words.ts reported Cannot run. Now both report it.
  • bun ./addon.node reports Bun cannot run napi files directly, as bun addon.node already does. Before, it loaded the addon and exited 0.
  • bun --watch ./x.css and bun --hot ./x.css report the file and exit 1, as bun --watch x.css already does.

What does not change

  • bun ./page.html, bun ./README.md, bun ./mod.wasm, bun ./script.sh, a file without an extension, and a file with an unknown extension.
  • A directory (bun ./dir), a missing file, and --if-present. bun --if-present ./x.css exits 0 with no output before and after, like bun --if-present x.css.
  • The loader that the resolve path finds for a configured or default extension. entry_point_loader reads ctx.args.loaders, which configure_env_for_run clones into the transpiler whose map the old code read. Both take the last entry for an extension.

Why the fast path does not fall through to the resolve path

The resolve path would find the same file and print the same message. But the fast path is for explicit paths, which the docs call source files, and the lookups after the resolve step are for names. nothing_ran gives the same message, exit code and --if-present behavior without them.

Tests

  • New rows in the 'bun run' priority table: ./no_run_json.json and the absolute path, each with bun, bun run, bun --bun and bun --bun run. All 8 fail on the released binary.
  • New block "a path to an existing file that Bun cannot run": each spelling (x, ./x, absolute, ../x, and .\x on Windows) with and without run, the loader name in the message for six targets, --if-present, a bunfig [loader] entry in both directions, --loader in both directions and with a repeated extension, the sh mapping, the md mappings, a preload plugin (flag and bunfig), a symlink in both directions, an executable file with and without a directory, and the Windows .cmd case. 18 of 22 fail on the released binary on Linux. The 4 that pass pin behavior that must not change: the spelling without ./, --if-present, and the preload plugin.
  • test/cli/run/preload-test.test.js: "as entry point > works from CLI" passes on the base and with this PR, on Linux and Windows, so it is a test again. The first version of this PR failed it.
  • Linux x64 debug build: test/cli/install/bun-run.test.ts 326 pass. Windows x64 debug build: 447 pass.
  • Also run on the Linux debug build: test/regression/issue/1365.test.ts, test/cli/run/run_command.test.ts, if-present.test.ts, markdown-entrypoint.test.ts, run-extensionless.test.ts, run-shell.test.ts, preload-test.test.js, test/cli/install/bun-run-bunfig.test.ts, test/js/bun/http/bun-serve-html-entry.test.ts, and the Rust source lints.

no test proof · iteration 2 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/install/bun-run.test.ts

`bun ./x.css`, `bun run ./x.css` and an absolute path take the fast path
`maybe_open_with_bun_js`, which booted every existing file. A file with a
css, text, toml, yaml or json loader was imported as a module. Nothing
ran, and the exit code was 0. `bun x.css` goes through the resolver and
reports `Cannot run`.

The fast path now applies the same loader check as the resolve path,
with the same loader lookup (`--loader`, bunfig `[loader]`, then the
default), and reports the file through the same failure tail.
@coderabbitai

coderabbitai Bot commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review paused — included plan limit reached

Keep your review moving with free on-demand reviews.

  • Run this review for free

On-demand reviews are free for one more day.

  • Ask an admin to make reviews automatic

Open in CodeRabbit

Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing.

Promotion and pricing details

On-demand reviews are free for one more day. After that, they cost $0.25 per reviewed file.

Review limit details

Or wait 4 minutes for your next included review.

Check out review usage here.

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: oven-sh/bun/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 0c1c85d0-4b1e-480f-a763-139de46e3e4a

📥 Commits

Reviewing files that changed from the base of the PR and between 26e7a4b and 98bf8f3.

📒 Files selected for processing (3)
  • src/runtime/cli/run_command.rs
  • test/cli/install/bun-run.test.ts
  • test/cli/run/preload-test.test.js

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 3:52 PM PT - Sep 19th, 2026

✅ @robobun, your commit 98bf8f39631221f68175ff7ed9343e284bab4d5a passed in Build #118543! 🎉


🧪   To try this PR locally:

bunx bun-pr 43480

That installs a local version of the PR into your bun-43480 executable, so you can run:

bun-43480 --bun

@robobun

robobun commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status

Reproduction (Linux x64, Bun 1.4.3-canary.1+367d939d9):

mkdir -p /tmp/norun && cd /tmp/norun
echo 'body{color:red}' > x.css
bun x.css     # error: Cannot run "/tmp/norun/x.css", exit 1
bun ./x.css   # prints nothing, exit 0

With this PR, bun ./x.css prints the same error and exits 1.

Test proof:

USE_SYSTEM_BUN=1 bun test test/cli/install/bun-run.test.ts -t "cannot run|no_run_json"   # 26 fail, 16 pass
bun bd test test/cli/install/bun-run.test.ts -t "cannot run|no_run_json"                  # 42 pass

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline findings, I also checked the log_errors == false branch of the new nothing_ran helper, which returns false and lets maybe_open_with_bun_js fall through to script/binary lookup for an unrunnable file — it is unreachable today because src/runtime/cli/mod.rs:1466 only sets log_errors: false together with --if-present, and nothing_ran returns true for --if-present first.

Extended reasoning...

Findings are already posted inline (loader-override cases such as sh and md remaps, the --loader flag precedence, and the resolve-path which() fall-through), and two further verified findings are not posted, so approval is off the table. The one extra thing I traced myself was whether nothing_ran returning false when log_errors is false could let the fast path fall through and execute an executable data file via which(); every caller except mod.rs:1466 passes log_errors: true, and that site only passes false when --if-present is set, which nothing_ran short-circuits to true before reaching the log_errors check. So that path is currently dead rather than a live bug, though it is fragile if a future caller passes false for another reason.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟣 src/runtime/cli/run_command.rs — pre-existing: a user running bun run sub/tool.css still has the data file executed as a binary (or gets "Failed to run"), while bun run ./sub/tool.css now gets "Cannot run". The resolve step records the unrunnable file at run_command.rs:2587 but then still calls which() at run_command.rs:2663. On POSIX which() returns any target containing / that has the executable bit; on Windows which_win returns any target spelled with a directory and an extension. Fix: when the resolve step found an existing file Bun cannot run and the target contains a path separator, go straight to nothing_ran and skip the binary lookup, keeping the PATH search for bare names. The new test only covers the ./tool.css spelling. [also at: src/runtime/cli/run_command.rs:2588 - pre-existing: a user running bun run sub/tool.css (a relative path with a directory but no leading ./) still has the data file executed as a shell binary when it has the executable bit, while bun ./tool.css now correctly reports Cannot run.]

    Extended reasoning...

    The PR description itself cites chmod +x sub/tool.css && bun sub/tool.css executing the file as the reason the fast path must not fall through, but leaves that spelling on the resolve path unchanged. Trigger: a target with a directory component that does not start with . and is not absolute, e.g. bun run sub/tool.css, bun run src/config.json, on a file with the executable bit (common for whole trees on WSL /mnt/c, FAT/NTFS or some bind mounts, where every file reports X_OK). exec_with_cfg: try_fast_run stays false at run_command.rs:2340-2349 because the name starts with s and .css/.json has no runnable default loader. Script lookup finds nothing. Resolver at run_command.rs:2547 fails for sub/tool.css as a package path, the ./ retry at run_command.rs:2555 resolves it. entry_point_loader returns Css, can_run_entry_point is false, so…

    Verification: pre-existing. acknowledged in diff: the PR description's "Why not fall through to the resolve path" section states chmod +x sub/tool.css && bun sub/tool.css executes the file today and that which_win accepts a dir+extension spelling on Windows; that claim is accurate, but the PR only guards the fast path and leaves this route intact. Trigger: a target with a directory component that does…

Comment thread src/runtime/cli/run_command.rs Outdated
Comment thread src/runtime/cli/run_command.rs
Comment thread src/runtime/cli/run_command.rs Outdated
Comment thread src/runtime/cli/run_command.rs
- A `.sh` entry point runs in the Bun shell whatever its loader is, as
  `boot` decides. `".sh" = "sh"` in bunfig and `--loader .sh:sh` arrive
  as the `file` loader, so the loader check refused the script.
- The fast path passes the loader it checked to `boot_and_handle_error`,
  as the resolve path does. A file mapped to `md` renders and a `.md`
  file mapped to `ts` runs, for every spelling.
- `entry_point_loader` reads the extension the way the VM's
  `loader_for_path` does: also as a loader name, in any case. `X.CSS` and
  `data.sqlite` report `Cannot run`.
- For a target with a directory, the binary lookup does not execute a
  file that Bun cannot run. `bun sub/tool.css` executed the file if it had
  the executable bit. On Windows it failed with EFTYPE from uv_spawn.
Comment thread src/runtime/cli/run_command.rs Outdated
Comment thread src/runtime/cli/run_command.rs Outdated
Comment thread src/runtime/cli/run_command.rs Outdated
Comment thread src/runtime/cli/run_command.rs Outdated
Comment thread src/runtime/cli/run_command.rs Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still open from earlier reviews (1):

  • Unresolved: 1 minor or pre-existing.

Comment thread src/runtime/cli/run_command.rs Outdated
- With a preload, the fast path boots an explicit path to a data file as
  before. A plugin from the preload can turn the file into code, and
  only the VM knows that. `test/cli/run/preload-test.test.js` documents
  this for a `.txt` entry point. That test passes, so it is no longer a
  todo.
- Every comment that this branch adds in src/ is one line.
@robobun robobun changed the title cli: report Cannot run for an explicit path to a data file (bun ./x.css) cli: report Cannot run for a path to a data file, for every spelling (bun ./x.css) Sep 19, 2026
@robobun

robobun commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

Reply to the review findings that have no open thread:

  • .sh mapped to the sh loader: confirmed, a regression from the first commit. Fixed in f3bf1f8. can_run_entry_point accepts a .sh path first, as boot does. This also repairs bun run script.sh with that mapping, which the base refuses with Bun cannot run file files directly. Test: "runs a .sh file whose extension is mapped to the sh loader".
  • An extension mapped to md: fixed in f3bf1f8. The fast path passes the loader that it checked to boot_and_handle_error, and the fallback to the default loader is gone. Test: "renders or runs a file by its configured loader, for every spelling".
  • X.CSS, data.sqlite, note.file: fixed in f3bf1f8. entry_point_loader ends with Loader::from_string, in the order of loader_for_path. Tests: the UPPER.CSS and data.sqlite rows.
  • bun run sub/tool.css: confirmed on Linux (the file runs) and on Windows (EFTYPE from uv_spawn). Fixed in f3bf1f8. For a target with a directory, the binary lookup skips a file that can_run_entry_point refuses. A bare name still goes to node_modules/.bin, and on Windows bun run sub/tool still runs sub\tool.cmd next to sub\tool.json. Tests: "does not execute the file as a binary" and "runs the .cmd file next to a data file of the same name".
  • nothing_ran with log_errors == false: agreed, not reachable today. exec_auto_or_run passes false only with --if-present, and nothing_ran returns true for that first. I left it as it is: the result is then Ok(false), as on the base.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Comment thread src/runtime/cli/run_command.rs

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant