Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
477d709a0c Implement --all flag for bun run command
Add support for sequential execution of multiple package.json scripts
and source files, providing npm-run-all compatibility.

Features:
- Sequential execution of multiple targets
- Pattern matching with :* and : suffixes
- Mixed script/file execution support
- Comprehensive error handling and reporting
- Drop-in replacement for npm-run-all

Implementation:
- Add --all flag to CLI argument parsing
- Add run_all field to Context structure
- Implement pattern matching and expansion logic
- Use anyerror\! return type to resolve recursion issues
- Add extensive test coverage for all scenarios

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-14 15:43:57 +00:00
7 changed files with 1315 additions and 2 deletions

235
docs/cli/run-all.md Normal file
View File

@@ -0,0 +1,235 @@
# bun run --all
The `--all` flag for `bun run` enables sequential execution of multiple package.json scripts or source files, providing a drop-in replacement for popular tools like `npm-run-all`.
## Syntax
```bash
bun run --all <target1> <target2> [target3] ...
```
or
```bash
bun --all <target1> <target2> [target3] ...
```
## Features
### Sequential Execution
The `--all` flag runs each target sequentially (one after another), not in parallel:
```bash
bun run --all clean build test
```
This will run:
1. `clean` script
2. `build` script (after clean completes)
3. `test` script (after build completes)
### Pattern Matching
The flag supports pattern matching using `:*` and `:` suffixes to match multiple scripts:
#### Using `:*` suffix
```bash
bun run --all "test:*"
```
Matches all scripts starting with `test:`:
- `test:unit`
- `test:integration`
- `test:e2e`
#### Using `:` suffix
```bash
bun run --all "build:"
```
Equivalent to `build:*`, matches all scripts starting with `build:`:
- `build:dev`
- `build:prod`
- `build:staging`
### Mixed Targets
You can mix script names, patterns, and file paths:
```bash
bun run --all clean "test:*" ./scripts/deploy.js
```
## Examples
### Basic Usage
```json
{
"scripts": {
"clean": "rm -rf dist/",
"build": "tsc",
"test": "jest"
}
}
```
```bash
# Run all scripts in sequence
bun run --all clean build test
```
### Pattern Matching
```json
{
"scripts": {
"test:unit": "jest --testPathPattern=unit",
"test:integration": "jest --testPathPattern=integration",
"test:e2e": "playwright test",
"build:lib": "tsc -p tsconfig.lib.json",
"build:app": "vite build"
}
}
```
```bash
# Run all test scripts
bun run --all "test:*"
# Run all build scripts
bun run --all "build:*"
# Mix patterns and explicit scripts
bun run --all clean "build:*" "test:*"
```
### Real-world Build Pipeline
```json
{
"scripts": {
"clean": "rimraf dist coverage",
"lint": "eslint src/",
"typecheck": "tsc --noEmit",
"build:lib": "rollup -c",
"build:docs": "typedoc",
"test:unit": "vitest run",
"test:integration": "playwright test"
}
}
```
```bash
# Complete CI pipeline
bun run --all clean lint typecheck "build:*" "test:*"
```
### Running Files
```bash
# Run multiple JavaScript files
bun run --all ./scripts/setup.js ./scripts/build.js ./scripts/deploy.js
# Mix scripts and files
bun run --all clean ./scripts/custom-build.js test
```
## Error Handling
### Failure Behavior
When a target fails:
- The command continues executing remaining targets
- The overall command exits with a non-zero status
- Error details are displayed for failed targets
```bash
bun run --all success1 failing-script success2
# Output: success1 runs, failing-script fails, success2 still runs
# Exit code: non-zero
```
### Missing Targets
If a target doesn't exist:
- An error is reported for that target
- Execution continues with remaining targets
- Overall command fails
### Pattern Matching Edge Cases
If a pattern matches no scripts:
- An error is reported
- The command fails immediately
```bash
bun run --all "nonexistent:*"
# Error: No targets found matching the given patterns
```
## npm-run-all Compatibility
The `--all` flag is designed as a drop-in replacement for `npm-run-all`:
### npm-run-all
```bash
npm-run-all clean build test
npm-run-all "test:*"
npm-run-all --serial clean build test
```
### bun equivalent
```bash
bun run --all clean build test
bun run --all "test:*"
bun run --all clean build test # always serial
```
## Implementation Notes
### Current Limitations
- **Sequential Only**: The current implementation runs targets sequentially. Parallel execution may be added in future versions.
- **Pattern Expansion**: Patterns are expanded against package.json scripts. More complex glob patterns are not currently supported.
- **Workspace Support**: Workspace-aware execution is planned for future versions.
### Future Enhancements
The implementation is designed to easily support:
- Parallel execution with `--parallel` flag
- Advanced glob patterns
- Workspace package filtering
- Output formatting options
## Migration from npm-run-all
To migrate from `npm-run-all` to `bun run --all`:
1. Replace `npm-run-all` with `bun run --all`
2. Remove `--serial` flag (always sequential)
3. Keep existing pattern syntax unchanged
4. Update CI/build scripts accordingly
### Before
```json
{
"scripts": {
"build": "npm-run-all clean lint build:*",
"test": "npm-run-all --serial test:unit test:integration"
}
}
```
### After
```json
{
"scripts": {
"build": "bun run --all clean lint build:*",
"test": "bun run --all test:unit test:integration"
}
}
```

