Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
de18af1557 fix(bunx): improve caching behavior and suppress redundant progress messages
Two improvements to bunx:

1. When no version is specified (e.g., `bunx tsc`), don't cache-bust on every
   run. Previously, because the version defaults to the "latest" dist tag,
   `do_cache_bust` was always true, causing `--no-cache --force` to be passed
   to `bun add`. Now we only cache-bust when the user explicitly specifies a
   dist tag like `@latest` or `@beta`.

2. When explicitly specifying a dist tag (e.g., `bunx tsc@latest`), use
   `--silent` by default to suppress progress messages since the package is
   likely already installed at the correct version. Errors are still shown,
   and `--verbose` overrides this behavior.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-05 15:29:30 +00:00
2 changed files with 84 additions and 2 deletions

View File

@@ -544,7 +544,10 @@ pub const BunxCommand = struct {
const passthrough = opts.passthrough_list.items;
var do_cache_bust = update_request.version.tag == .dist_tag;
// Only cache-bust when the user explicitly specifies a dist tag like @latest or @beta.
// When no version is specified (version.literal.isEmpty()), we should use the cached
// version if it's fresh, not force a re-fetch every time.
var do_cache_bust = update_request.version.tag == .dist_tag and !update_request.version.literal.isEmpty();
const look_for_existing_bin = update_request.version.literal.isEmpty() or update_request.version.tag != .dist_tag;
debug("try run existing? {}", .{look_for_existing_bin});
@@ -711,7 +714,7 @@ pub const BunxCommand = struct {
package_json.writeAll("{}\n") catch {};
}
var args = bun.BoundedArray([]const u8, 8).fromSlice(&.{
var args = bun.BoundedArray([]const u8, 9).fromSlice(&.{
try bun.selfExePath(),
"add",
install_param,
@@ -728,6 +731,14 @@ pub const BunxCommand = struct {
// forcefully re-install packages in this mode too
args.append("--force") catch
unreachable; // upper bound is known
// When explicitly specifying a dist tag like @latest, the package is likely
// already installed at the correct version. Use --silent to suppress progress
// messages in this case. Errors will still be shown.
if (!opts.verbose_install and !opts.silent_install) {
args.append("--silent") catch
unreachable; // upper bound is known
}
}
if (opts.verbose_install) {

View File

@@ -793,6 +793,77 @@ console.log("EXECUTED: multi-tool-alt (alternate binary)");
});
});
describe("bunx caching behavior", () => {
const run = async (...args: string[]): Promise<{ err: string; out: string; exited: number }> => {
const subprocess = spawn({
cmd: [bunExe(), "x", ...args],
cwd: x_dir,
stdout: "pipe",
stdin: "inherit",
stderr: "pipe",
env,
});
const [err, out, exited] = await Promise.all([
subprocess.stderr.text(),
subprocess.stdout.text(),
subprocess.exited,
]);
return { err, out, exited };
};
it("should use cached binary on second run without version specified", async () => {
// First run - should install
const first = await run("uglify-js", "-v");
expect(first.err).not.toContain("error:");
expect(first.out).toContain("uglify-js");
expect(first.exited).toBe(0);
// First run should show install progress
expect(first.err).toContain("Saved lockfile");
// Second run - should use cache, no install messages
const second = await run("uglify-js", "-v");
expect(second.err).not.toContain("error:");
expect(second.out).toContain("uglify-js");
expect(second.exited).toBe(0);
// Second run should NOT show install progress
expect(second.err).not.toContain("Resolving");
expect(second.err).not.toContain("Saved lockfile");
});
it("should suppress progress messages when explicitly specifying @latest for already installed package", async () => {
// First run without version - should install and show progress
const first = await run("uglify-js", "-v");
expect(first.err).not.toContain("error:");
expect(first.out).toContain("uglify-js");
expect(first.exited).toBe(0);
// Second run with @latest - should be silent (no progress messages)
const second = await run("uglify-js@latest", "-v");
expect(second.err).not.toContain("error:");
expect(second.out).toContain("uglify-js");
expect(second.exited).toBe(0);
// Explicit @latest should suppress progress messages
expect(second.err).not.toContain("Resolving");
expect(second.err).not.toContain("Saved lockfile");
});
it("should show progress when --verbose is passed with @latest", async () => {
// First install
const first = await run("uglify-js", "-v");
expect(first.exited).toBe(0);
// With --verbose, progress should be shown even with @latest
const verbose = await run("--verbose", "uglify-js@latest", "-v");
expect(verbose.err).not.toContain("error:");
expect(verbose.out).toContain("uglify-js");
expect(verbose.exited).toBe(0);
// --verbose should override silent behavior
expect(verbose.err).toContain("Resolving");
});
});
// Regression test: bunx should not crash on corrupted .bunx files (Windows only)
// When the .bunx metadata file is corrupted (e.g., missing quote terminator in bin_path),
// bunx should gracefully fall back to the slow path instead of panicking.