Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ The format is based on [Keep a Changelog], and this project adheres to [Semantic

### Fixed

- Bun package-exec fallback now invokes the explicit `bun x` subcommand instead
of the `bunx` hard-link alias. On Windows, runner resolves bare executables
through `PATH` and `PATHEXT`; the stock uppercase `.EXE` produced `bunx.EXE`,
which Bun's case-sensitive invocation-name detection misclassified as plain
`bun` and reported `Script not found` for the requested package
(https://github.com/kjanat/runner/issues/103,
https://github.com/oven-sh/bun/issues/36826).
- `install.sh` now completes on Alpine and other busybox userlands. Checksum
verification called `sha256sum -c --status`, and `--status` is GNU-only, so
busybox aborted the install right after downloading the archive
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ And re-read/review the readme...
**runner** is for people who bounce between codebases and refuse to memorize
each repo’s private little task-running religion.

Instead of guessing whether this one wants `npm run`, `pnpm exec`, `bunx`,
Instead of guessing whether this one wants `npm run`, `pnpm exec`, `bun x`,
`cargo`, `uv run`, `deno task`, `turbo`, `make`, `just`, etc. type:

```sh
Expand Down Expand Up @@ -380,7 +380,7 @@ Failing that, it executes `<target>` through the detected toolchain where
appropriate, such as:

```text
npm exec / npx, yarn run / yarn exec, pnpm exec, bun x / bunx,
npm exec / npx, yarn run / yarn exec, pnpm exec, bun x,
deno x, uvx, go run
```

Expand All @@ -401,13 +401,13 @@ manager gets no vote:
| `--runtime` | `package.json` script | local file | ad-hoc binary |
| ----------- | ---------------------- | ----------------- | ------------- |
| `node` | `node --run <task>` | `node <file>` | `npx` |
| `bun` | `bun --bun run <task>` | `bun <file>` | `bunx --bun` |
| `bun` | `bun --bun run <task>` | `bun <file>` | `bun x --bun` |
| `deno` | `deno task <task>` | `deno run <file>` | `deno x` |

```sh
run --runtime bun build # bun --bun run build
run --runtime bun ./cli.js # bun ./cli.js, even with a #!/usr/bin/env node line
run --runtime bun eslint . # bunx --bun eslint .
run --runtime bun eslint . # bun x --bun eslint .
```

`bun run build` starts the script under bun, but a dependency bin carrying a
Expand Down
2 changes: 1 addition & 1 deletion npm/facade/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ runner install --frozen test build
```

Instead of remembering whether this project wants `npm run`, `pnpm exec`,
`bunx`, `cargo`, `uv run`, `deno task`, `make`, `just`, `go-task`, or a monorepo
`bun x`, `cargo`, `uv run`, `deno task`, `make`, `just`, `go-task`, or a monorepo
wrapper, use the same shape everywhere.

## What You Get
Expand Down
2 changes: 1 addition & 1 deletion schemas/doctor.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,7 @@
"const": "node"
},
{
"description": "Bun, via `bun --bun run <script>`, `bun <file>` and `bunx --bun`.",
"description": "Bun, via `bun --bun run <script>`, `bun <file>` and `bun x --bun`.",
"type": "string",
"const": "bun"
},
Expand Down
2 changes: 1 addition & 1 deletion site/src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ <h3>Task sources · 8</h3>
</li>
<li>Monorepo-aware: turbo, nx, pnpm, npm/yarn workspaces, Cargo workspaces.</li>
<li>
No task by that name? Falls through to the package manager's exec primitive, <code>npx</code>, <code>bunx</code>, <code>pnpm exec</code>,
No task by that name? Falls through to the package manager's exec primitive, <code>npx</code>, <code>bun x</code>, <code>pnpm exec</code>,
<code>uv run</code>, etc.
</li>
</ol>
Expand Down
4 changes: 2 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ static RUNTIME_LONG_HELP: LazyLock<String> = LazyLock::new(|| {
cyan_str("node")
),
&format!(
" {} bun --bun run <task> bun <file> bunx --bun",
" {} bun --bun run <task> bun <file> bun x --bun",
cyan_str("bun")
),
&format!(
Expand Down Expand Up @@ -1410,7 +1410,7 @@ pub(crate) struct GlobalOpts {
/// Force the JavaScript runtime a task's process tree runs on, an axis
/// distinct from `--pm`. Each runtime brings its own script runner
/// (`node --run` / `bun --bun run` / `deno task`), file runner and
/// package-exec primitive (`npx` / `bunx --bun` / `deno x`), and outranks
/// package-exec primitive (`npx` / `bun x --bun` / `deno x`), and outranks
/// a local file's `#!` line. No package manager is consulted. The resolver
/// also consults `$RUNNER_RUNTIME` and `[runtime].js` when this flag is
/// omitted.
Expand Down
2 changes: 1 addition & 1 deletion src/cmd/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ use crate::types::ProjectContext;
/// Resolve `task` and run it with inherited stdio, returning the exit
/// code. Bun special case: when `task == "test"` and no package-manifest
/// `test` script exists, falls back to `bun test`. PM-exec fallback for
/// unqualified misses runs the target through `npx`/`bunx`/`pnpm exec`/
/// unqualified misses runs the target through `npx`/`bun x`/`pnpm exec`/
/// `deno x`/`uvx`, plus `go run` for Go module/path-shaped targets;
/// otherwise spawns the binary directly from `PATH`.
pub(crate) fn run(
Expand Down
14 changes: 7 additions & 7 deletions src/cmd/run/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
//! - bun-test special case: `runner test` with no `package.json` script
//! forwards to `bun test` directly;
//! - PM-exec fallback: no task matched, so the token is run through
//! `npx`/`bunx`/`pnpm exec`/`deno x`/`uvx` or spawned from `$PATH`
//! `npx`/`bun x`/`pnpm exec`/`deno x`/`uvx` or spawned from `$PATH`
//! directly when the resolver landed on a PM without an exec primitive.

use std::process::Command;
Expand Down Expand Up @@ -219,7 +219,7 @@ pub(super) fn resolve_dispatch(
// strict devEngines/`packageManager` mismatch, an incompatible
// `--pm`); running `main.ts` via its runtime doesn't need the
// package.json PM. Tasks already matched above (`restricted` is empty
// here), so this never shadows a same-named task, and `bunx`/`npx`
// here), so this never shadows a same-named task, and `bun x`/`npx`
// never sees a local file.
if let Some(local) = super::local_file::try_bare_file(ctx, overrides, task_name, args)?
{
Expand Down Expand Up @@ -249,7 +249,7 @@ pub(super) fn resolve_dispatch(

// Qualified miss (colon or FQN syntax): the qualifier is explicit
// task-lookup intent, so error here, never fall through to
// PM-exec, which would hand the token to bunx/npx as a package
// PM-exec, which would hand the token to bun x/npx as a package
// spec and resolve it off the network.
return Err(qualified_miss_error(ctx, missed_source, task_name));
}
Expand Down Expand Up @@ -336,7 +336,7 @@ fn dispatch_after_miss(
}

/// The exec primitive's argument vector: the token followed by the user's
/// args, as `npx`/`bunx`/`deno x`/`uvx` all take it.
/// args, as `npx`/`bun x`/`deno x`/`uvx` all take it.
fn exec_argv(task_name: &str, args: &[String]) -> Vec<String> {
let mut v = Vec::with_capacity(args.len() + 1);
v.push(task_name.to_string());
Expand All @@ -363,7 +363,7 @@ fn build_pm_exec_command(
Some(PackageManager::Npm) => ("npx", tool::npm::exec_cmd(&combined())),
Some(PackageManager::Yarn) => ("yarn exec", tool::yarn::exec_cmd(&ctx.root, &combined())),
Some(PackageManager::Pnpm) => ("pnpm exec", tool::pnpm::exec_cmd(&combined())),
Some(PackageManager::Bun) => ("bunx", tool::bun::exec_cmd(&combined())),
Some(PackageManager::Bun) => ("bun x", tool::bun::exec_cmd(&combined())),
Some(PackageManager::Deno) => ("deno x", tool::deno::exec_cmd(&combined())),
Some(PackageManager::Uv) => ("uvx", tool::uv::exec_cmd(&combined())),
Some(PackageManager::Go) => {
Expand Down Expand Up @@ -682,7 +682,7 @@ mod tests {
#[test]
fn resolve_dispatch_fqn_miss_errors_instead_of_pm_exec() {
// Previously `run root:package.json#nope` fell through to
// PM-exec and bunx tried to resolve it as a GitHub package spec
// PM-exec and bun x tried to resolve it as a GitHub package spec
// off the network. A `#` FQN miss must be a hard error.
let err = resolve_dispatch(
&context(),
Expand All @@ -699,7 +699,7 @@ mod tests {

#[test]
fn resolve_dispatch_github_spec_still_reaches_pm_exec() {
// `user/repo#ref` is a legit bunx/npx package spec; its prefix
// `user/repo#ref` is a legit bun x/npx package spec; its prefix
// is not a source label, so it must keep flowing to PM-exec.
let dispatch = resolve_dispatch(
&context(),
Expand Down
14 changes: 7 additions & 7 deletions src/cmd/run/local_file.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Local-file execution for `run <path>`.
//!
//! A token that points at a local file should be *run as that file*, never
//! handed to a package manager's package-exec primitive (`bunx`/`npx`/
//! handed to a package manager's package-exec primitive (`bun x`/`npx`/
//! `pnpm dlx`/`deno x`/`uvx`), which would resolve a local path as a remote
//! package spec and fail with a registry 404 or a `git clone` error.
//!
Expand Down Expand Up @@ -1208,9 +1208,9 @@ mod tests {

// A recognized source file at mode 0111 (execute-only). The shebang
// probe hits EACCES, but that must NOT propagate as an error (which
// `bare_file_in`'s `.ok()` would swallow into a `bunx main.ts` 404).
// `bare_file_in`'s `.ok()` would swallow into a `bun x main.ts` 404).
// It routes through the detected runtime instead, upholding the
// no-bunx-on-a-local-file invariant.
// no-bun-x-on-a-local-file invariant.
let dir = TempDir::new("local-exec-only-ts");
let file = dir.path().join("main.ts");
std::fs::write(&file, "console.log(1)\n").expect("file should be written");
Expand All @@ -1227,7 +1227,7 @@ mod tests {
)
.expect("an execute-only source file should route to its runtime");

assert_eq!(label, "bun", "a 0111 .ts routes to bun, never bunx");
assert_eq!(label, "bun", "a 0111 .ts routes to bun, never bun x");
assert_eq!(command.get_program().to_string_lossy(), "bun");
}

Expand Down Expand Up @@ -1552,7 +1552,7 @@ mod tests {
#[test]
fn bare_file_missing_token_falls_through_to_pm_exec() {
// A bare token with no matching file on disk is a real package-spec
// candidate (`runner biome` → `bunx biome`), so it keeps falling
// candidate (`runner biome` → `bun x biome`), so it keeps falling
// through to the PM-exec fallback rather than erroring.
let dir = TempDir::new("bare-file-miss");
let ctx = context(vec![PackageManager::Bun]);
Expand All @@ -1571,7 +1571,7 @@ mod tests {
// Once the token names an existing regular file it is a local file: an
// unsupported one (`data.bin`, no extension we run, no shebang, not
// executable) surfaces the clear local-file error instead of falling
// through to `bunx data.bin` and a registry 404, matching the explicit
// through to `bun x data.bin` and a registry 404, matching the explicit
// `./data.bin` form (#69).
let dir = TempDir::new("bare-file-unsupported");
std::fs::write(dir.path().join("data.bin"), [0u8, 1, 2]).expect("file should be written");
Expand Down Expand Up @@ -1678,7 +1678,7 @@ mod tests {
// dir / `--dir` target), not the live process cwd; a `main.ts` under
// the project root runs even when the shell cwd is elsewhere. This is
// what stops a `--dir`-set run from missing the file and mis-routing
// into the `bunx main.ts` 404 fallback (issue #69).
// into the `bun x main.ts` 404 fallback (issue #69).
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
let dir = TempDir::new("bare-root");
std::fs::write(dir.path().join("main.ts"), "console.log(1)\n")
.expect("file should be written");
Expand Down
4 changes: 2 additions & 2 deletions src/cmd/run/qualify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ pub(super) fn parse_fqn_task(input: &str) -> Option<(TaskSource, &str)> {
/// A `Some` qualifier, whether from colon or FQN syntax, pins the
/// lookup to that source; the caller errors on a miss instead of
/// falling through to PM-exec. That property is what keeps an FQN typo
/// (`root:package.json#nope`) from being handed to bunx/npx as a
/// (`root:package.json#nope`) from being handed to bun x/npx as a
/// package spec and resolved off the network.
pub(crate) struct TokenLookup<'a> {
/// Pinned source from `source:task` or FQN (`root:source#task`) syntax.
Expand Down Expand Up @@ -388,7 +388,7 @@ mod tests {

#[test]
fn parse_fqn_task_leaves_package_specs_alone() {
// bunx/npx GitHub specs share the `#` separator; anything whose
// bun x/npx GitHub specs share the `#` separator; anything whose
// prefix isn't a source label must keep flowing to PM-exec.
assert_eq!(parse_fqn_task("user/repo#ref"), None);
assert_eq!(parse_fqn_task("root:unknown#x"), None);
Expand Down
28 changes: 20 additions & 8 deletions src/cmd/run/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
//! | runtime | script | file | exec |
//! |---------|-------------------------|-----------------|--------------|
//! | node | `node --run <task> --` | `node <file>` | `npx` |
//! | bun | `bun --bun run <task>` | `bun <file>` | `bunx --bun` |
//! | bun | `bun --bun run <task>` | `bun <file>` | `bun x --bun` |
//! | deno | `deno task <task>` | `deno run <file>` | `deno x` |
//!
//! Argument forwarding differs per runtime and is not interchangeable.
Expand Down Expand Up @@ -50,14 +50,14 @@ pub(super) fn script_cmd(
pub(super) fn exec_cmd(runtime: JsRuntime, argv: &[String]) -> (&'static str, Command) {
match runtime {
JsRuntime::Node => ("npx", tool::npm::exec_cmd(argv)),
JsRuntime::Bun => ("bunx", tool::bun::exec_cmd_with_runtime(argv, true)),
JsRuntime::Bun => ("bun x", tool::bun::exec_cmd_with_runtime(argv, true)),
JsRuntime::Deno => ("deno x", tool::deno::exec_cmd(argv)),
}
}

/// Whether the runtime axis replaces the exec primitive the resolver picked.
///
/// It replaces a JS one (`npx`, `yarn exec`, `pnpm exec`, `bunx`, `deno x`)
/// It replaces a JS one (`npx`, `yarn exec`, `pnpm exec`, `bun x`, `deno x`)
/// and a resolver that found nothing at all. A Python/Go/Rust/Ruby/PHP
/// project's exec primitive is left alone: there is no JS process to move.
pub(super) fn replaces_exec(resolved_pm: Option<PackageManager>) -> bool {
Expand Down Expand Up @@ -205,6 +205,18 @@ mod tests {
.collect()
}

fn assert_program(cmd: &std::process::Command, expected: &str) {
let stem = std::path::Path::new(cmd.get_program())
.file_stem()
.expect("runtime command should have a file stem")
.to_string_lossy();
assert!(
stem.eq_ignore_ascii_case(expected),
"expected {expected} executable, got {:?}",
cmd.get_program()
);
}

fn script(runtime: JsRuntime, args: &[String]) -> (String, Vec<String>) {
let cmd = script_cmd(runtime, "build", args, HostVerbosity::default());
(cmd.get_program().to_string_lossy().into_owned(), argv(&cmd))
Expand Down Expand Up @@ -245,17 +257,17 @@ mod tests {

let (label, cmd) = exec_cmd(JsRuntime::Node, &argv_in);
assert_eq!(label, "npx");
assert_eq!(cmd.get_program().to_string_lossy(), "npx");
assert_program(&cmd, "npx");
assert_eq!(argv(&cmd), ["eslint", "."]);

let (label, cmd) = exec_cmd(JsRuntime::Bun, &argv_in);
assert_eq!(label, "bunx");
assert_eq!(cmd.get_program().to_string_lossy(), "bunx");
assert_eq!(argv(&cmd), ["--bun", "eslint", "."]);
assert_eq!(label, "bun x");
assert_program(&cmd, "bun");
assert_eq!(argv(&cmd), ["x", "--bun", "eslint", "."]);

let (label, cmd) = exec_cmd(JsRuntime::Deno, &argv_in);
assert_eq!(label, "deno x");
assert_eq!(cmd.get_program().to_string_lossy(), "deno");
assert_program(&cmd, "deno");
assert_eq!(argv(&cmd), ["x", "eslint", "."]);
}

Expand Down
Loading