View File

@@ -410,6 +410,7 @@ pub const Command = struct {
runtime_options: RuntimeOptions = .{},
filters: []const []const u8 = &.{},
run_all: bool = false,
preloads: []const string = &.{},
has_loaded_global_config: bool = false,

View File

@@ -112,12 +112,12 @@ pub const runtime_params_ = [_]ParamType{
pub const auto_or_run_params = [_]ParamType{
clap.parseParam("-F, --filter <STR>... Run a script in all workspace packages matching the pattern") catch unreachable,
clap.parseParam("--all Run multiple scripts or files sequentially") catch unreachable,
clap.parseParam("-b, --bun Force a script or package to use Bun's runtime instead of Node.js (via symlinking node)") catch unreachable,
clap.parseParam("--shell <STR> Control the shell used for package.json scripts. Supports either 'bun' or 'system'") catch unreachable,
};
pub const auto_only_params = [_]ParamType{
// clap.parseParam("--all") catch unreachable,
clap.parseParam("--silent Don't print the script command") catch unreachable,
clap.parseParam("--elide-lines <NUMBER> Number of lines of script output shown when using --filter (default: 10). Set to 0 to show all lines.") catch unreachable,
clap.parseParam("-v, --version Print version and exit") catch unreachable,
@@ -379,6 +379,7 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
if (cmd == .RunCommand or cmd == .AutoCommand) {
ctx.filters = args.options("--filter");
ctx.run_all = args.flag("--all");
if (args.option("--elide-lines")) |elide_lines| {
if (elide_lines.len > 0) {

View File

@@ -1330,6 +1330,52 @@ pub const RunCommand = struct {
_ = _bootAndHandleError(ctx, absolute_script_path.?, null);
return true;
}
/// Match a pattern against script names, supporting :* and : suffixes
fn matchesPattern(script_name: []const u8, pattern: []const u8) bool {
if (strings.eql(script_name, pattern)) {
return true;
}
// Handle pattern:* (match anything starting with "pattern:")
if (strings.endsWith(pattern, ":*")) {
const prefix = pattern[0..pattern.len - 1]; // Remove the '*'
return strings.startsWith(script_name, prefix);
}
// Handle pattern: (same as pattern:*)
if (strings.endsWith(pattern, ":")) {
return strings.startsWith(script_name, pattern);
}
return false;
}
/// Find all scripts matching the given patterns
fn findMatchingScripts(
allocator: std.mem.Allocator,
package_json: *const PackageJSON,
patterns: []const []const u8,
) !std.ArrayList([]const u8) {
var matches = std.ArrayList([]const u8).init(allocator);
if (package_json.scripts) |scripts| {
var script_iter = scripts.iterator();
while (script_iter.next()) |entry| {
const script_name = entry.key_ptr.*;
for (patterns) |pattern| {
if (matchesPattern(script_name, pattern)) {
try matches.append(script_name);
break; // Don't add the same script multiple times
}
}
}
}
return matches;
}
pub fn exec(
ctx: Command.Context,
cfg: struct {
@@ -1337,7 +1383,7 @@ pub const RunCommand = struct {
log_errors: bool,
allow_fast_run_for_extensions: bool,
},
) !bool {
) anyerror!bool {
const bin_dirs_only = cfg.bin_dirs_only;
const log_errors = cfg.log_errors;
@@ -1355,6 +1401,101 @@ pub const RunCommand = struct {
}
const passthrough = ctx.passthrough; // unclear why passthrough is an escaped string, it should probably be []const []const u8 and allow its users to escape it.
// Handle --all flag: run multiple targets sequentially
if (ctx.run_all) {
if (target_name.len == 0 and positionals.len == 0) {
if (log_errors) {
Output.prettyErrorln("<r><red>error<r>: --all flag requires at least one target", .{});
}
return false;
}
// Collect all targets and expand patterns
var all_targets = std.ArrayList([]const u8).init(ctx.allocator);
defer all_targets.deinit();
if (target_name.len > 0) {
try all_targets.append(target_name);
}
for (positionals) |pos| {
try all_targets.append(pos);
}
// Check if any targets are patterns that need expansion
var expanded_targets = std.ArrayList([]const u8).init(ctx.allocator);
defer expanded_targets.deinit();
// Get package.json for pattern expansion
var this_transpiler: transpiler.Transpiler = undefined;
const root_dir_info = configureEnvForRun(ctx, &this_transpiler, null, log_errors, false) catch |err| {
if (log_errors) {
Output.prettyErrorln("<r><red>error<r>: Failed to configure environment: {s}", .{@errorName(err)});
}
return false;
};
for (all_targets.items) |target| {
// Check if this is a pattern (ends with :* or :)
if (strings.endsWith(target, ":*") or strings.endsWith(target, ":")) {
// Expand pattern against package.json scripts
if (root_dir_info.enclosing_package_json) |package_json| {
var matches = findMatchingScripts(ctx.allocator, package_json, &[_][]const u8{target}) catch |err| {
if (log_errors) {
Output.prettyErrorln("<r><red>error<r>: Failed to expand pattern '{s}': {s}", .{ target, @errorName(err) });
}
continue;
};
defer matches.deinit();
for (matches.items) |match| {
try expanded_targets.append(match);
}
} else {
// No package.json found, treat as literal target
try expanded_targets.append(target);
}
} else {
// Regular target, add as-is
try expanded_targets.append(target);
}
}
if (expanded_targets.items.len == 0) {
if (log_errors) {
Output.prettyErrorln("<r><red>error<r>: No targets found matching the given patterns", .{});
}
return false;
}
// Execute each target sequentially
var failed = false;
for (expanded_targets.items) |target| {
// Create new context with single target
var new_ctx = ctx.*;
var temp_positionals = [_][]const u8{target};
new_ctx.positionals = &temp_positionals;
new_ctx.run_all = false; // Disable --all for recursive calls
// Call exec recursively for this single target
const success = exec(&new_ctx, cfg) catch |err| {
if (log_errors) {
Output.prettyErrorln("<r><red>error<r>: Failed to run target '{s}': {s}", .{ target, @errorName(err) });
}
failed = true;
continue;
};
if (!success) {
if (log_errors) {
Output.prettyErrorln("<r><red>error<r>: Target '{s}' failed", .{target});
}
failed = true;
}
}
return !failed;
}
var try_fast_run = false;
var skip_script_check = false;
if (target_name.len > 0 and target_name[0] == '.') {

View File

@@ -0,0 +1,337 @@
import { describe, test, expect } from "bun:test";
import { bunExe, bunEnv, tempDirWithFiles } from "harness";
describe("bun run --all edge cases", () => {
test("should handle empty scripts in package.json", async () => {
const dir = tempDirWithFiles("run-all-empty-scripts", {
"package.json": JSON.stringify({
name: "test-package",
scripts: {},
}),
"app.js": 'console.log("Running app");',
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "app.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stdout).toContain("Running app");
expect(stderr).toBe("");
});
test("should handle scripts with special characters", async () => {
const dir = tempDirWithFiles("run-all-special-chars", {
"package.json": JSON.stringify({
name: "test-package",
scripts: {
"test:with-dashes": 'echo "Test with dashes"',
"test_with_underscores": 'echo "Test with underscores"',
"test.with.dots": 'echo "Test with dots"',
},
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "test:with-dashes", "test_with_underscores", "test.with.dots"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stdout).toContain("Test with dashes");
expect(stdout).toContain("Test with underscores");
expect(stdout).toContain("Test with dots");
expect(stderr).toBe("");
});
test("should handle very long script output", async () => {
const dir = tempDirWithFiles("run-all-long-output", {
"package.json": JSON.stringify({
name: "test-package",
scripts: {
"long": 'for i in {1..100}; do echo "Line $i"; done',
},
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "long"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stdout).toContain("Line 1");
expect(stdout).toContain("Line 100");
expect(stderr).toBe("");
});
test("should handle scripts that take time", async () => {
const dir = tempDirWithFiles("run-all-timing", {
"package.json": JSON.stringify({
name: "test-package",
scripts: {
"first": 'echo "Starting"; sleep 0.1; echo "First done"',
"second": 'echo "Second done"',
},
}),
});
const startTime = Date.now();
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "first", "second"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
const endTime = Date.now();
expect(exitCode).toBe(0);
expect(stdout).toContain("Starting");
expect(stdout).toContain("First done");
expect(stdout).toContain("Second done");
expect(endTime - startTime).toBeGreaterThan(90); // Should take at least 100ms
expect(stderr).toBe("");
});
test("should handle absolute paths", async () => {
const dir = tempDirWithFiles("run-all-absolute", {
"script.js": 'console.log("Absolute path script");',
});
const absolutePath = `${dir}/script.js`;
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", absolutePath],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stdout).toContain("Absolute path script");
expect(stderr).toBe("");
});
test("should handle relative paths with ./", async () => {
const dir = tempDirWithFiles("run-all-relative", {
"script.js": 'console.log("Relative path script");',
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "./script.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stdout).toContain("Relative path script");
expect(stderr).toBe("");
});
test("should handle non-existent scripts gracefully", async () => {
const dir = tempDirWithFiles("run-all-nonexistent", {
"package.json": JSON.stringify({
name: "test-package",
scripts: {
"existing": 'echo "This exists"',
},
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "existing", "nonexistent"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).not.toBe(0);
expect(stdout).toContain("This exists"); // Should run the existing script
expect(stderr).toContain("nonexistent"); // Should report the error
});
test("should handle non-existent files gracefully", async () => {
const dir = tempDirWithFiles("run-all-nonexistent-file", {
"existing.js": 'console.log("This file exists");',
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "existing.js", "nonexistent.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).not.toBe(0);
expect(stdout).toContain("This file exists"); // Should run the existing file
expect(stderr).toContain("nonexistent.js"); // Should report the error
});
test("should handle patterns with no package.json", async () => {
const dir = tempDirWithFiles("run-all-no-package-pattern", {
"app.js": 'console.log("App running");',
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "test:*", "app.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
// Should handle pattern as literal when no package.json
// and continue with the file execution
expect(stdout).toContain("App running");
// May have warnings about test:* but should continue
});
test("should handle scripts with environment variables", async () => {
const dir = tempDirWithFiles("run-all-env", {
"package.json": JSON.stringify({
name: "test-package",
scripts: {
"env-test": 'echo "NODE_ENV is $NODE_ENV"',
"custom-env": 'echo "CUSTOM_VAR is $CUSTOM_VAR"',
},
}),
});
const customEnv = {
...bunEnv,
NODE_ENV: "test",
CUSTOM_VAR: "hello-world",
};
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "env-test", "custom-env"],
env: customEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stdout).toContain("NODE_ENV is test");
expect(stdout).toContain("CUSTOM_VAR is hello-world");
expect(stderr).toBe("");
});
test("should handle scripts with complex commands", async () => {
const dir = tempDirWithFiles("run-all-complex-cmd", {
"package.json": JSON.stringify({
name: "test-package",
scripts: {
"complex": 'echo "Start" && echo "Middle" && echo "End"',
"with-pipe": 'echo "Hello World" | grep "World"',
},
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "complex", "with-pipe"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stdout).toContain("Start");
expect(stdout).toContain("Middle");
expect(stdout).toContain("End");
expect(stdout).toContain("World");
expect(stderr).toBe("");
});
test("should handle duplicate targets", async () => {
const dir = tempDirWithFiles("run-all-duplicates", {
"package.json": JSON.stringify({
name: "test-package",
scripts: {
"test": 'echo "Running test"',
},
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "test", "test", "test"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
// Should run the script multiple times
const testOutputs = (stdout.match(/Running test/g) || []).length;
expect(testOutputs).toBe(3);
expect(stderr).toBe("");
});
});

341
test/cli/run-all.test.ts Normal file
View File

@@ -0,0 +1,341 @@
import { describe, test, expect } from "bun:test";
import { bunExe, bunEnv, tempDirWithFiles } from "harness";
describe("bun run --all", () => {
test("should run multiple scripts sequentially", async () => {
const dir = tempDirWithFiles("run-all-basic", {
"package.json": JSON.stringify({
name: "test-package",
scripts: {
"test:unit": 'echo "Running unit tests"',
"test:integration": 'echo "Running integration tests"',
"build": 'echo "Building project"',
},
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "test:unit", "test:integration"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stdout).toContain("Running unit tests");
expect(stdout).toContain("Running integration tests");
expect(stderr).toBe("");
});
test("should support pattern matching with :*", async () => {
const dir = tempDirWithFiles("run-all-pattern", {
"package.json": JSON.stringify({
name: "test-package",
scripts: {
"test:unit": 'echo "Unit tests"',
"test:integration": 'echo "Integration tests"',
"test:e2e": 'echo "E2E tests"',
"build": 'echo "Building"',
"lint": 'echo "Linting"',
},
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "test:*"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stdout).toContain("Unit tests");
expect(stdout).toContain("Integration tests");
expect(stdout).toContain("E2E tests");
expect(stdout).not.toContain("Building");
expect(stdout).not.toContain("Linting");
expect(stderr).toBe("");
});
test("should support pattern matching with : suffix", async () => {
const dir = tempDirWithFiles("run-all-colon", {
"package.json": JSON.stringify({
name: "test-package",
scripts: {
"build:dev": 'echo "Dev build"',
"build:prod": 'echo "Prod build"',
"build:staging": 'echo "Staging build"',
"test": 'echo "Testing"',
},
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "build:"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stdout).toContain("Dev build");
expect(stdout).toContain("Prod build");
expect(stdout).toContain("Staging build");
expect(stdout).not.toContain("Testing");
expect(stderr).toBe("");
});
test("should run source files when specified", async () => {
const dir = tempDirWithFiles("run-all-files", {
"script1.js": 'console.log("Script 1 executed");',
"script2.js": 'console.log("Script 2 executed");',
"package.json": JSON.stringify({
name: "test-package",
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "script1.js", "script2.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stdout).toContain("Script 1 executed");
expect(stdout).toContain("Script 2 executed");
expect(stderr).toBe("");
});
test("should handle mix of scripts and files", async () => {
const dir = tempDirWithFiles("run-all-mixed", {
"package.json": JSON.stringify({
name: "test-package",
scripts: {
"build": 'echo "Building"',
},
}),
"test.js": 'console.log("Test file executed");',
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "build", "test.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stdout).toContain("Building");
expect(stdout).toContain("Test file executed");
expect(stderr).toBe("");
});
test("should error when no targets provided", async () => {
const dir = tempDirWithFiles("run-all-no-targets", {
"package.json": JSON.stringify({
name: "test-package",
scripts: {
"test": 'echo "Testing"',
},
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).not.toBe(0);
expect(stderr).toContain("--all flag requires at least one target");
});
test("should continue execution when one target fails", async () => {
const dir = tempDirWithFiles("run-all-failure", {
"package.json": JSON.stringify({
name: "test-package",
scripts: {
"success1": 'echo "Success 1"',
"failure": "exit 1",
"success2": 'echo "Success 2"',
},
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "success1", "failure", "success2"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).not.toBe(0); // Should fail overall
expect(stdout).toContain("Success 1");
expect(stdout).toContain("Success 2");
expect(stderr).toContain("failure"); // Should report the failed target
});
test("should handle pattern that matches no scripts", async () => {
const dir = tempDirWithFiles("run-all-no-match", {
"package.json": JSON.stringify({
name: "test-package",
scripts: {
"build": 'echo "Building"',
"test": 'echo "Testing"',
},
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "deploy:*"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).not.toBe(0);
expect(stderr).toContain("No targets found matching the given patterns");
});
test("should work without package.json when running files", async () => {
const dir = tempDirWithFiles("run-all-no-package", {
"app.js": 'console.log("App running");',
"server.js": 'console.log("Server running");',
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "app.js", "server.js"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stdout).toContain("App running");
expect(stdout).toContain("Server running");
expect(stderr).toBe("");
});
test("should execute scripts in order", async () => {
const dir = tempDirWithFiles("run-all-order", {
"package.json": JSON.stringify({
name: "test-package",
scripts: {
"first": 'echo "First script"',
"second": 'echo "Second script"',
"third": 'echo "Third script"',
},
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "first", "second", "third"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
const lines = stdout.trim().split('\n');
expect(lines).toContain("First script");
expect(lines).toContain("Second script");
expect(lines).toContain("Third script");
// Check order (allowing for some output variation)
const firstIndex = lines.findIndex(line => line.includes("First script"));
const secondIndex = lines.findIndex(line => line.includes("Second script"));
const thirdIndex = lines.findIndex(line => line.includes("Third script"));
expect(firstIndex).toBeLessThan(secondIndex);
expect(secondIndex).toBeLessThan(thirdIndex);
});
test("should handle complex patterns with multiple matches", async () => {
const dir = tempDirWithFiles("run-all-complex", {
"package.json": JSON.stringify({
name: "test-package",
scripts: {
"test:unit:fast": 'echo "Fast unit tests"',
"test:unit:slow": 'echo "Slow unit tests"',
"test:integration:api": 'echo "API integration tests"',
"test:integration:ui": 'echo "UI integration tests"',
"build:dev": 'echo "Dev build"',
"build:prod": 'echo "Prod build"',
},
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "test:unit:", "test:integration:"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stdout).toContain("Fast unit tests");
expect(stdout).toContain("Slow unit tests");
expect(stdout).toContain("API integration tests");
expect(stdout).toContain("UI integration tests");
expect(stdout).not.toContain("Dev build");
expect(stdout).not.toContain("Prod build");
expect(stderr).toBe("");
});
});

View File

@@ -0,0 +1,257 @@
import { describe, test, expect } from "bun:test";
import { bunExe, bunEnv, tempDirWithFiles } from "harness";
describe("bun run --all regression tests", () => {
test("should be a drop-in replacement for npm-run-all basic usage", async () => {
// This test ensures --all flag works like npm-run-all
const dir = tempDirWithFiles("npm-run-all-compat", {
"package.json": JSON.stringify({
name: "test-package",
scripts: {
"clean": 'echo "Cleaning..."',
"build": 'echo "Building..."',
"test": 'echo "Testing..."',
},
}),
});
// Test equivalent to: npm-run-all clean build test
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "clean", "build", "test"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stdout).toContain("Cleaning...");
expect(stdout).toContain("Building...");
expect(stdout).toContain("Testing...");
expect(stderr).toBe("");
});
test("should support npm-run-all pattern syntax", async () => {
// Test equivalent to: npm-run-all "test:*"
const dir = tempDirWithFiles("npm-run-all-pattern", {
"package.json": JSON.stringify({
name: "test-package",
scripts: {
"test:unit": 'echo "Unit tests"',
"test:integration": 'echo "Integration tests"',
"test:e2e": 'echo "E2E tests"',
"build": 'echo "Building"',
},
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "test:*"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stdout).toContain("Unit tests");
expect(stdout).toContain("Integration tests");
expect(stdout).toContain("E2E tests");
expect(stdout).not.toContain("Building");
expect(stderr).toBe("");
});
test("should work with typical build pipeline", async () => {
// Real-world usage example
const dir = tempDirWithFiles("build-pipeline", {
"package.json": JSON.stringify({
name: "test-package",
scripts: {
"clean": 'echo "🧹 Cleaning dist/"',
"lint": 'echo "🔍 Linting code"',
"typecheck": 'echo "🔍 Type checking"',
"build:lib": 'echo "📦 Building library"',
"build:docs": 'echo "📚 Building docs"',
"test:unit": 'echo "🧪 Running unit tests"',
"test:integration": 'echo "🔧 Running integration tests"',
},
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "clean", "lint", "typecheck", "build:*", "test:*"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stdout).toContain("🧹 Cleaning dist/");
expect(stdout).toContain("🔍 Linting code");
expect(stdout).toContain("🔍 Type checking");
expect(stdout).toContain("📦 Building library");
expect(stdout).toContain("📚 Building docs");
expect(stdout).toContain("🧪 Running unit tests");
expect(stdout).toContain("🔧 Running integration tests");
expect(stderr).toBe("");
});
test("should handle failure gracefully like npm-run-all", async () => {
const dir = tempDirWithFiles("failure-handling", {
"package.json": JSON.stringify({
name: "test-package",
scripts: {
"step1": 'echo "Step 1 success"',
"step2": 'echo "Step 2 fail" && exit 1',
"step3": 'echo "Step 3 success"',
},
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "step1", "step2", "step3"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).not.toBe(0); // Should fail overall
expect(stdout).toContain("Step 1 success");
expect(stdout).toContain("Step 3 success"); // Should continue after failure
expect(stderr).toContain("step2"); // Should report which step failed
});
test("should preserve script execution order", async () => {
const dir = tempDirWithFiles("execution-order", {
"package.json": JSON.stringify({
name: "test-package",
scripts: {
"a": 'echo "A" && sleep 0.05',
"b": 'echo "B" && sleep 0.05',
"c": 'echo "C" && sleep 0.05',
"d": 'echo "D" && sleep 0.05',
},
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "a", "b", "c", "d"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
// Extract the letters in order they appear in output
const lines = stdout.split('\n').filter(line => line.trim().match(/^[ABCD]$/));
expect(lines).toEqual(['A', 'B', 'C', 'D']);
expect(stderr).toBe("");
});
test("should work without explicit run command", async () => {
// Test equivalent to: bun --all script1 script2
const dir = tempDirWithFiles("implicit-run", {
"package.json": JSON.stringify({
name: "test-package",
scripts: {
"start": 'echo "Starting app"',
"dev": 'echo "Development mode"',
},
}),
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--all", "start", "dev"],
env: bunEnv,
cwd: dir,
});
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
expect(exitCode).toBe(0);
expect(stdout).toContain("Starting app");
expect(stdout).toContain("Development mode");
expect(stderr).toBe("");
});
test("should handle workspace scenarios", async () => {
// Simulate monorepo workspace scenario
const dir = tempDirWithFiles("workspace-scenario", {
"package.json": JSON.stringify({
name: "monorepo-root",
scripts: {
"build:ui": 'echo "Building UI package"',
"build:api": 'echo "Building API package"',
"build:shared": 'echo "Building shared package"',
"test:ui": 'echo "Testing UI package"',
"test:api": 'echo "Testing API package"',
"lint:ui": 'echo "Linting UI package"',
"lint:api": 'echo "Linting API package"',
},
}),
});
// Build all packages
await using buildProc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "build:*"],
env: bunEnv,
cwd: dir,
});
const [buildStdout, buildStderr, buildExitCode] = await Promise.all([
new Response(buildProc.stdout).text(),
new Response(buildProc.stderr).text(),
buildProc.exited,
]);
expect(buildExitCode).toBe(0);
expect(buildStdout).toContain("Building UI package");
expect(buildStdout).toContain("Building API package");
expect(buildStdout).toContain("Building shared package");
// Test all packages
await using testProc = Bun.spawn({
cmd: [bunExe(), "run", "--all", "test:*"],
env: bunEnv,
cwd: dir,
});
const [testStdout, testStderr, testExitCode] = await Promise.all([
new Response(testProc.stdout).text(),
new Response(testProc.stderr).text(),
testProc.exited,
]);
expect(testExitCode).toBe(0);
expect(testStdout).toContain("Testing UI package");
expect(testStdout).toContain("Testing API package");
});
});