diff --git a/docs/cli/run-all.md b/docs/cli/run-all.md new file mode 100644 index 0000000000..31976f61db --- /dev/null +++ b/docs/cli/run-all.md @@ -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 [target3] ... +``` + +or + +```bash +bun --all [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" + } +} +``` \ No newline at end of file diff --git a/src/cli.zig b/src/cli.zig index 96c3f2d4b1..cf697e1810 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -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, diff --git a/src/cli/Arguments.zig b/src/cli/Arguments.zig index 55eb512c7f..843bf54069 100644 --- a/src/cli/Arguments.zig +++ b/src/cli/Arguments.zig @@ -112,12 +112,12 @@ pub const runtime_params_ = [_]ParamType{ pub const auto_or_run_params = [_]ParamType{ clap.parseParam("-F, --filter ... 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 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 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) { diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig index 94821e2209..724f0514c7 100644 --- a/src/cli/run_command.zig +++ b/src/cli/run_command.zig @@ -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("error: --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("error: 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("error: 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("error: 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("error: Failed to run target '{s}': {s}", .{ target, @errorName(err) }); + } + failed = true; + continue; + }; + + if (!success) { + if (log_errors) { + Output.prettyErrorln("error: 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] == '.') { diff --git a/test/cli/run-all-edge-cases.test.ts b/test/cli/run-all-edge-cases.test.ts new file mode 100644 index 0000000000..70d0aaf5c8 --- /dev/null +++ b/test/cli/run-all-edge-cases.test.ts @@ -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(""); + }); +}); \ No newline at end of file diff --git a/test/cli/run-all.test.ts b/test/cli/run-all.test.ts new file mode 100644 index 0000000000..4d11f0beab --- /dev/null +++ b/test/cli/run-all.test.ts @@ -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(""); + }); +}); \ No newline at end of file diff --git a/test/regression/issue/run-all-flag.test.ts b/test/regression/issue/run-all-flag.test.ts new file mode 100644 index 0000000000..f0a62da3b9 --- /dev/null +++ b/test/regression/issue/run-all-flag.test.ts @@ -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"); + }); +}); \ No newline at end of file