-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(cli/bunx): support
--no-install
flag (#16315)
- Loading branch information
1 parent
5104220
commit 258aee7
Showing
3 changed files
with
119 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
const std = @import("std"); | ||
const bun = @import("root").bun; | ||
const string = bun.string; | ||
const Allocator = std.mem.Allocator; | ||
const Output = bun.Output; | ||
const Global = bun.Global; | ||
const Environment = bun.Environment; | ||
|
@@ -8,11 +10,11 @@ const MutableString = bun.MutableString; | |
const stringZ = bun.stringZ; | ||
const default_allocator = bun.default_allocator; | ||
const C = bun.C; | ||
const std = @import("std"); | ||
const cli = @import("../cli.zig"); | ||
|
||
const Command = cli.Command; | ||
const Run = @import("./run_command.zig").RunCommand; | ||
const Allocator = std.mem.Allocator; | ||
const UpdateRequest = bun.PackageManager.UpdateRequest; | ||
|
||
const debug = Output.scoped(.bunx, false); | ||
|
||
|
@@ -30,6 +32,9 @@ pub const BunxCommand = struct { | |
// purposes. | ||
verbose_install: bool = false, | ||
silent_install: bool = false, | ||
/// Skip installing the package, only running the target command if its | ||
/// already downloaded. If its not, `bunx` exits with an error. | ||
no_install: bool = false, | ||
allocator: Allocator, | ||
|
||
/// Create a new `Options` instance by parsing CLI arguments. `ctx` may be mutated. | ||
|
@@ -65,6 +70,8 @@ pub const BunxCommand = struct { | |
opts.silent_install = true; | ||
} else if (strings.eqlComptime(positional, "--bun") or strings.eqlComptime(positional, "-b")) { | ||
ctx.debug.run_in_bun = true; | ||
} else if (strings.eqlComptime(positional, "--no-install")) { | ||
opts.no_install = true; | ||
} | ||
} else { | ||
if (!found_subcommand_name) { | ||
|
@@ -298,9 +305,9 @@ pub const BunxCommand = struct { | |
var opts = try Options.parse(ctx, argv); | ||
defer opts.deinit(); | ||
|
||
var requests_buf = bun.PackageManager.UpdateRequest.Array.initCapacity(ctx.allocator, 64) catch bun.outOfMemory(); | ||
var requests_buf = UpdateRequest.Array.initCapacity(ctx.allocator, 64) catch bun.outOfMemory(); | ||
defer requests_buf.deinit(ctx.allocator); | ||
const update_requests = bun.PackageManager.UpdateRequest.parse( | ||
const update_requests = UpdateRequest.parse( | ||
ctx.allocator, | ||
null, | ||
ctx.log, | ||
|
@@ -335,6 +342,7 @@ pub const BunxCommand = struct { | |
|
||
// fast path: they're actually using this interchangeably with `bun run` | ||
// so we use Bun.which to check | ||
// SAFETY: initialized by Run.configureEnvForRun | ||
var this_transpiler: bun.Transpiler = undefined; | ||
var ORIGINAL_PATH: string = ""; | ||
|
||
|
@@ -384,7 +392,7 @@ pub const BunxCommand = struct { | |
else => ":", | ||
}; | ||
|
||
const has_banned_char = bun.strings.indexAnyComptime(update_request.name, banned_path_chars) != null or bun.strings.indexAnyComptime(display_version, banned_path_chars) != null; | ||
const has_banned_char = strings.indexAnyComptime(update_request.name, banned_path_chars) != null or strings.indexAnyComptime(display_version, banned_path_chars) != null; | ||
|
||
break :brk try if (has_banned_char) | ||
// This branch gets hit usually when a URL is requested as the package | ||
|
@@ -503,8 +511,10 @@ pub const BunxCommand = struct { | |
const passthrough = opts.passthrough_list.items; | ||
|
||
var do_cache_bust = update_request.version.tag == .dist_tag; | ||
const look_for_existing_bin = update_request.version.literal.isEmpty() or update_request.version.tag != .dist_tag; | ||
|
||
if (update_request.version.literal.isEmpty() or update_request.version.tag != .dist_tag) try_run_existing: { | ||
debug("try run existing? {}", .{look_for_existing_bin}); | ||
if (look_for_existing_bin) try_run_existing: { | ||
var destination_: ?[:0]const u8 = null; | ||
|
||
// Only use the system-installed version if there is no version specified | ||
|
@@ -563,11 +573,17 @@ pub const BunxCommand = struct { | |
}; | ||
|
||
if (is_stale) { | ||
debug("found stale binary: {s}", .{out}); | ||
do_cache_bust = true; | ||
break :try_run_existing; | ||
if (opts.no_install) { | ||
Output.warn("Using a stale installation of <b>{s}<r> because --no-install was passed. Run `bunx` without --no-install to use a fresh binary.", .{update_request.name}); | ||
} else { | ||
break :try_run_existing; | ||
} | ||
} | ||
} | ||
|
||
debug("running existing binary: {s}", .{destination}); | ||
try Run.runBinary( | ||
ctx, | ||
try this_transpiler.fs.dirname_store.append(@TypeOf(out), out), | ||
|
@@ -626,6 +642,24 @@ pub const BunxCommand = struct { | |
} | ||
} | ||
} | ||
// If we've reached this point, it means we couldn't find an existing binary to run. | ||
// Next step is to install, then run it. | ||
|
||
// NOTE: npx prints errors like this: | ||
// | ||
// npm error npx canceled due to missing packages and no YES option: ["[email protected]"] | ||
// npm error A complete log of this run can be found in: [folder]/debug.log | ||
// | ||
// Which is not very helpful. | ||
|
||
if (opts.no_install) { | ||
Output.errGeneric( | ||
"Could not find an existing '{s}' binary to run. Stopping because --no-install was passed.", | ||
.{initial_bin_name}, | ||
); | ||
Global.exit(1); | ||
} | ||
|
||
const bunx_install_dir = try std.fs.cwd().makeOpenPath(bunx_cache_dir, .{}); | ||
|
||
create_package_json: { | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
import { spawn } from "bun"; | ||
import { beforeAll, beforeEach, expect, it, setDefaultTimeout } from "bun:test"; | ||
import { describe, beforeAll, beforeEach, expect, it, setDefaultTimeout } from "bun:test"; | ||
import { rm, writeFile } from "fs/promises"; | ||
import { bunEnv, bunExe, isWindows, tmpdirSync, readdirSorted } from "harness"; | ||
import { readdirSync } from "node:fs"; | ||
|
@@ -83,7 +83,7 @@ it("should choose the tagged versions instead of the PATH versions when a tag is | |
cwd: x_dir, | ||
stdout: "pipe", | ||
stdin: "ignore", | ||
stderr: "inherit", | ||
stderr: "ignore", | ||
env: { | ||
...env, | ||
// BUN_DEBUG_QUIET_LOGS: undefined, | ||
|
@@ -280,7 +280,7 @@ it("should work for github repository with committish", async () => { | |
|
||
// cached | ||
const cached = spawn({ | ||
cmd: [bunExe(), "x", "github:piuccio/cowsay#HEAD", "hello bun!"], | ||
cmd: [bunExe(), "x", "--no-install", "github:piuccio/cowsay#HEAD", "hello bun!"], | ||
cwd: x_dir, | ||
stdout: "pipe", | ||
stdin: "inherit", | ||
|
@@ -396,3 +396,77 @@ it('should set "npm_config_user_agent" to bun', async () => { | |
expect(out.trim()).toContain(`bun/${Bun.version}`); | ||
expect(exited).toBe(0); | ||
}); | ||
|
||
/** | ||
* IMPORTANT | ||
* Please only use packages with small unpacked sizes for tests. It helps keep them fast. | ||
*/ | ||
describe("bunx --no-install", () => { | ||
const run = (...args: string[]): Promise<[stderr: string, stdout: string, exitCode: number]> => { | ||
const subprocess = spawn({ | ||
cmd: [bunExe(), "x", ...args], | ||
cwd: x_dir, | ||
env: bunEnv, | ||
stdout: "pipe", | ||
stderr: "pipe", | ||
}); | ||
|
||
return Promise.all([ | ||
new Response(subprocess.stderr).text(), | ||
new Response(subprocess.stdout).text(), | ||
subprocess.exited, | ||
] as const); | ||
}; | ||
|
||
it("if the package is not installed, it should fail and print an error message", async () => { | ||
const [err, out, exited] = await run("--no-install", "http-server", "--version"); | ||
|
||
expect(err.trim()).toContain( | ||
"Could not find an existing 'http-server' binary to run.", | ||
); | ||
expect(out).toHaveLength(0); | ||
expect(exited).toBe(1); | ||
}); | ||
|
||
/* | ||
yes, multiple package tests are neccessary. | ||
1. there's specialized logic for `bunx tsc` and `bunx typescript` | ||
2. http-server checks for non-alphanumeric edge cases. Plus it's small | ||
3. eslint is alphanumeric and extremely common | ||
*/ | ||
it.each(["typescript", "http-server", "eslint"])("`bunx --no-install %s` should find cached packages", async pkg => { | ||
// not cached | ||
{ | ||
const [err, out, code] = await run(pkg, "--version"); | ||
expect(err).not.toContain("error:"); | ||
expect(out).not.toBeEmpty(); | ||
expect(code).toBe(0); | ||
} | ||
|
||
// cached | ||
{ | ||
const [err, out, code] = await run("--no-install", pkg, "--version"); | ||
expect(err).not.toContain("error:"); | ||
expect(out).not.toBeEmpty(); | ||
expect(code).toBe(0); | ||
} | ||
}); | ||
|
||
it("when an exact version match is found, should find cached packages", async () => { | ||
// not cached | ||
{ | ||
const [err, out, code] = await run("[email protected]", "--version"); | ||
expect(err).not.toContain("error:"); | ||
expect(out).not.toBeEmpty(); | ||
expect(code).toBe(0); | ||
} | ||
|
||
// cached | ||
{ | ||
const [err, out, code] = await run("--no-install", "[email protected]", "--version"); | ||
expect(err).not.toContain("error:"); | ||
expect(out).not.toBeEmpty(); | ||
expect(code).toBe(0); | ||
} | ||
}); | ||
}); |