Compare commits

...

2 Commits

Author SHA1 Message Date
Meghan Denny
564fd9e19e Merge branch 'main' into nektro-patch-24869 2024-10-09 15:48:13 -07:00
Meghan Denny
6525282cf2 Implement 'bun run-script' 2024-10-08 20:23:56 -07:00
4 changed files with 311 additions and 4 deletions

36
docs/cli/run-script.md Normal file
View File

@@ -0,0 +1,36 @@
The `bun` CLI can be used to execute `package.json` scripts and much more.
The CLI options for this subcommand is identical to [`bun run`](https://bun.sh/docs/cli/run) but is specifically catered to running `package.json` scripts.
This specificity may be useful since if used in a directory with a file or folder that shares a name with a `package.json` script, then `bun run` will prefer attempting to load the path instead and not run the script.
## Example
```sh
$ bun [bun flags] run-script <script> [script flags]
```
Your `package.json` can define a number of named `"scripts"` that correspond to shell commands.
```json
{
// ... other fields
"scripts": {
"clean": "rm -rf dist && echo 'Done.'",
"dev": "bun server.ts"
}
}
```
Use `bun run-script <script>` to execute these scripts.
```bash
$ bun run-script clean
$ rm -rf dist && echo 'Done.'
Cleaning...
Done.
```
## See Also
- https://bun.sh/docs/cli/run#run-a-package-json-script

View File

@@ -33,8 +33,6 @@ const BunJS = @import("./bun_js.zig");
const Install = @import("./install/install.zig");
const bundler = bun.bundler;
const DotEnv = @import("./env_loader.zig");
const RunCommand_ = @import("./cli/run_command.zig").RunCommand;
const CreateCommand_ = @import("./cli/create_command.zig").CreateCommand;
const FilterRun = @import("./cli/filter_run.zig");
const fs = @import("fs.zig");
@@ -122,6 +120,7 @@ pub const InstallCompletionsCommand = @import("./cli/install_completions_command
pub const PackageManagerCommand = @import("./cli/package_manager_command.zig").PackageManagerCommand;
pub const RemoveCommand = @import("./cli/remove_command.zig").RemoveCommand;
pub const RunCommand = @import("./cli/run_command.zig").RunCommand;
pub const RunScriptCommand = @import("./cli/run_command.zig").RunScriptCommand;
pub const ShellCompletions = @import("./cli/shell_completions.zig");
pub const UpdateCommand = @import("./cli/update_command.zig").UpdateCommand;
pub const UpgradeCommand = @import("./cli/upgrade_command.zig").UpgradeCommand;
@@ -1556,6 +1555,7 @@ pub const Command = struct {
=> .RemoveCommand,
RootCommandMatcher.case("run") => .RunCommand,
RootCommandMatcher.case("run-script") => .RunScriptCommand,
RootCommandMatcher.case("help") => .HelpCommand,
RootCommandMatcher.case("exec") => .ExecCommand,
@@ -2014,6 +2014,24 @@ pub const Command = struct {
bun.assert(pretend_to_be_node);
try RunCommand.execAsIfNode(ctx);
},
.RunScriptCommand => {
if (comptime bun.fast_debug_build_mode and bun.fast_debug_build_cmd != .RunScriptCommand) unreachable;
const ctx = try Command.init(allocator, log, .RunScriptCommand);
if (ctx.filters.len > 0) {
FilterRun.runScriptsWithFilter(ctx) catch |err| {
Output.prettyErrorln("<r><red>error<r>: {s}", .{@errorName(err)});
Global.exit(1);
};
}
if (ctx.positionals.len > 0) {
if (try RunScriptCommand.exec(ctx)) {
return;
}
Global.exit(1);
}
},
.UpgradeCommand => {
if (comptime bun.fast_debug_build_mode and bun.fast_debug_build_cmd != .UpgradeCommand) unreachable;
const ctx = try Command.init(allocator, log, .UpgradeCommand);
@@ -2294,6 +2312,7 @@ pub const Command = struct {
PatchCommitCommand,
OutdatedCommand,
PublishCommand,
RunScriptCommand,
/// Used by crash reports.
///
@@ -2327,6 +2346,7 @@ pub const Command = struct {
.PatchCommitCommand => 'z',
.OutdatedCommand => 'o',
.PublishCommand => 'k',
.RunScriptCommand => 's',
};
}
@@ -2363,7 +2383,10 @@ pub const Command = struct {
HelpCommand.printWithReason(.explicit, show_all_flags);
},
.RunCommand, .RunAsNodeCommand => {
RunCommand_.printHelp(null);
RunCommand.printHelp(null);
},
.RunScriptCommand => {
RunScriptCommand.printHelp(null);
},
.InitCommand => {
@@ -2556,7 +2579,16 @@ pub const Command = struct {
.PublishCommand => .publish,
});
},
else => {
.AddCommand,
.DiscordCommand,
.InstallCommand,
.LinkCommand,
.PackageManagerCommand,
.RemoveCommand,
.UnlinkCommand,
.UpdateCommand,
.ReservedCommand,
=> {
HelpCommand.printWithReason(.explicit);
},
}

View File

@@ -1719,3 +1719,149 @@ pub const BunXFastPath = struct {
};
}
};
pub const RunScriptCommand = struct {
pub fn exec(ctx: Command.Context) !bool {
const positionals = ctx.positionals[1..];
var script_name_to_search: string = "";
if (positionals.len > 0) {
script_name_to_search = positionals[0];
}
const passthrough = ctx.passthrough;
const force_using_bun = ctx.debug.run_in_bun;
Global.configureAllocator(.{ .long_running = false });
var ORIGINAL_PATH: string = "";
var this_bundler: bundler.Bundler = undefined;
const root_dir_info = try RunCommand.configureEnvForRun(ctx, &this_bundler, null, true, false);
try RunCommand.configurePathForRun(ctx, root_dir_info, &this_bundler, &ORIGINAL_PATH, root_dir_info.abs_path, force_using_bun);
this_bundler.env.map.put("npm_lifecycle_event", script_name_to_search) catch unreachable;
// Run package.json script
if (root_dir_info.enclosing_package_json) |package_json| {
if (package_json.scripts) |scripts| {
if (scripts.get(script_name_to_search)) |script_content| {
// allocate enough to hold "post${scriptname}"
var temp_script_buffer = try std.fmt.allocPrint(ctx.allocator, "ppre{s}", .{script_name_to_search});
defer ctx.allocator.free(temp_script_buffer);
if (scripts.get(temp_script_buffer[1..])) |prescript| {
try RunCommand.runPackageScriptForeground(
ctx,
ctx.allocator,
prescript,
temp_script_buffer[1..],
this_bundler.fs.top_level_dir,
this_bundler.env,
&.{},
ctx.debug.silent,
ctx.debug.use_system_shell,
);
}
try RunCommand.runPackageScriptForeground(
ctx,
ctx.allocator,
script_content,
script_name_to_search,
this_bundler.fs.top_level_dir,
this_bundler.env,
passthrough,
ctx.debug.silent,
ctx.debug.use_system_shell,
);
temp_script_buffer[0.."post".len].* = "post".*;
if (scripts.get(temp_script_buffer)) |postscript| {
try RunCommand.runPackageScriptForeground(
ctx,
ctx.allocator,
postscript,
temp_script_buffer,
this_bundler.fs.top_level_dir,
this_bundler.env,
&.{},
ctx.debug.silent,
ctx.debug.use_system_shell,
);
}
return true;
}
}
}
if (script_name_to_search.len == 0) {
if (root_dir_info.enclosing_package_json) |package_json| {
printHelp(package_json);
} else {
printHelp(null);
Output.prettyln("\n<r><yellow>No package.json found.<r>\n", .{});
Output.flush();
}
return true;
}
Output.prettyError("<r><red>error<r><d>:<r> <b>Script not found \"<b>{s}<r>\"\n", .{script_name_to_search});
Global.exit(1);
}
pub fn printHelp(package_json: ?*PackageJSON) void {
const intro_text =
\\<b>Usage<r>: <b><green>bun run-script<r> <cyan>[flags]<r> \<script\>
;
const examples_text =
\\<b>Examples:<r>
\\ <b><green>bun run-script<r> <blue>dev<r>
\\ <b><green>bun run-script<r> <blue>lint<r>
\\ <b><green>bun run-script<r> <blue>test<r>
\\
\\Full documentation is available at <magenta>https://bun.sh/docs/cli/run-script<r>
\\
;
Output.pretty(intro_text ++ "\n\n", .{});
Output.pretty("<b>Flags:<r>", .{});
clap.simpleHelp(&Arguments.run_params);
Output.pretty("\n\n" ++ examples_text, .{});
if (package_json) |pkg| {
if (pkg.scripts) |scripts| {
var display_name = pkg.name;
if (display_name.len == 0) {
display_name = std.fs.path.basename(pkg.source.path.name.dir);
}
var iterator = scripts.iterator();
if (scripts.count() > 0) {
Output.pretty("\n<b>package.json scripts ({d} found):<r>", .{scripts.count()});
// Output.prettyln("<r><blue><b>{s}<r> scripts:<r>\n", .{display_name});
while (iterator.next()) |entry| {
Output.prettyln("\n", .{});
Output.prettyln(" <d>$</r> bun run-script<r> <blue>{s}<r>\n", .{entry.key_ptr.*});
Output.prettyln(" <d> {s}<r>\n", .{entry.value_ptr.*});
}
// Output.prettyln("\n<d>{d} scripts<r>", .{scripts.count()});
Output.prettyln("\n", .{});
} else {
Output.prettyln("\n<r><yellow>No \"scripts\" found in package.json.<r>\n", .{});
}
} else {
Output.prettyln("\n<r><yellow>No \"scripts\" found in package.json.<r>\n", .{});
}
}
Output.flush();
}
};

View File

@@ -0,0 +1,93 @@
import { file, spawn, spawnSync } from "bun";
import { beforeEach, describe, expect, it } from "bun:test";
import { exists, mkdir, rm, writeFile } from "fs/promises";
import { bunEnv, bunExe, bunEnv as env, isWindows, tempDirWithFiles, tmpdirSync } from "harness";
import { join } from "path";
import { readdirSorted } from "./dummy.registry";
let run_dir: string;
beforeEach(async () => {
run_dir = tmpdirSync();
});
it("can run a script", async () => {
await writeFile(
join(run_dir, "package.json"),
JSON.stringify({
name: "foo",
version: "0.0.1",
scripts: {
foo: "echo hello world",
},
}),
);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "run-script", "foo"],
cwd: run_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err = await new Response(stderr).text();
expect(err).toEqual(`$ echo hello world\n`);
const out = await new Response(stdout).text();
expect(out).toEqual("hello world\n");
expect(await exited).toBe(0);
});
it("cannot run a file", async () => {
await writeFile(
join(run_dir, "package.json"),
JSON.stringify({
name: "foo",
version: "0.0.1",
scripts: {
foo: "echo hello world",
},
}),
);
await writeFile(join(run_dir, "index.js"), `console.log('hello from js');\n`);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "run-script", "index.js"],
cwd: run_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err = await new Response(stderr).text();
expect(err).toEqual(`error: Script not found "index.js"\n`);
const out = await new Response(stdout).text();
expect(out).toBeEmpty();
expect(await exited).toBe(1);
});
it("can run a script when there is a folder matching its name", async () => {
await writeFile(
join(run_dir, "package.json"),
JSON.stringify({
name: "foo",
version: "0.0.1",
scripts: {
foo: "echo hello world",
},
}),
);
await mkdir(join(run_dir, "foo"));
await writeFile(join(run_dir, "foo", "index.js"), `console.log('hello from js');\n`);
const { stdout, stderr, exited } = spawn({
cmd: [bunExe(), "run-script", "foo"],
cwd: run_dir,
stdout: "pipe",
stdin: "pipe",
stderr: "pipe",
env,
});
const err = await new Response(stderr).text();
expect(err).toEqual(`$ echo hello world\n`);
const out = await new Response(stdout).text();
expect(out).toEqual("hello world\n");
expect(await exited).toBe(0);
});