mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 06:12:08 +00:00
Compare commits
9 Commits
nektro-pat
...
claude/rel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97a2c76e73 | ||
|
|
f231074908 | ||
|
|
3718acae60 | ||
|
|
f3e5ade18a | ||
|
|
a00f68674c | ||
|
|
7c516aba0d | ||
|
|
b4e5fe01c6 | ||
|
|
7d7a77bd78 | ||
|
|
6c874c5c90 |
7
bun.lock
7
bun.lock
@@ -3,6 +3,9 @@
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "bun",
|
||||
"dependencies": {
|
||||
"no": "^0.0.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@lezer/common": "^1.2.3",
|
||||
"@lezer/cpp": "^1.1.3",
|
||||
@@ -188,6 +191,8 @@
|
||||
|
||||
"clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="],
|
||||
|
||||
"coffee-script": ["coffee-script@1.12.7", "", { "bin": { "coffee": "./bin/coffee", "cake": "./bin/cake" } }, "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw=="],
|
||||
|
||||
"commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="],
|
||||
|
||||
"constant-case": ["constant-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3", "upper-case": "^2.0.2" } }, "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ=="],
|
||||
@@ -268,6 +273,8 @@
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"no": ["no@0.0.1", "", { "dependencies": { "coffee-script": "*" } }, "sha512-9fDwk9njTDe89ip96JobYnmtWOGv+EkvyP2ErzxuLPr4WG6fGjl9BWGOj09Bc7sP26p4zvemhWRNdp65T0eX+g=="],
|
||||
|
||||
"no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="],
|
||||
|
||||
"octokit": ["octokit@3.2.2", "", { "dependencies": { "@octokit/app": "^14.0.2", "@octokit/core": "^5.0.0", "@octokit/oauth-app": "^6.0.0", "@octokit/plugin-paginate-graphql": "^4.0.0", "@octokit/plugin-paginate-rest": "11.4.4-cjs.2", "@octokit/plugin-rest-endpoint-methods": "13.3.2-cjs.1", "@octokit/plugin-retry": "^6.0.0", "@octokit/plugin-throttling": "^8.0.0", "@octokit/request-error": "^5.0.0", "@octokit/types": "^13.0.0", "@octokit/webhooks": "^12.3.1" } }, "sha512-7Abo3nADdja8l/aglU6Y3lpnHSfv0tw7gFPiqzry/yCU+2gTAX7R1roJ8hJrxIK+S1j+7iqRJXtmuHJ/UDsBhQ=="],
|
||||
|
||||
@@ -90,5 +90,8 @@
|
||||
"machine:linux:amazonlinux": "./scripts/machine.mjs ssh --cloud=aws --arch=x64 --instance-type c7i.2xlarge --os=linux --distro=amazonlinux --release=2023",
|
||||
"machine:windows:2019": "./scripts/machine.mjs ssh --cloud=aws --arch=x64 --instance-type c7i.2xlarge --os=windows --release=2019",
|
||||
"sync-webkit-source": "bun ./scripts/sync-webkit-source.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"no": "^0.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const Global = @This();
|
||||
|
||||
extern "c" fn setenv(name: [*:0]const u8, value: [*:0]const u8, overwrite: i32) i32;
|
||||
|
||||
/// Does not have the canary tag, because it is exposed in `Bun.version`
|
||||
/// "1.0.0" or "1.0.0-debug"
|
||||
pub const package_json_version = if (Environment.isDebug)
|
||||
@@ -109,6 +111,49 @@ pub fn exit(code: u32) noreturn {
|
||||
// If we are crashing, allow the crash handler to finish it's work.
|
||||
bun.crash_handler.sleepForeverIfAnotherThreadIsCrashing();
|
||||
|
||||
// Check restart policy for JavaScript files
|
||||
const cli_ctx = bun.cli.Command.get();
|
||||
const restart_policy = cli_ctx.runtime_options.restart_policy;
|
||||
|
||||
// Determine if we should restart based on the policy and exit code
|
||||
const should_restart = switch (restart_policy) {
|
||||
.no => false,
|
||||
.on_failure => code != 0,
|
||||
.always => true,
|
||||
.unless_stopped => code != 0,
|
||||
};
|
||||
|
||||
if (should_restart) {
|
||||
// Track restart count for logging purposes
|
||||
const restart_count_env = bun.getenvZ("BUN_RESTART_COUNT") orelse "0";
|
||||
const restart_count = std.fmt.parseInt(u32, restart_count_env, 10) catch 0;
|
||||
|
||||
// Set environment variable for next restart
|
||||
var restart_count_buf: [16]u8 = undefined;
|
||||
const new_count_str = std.fmt.bufPrint(&restart_count_buf, "{d}", .{restart_count + 1}) catch "0";
|
||||
|
||||
// Create null-terminated string for setenv
|
||||
var value_buf: [32]u8 = undefined;
|
||||
if (std.fmt.bufPrintZ(&value_buf, "{s}", .{new_count_str})) |value_z| {
|
||||
if (setenv("BUN_RESTART_COUNT", value_z.ptr, 1) == 0) {
|
||||
// Show restart message (similar to Docker)
|
||||
if (restart_count == 0) {
|
||||
bun.Output.prettyln("<d>Process restarting due to restart policy: {s}<r>", .{@tagName(restart_policy)});
|
||||
} else if (restart_count <= 5 or restart_count % 10 == 0) {
|
||||
bun.Output.prettyln("<d>Process restarting (attempt {d})...<r>", .{restart_count + 1});
|
||||
}
|
||||
bun.Output.flush();
|
||||
|
||||
// Use bun.reloadProcess to restart the process
|
||||
bun.reloadProcess(bun.default_allocator, false, false);
|
||||
// If reloadProcess returned (failure case), continue with normal exit
|
||||
}
|
||||
// If setenv failed, continue with normal exit
|
||||
} else |_| {
|
||||
// Failed to format value, continue with normal exit
|
||||
}
|
||||
}
|
||||
|
||||
if (Environment.isDebug) {
|
||||
bun.assert(bun.debug_allocator_data.backing.?.deinit() == .ok);
|
||||
bun.debug_allocator_data.backing = null;
|
||||
|
||||
21
src/cli.zig
21
src/cli.zig
@@ -368,6 +368,26 @@ pub const Command = struct {
|
||||
},
|
||||
};
|
||||
|
||||
/// Restart policy for `bun run --restart`
|
||||
/// - `no`: No automatic restart (default)
|
||||
/// - `on_failure`: Restart only on non-zero exit
|
||||
/// - `always`: Always restart regardless of exit code
|
||||
/// - `unless_stopped`: In CLI context, behaves like `on_failure` (no persistent state between runs)
|
||||
pub const RestartPolicy = enum {
|
||||
no,
|
||||
on_failure,
|
||||
always,
|
||||
unless_stopped,
|
||||
|
||||
pub fn fromString(str: []const u8) ?RestartPolicy {
|
||||
if (strings.eqlComptime(str, "no")) return .no;
|
||||
if (strings.eqlComptime(str, "on-failure")) return .on_failure;
|
||||
if (strings.eqlComptime(str, "always")) return .always;
|
||||
if (strings.eqlComptime(str, "unless-stopped")) return .unless_stopped;
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
pub const RuntimeOptions = struct {
|
||||
smol: bool = false,
|
||||
debugger: Debugger = .{ .unspecified = {} },
|
||||
@@ -385,6 +405,7 @@ pub const Command = struct {
|
||||
expose_gc: bool = false,
|
||||
preserve_symlinks_main: bool = false,
|
||||
console_depth: ?u16 = null,
|
||||
restart_policy: RestartPolicy = .no,
|
||||
};
|
||||
|
||||
var global_cli_ctx: Context = undefined;
|
||||
|
||||
@@ -119,6 +119,7 @@ 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("-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,
|
||||
clap.parseParam("--restart <STR> Configure the restart policy. One of \"no\" (default), \"on-failure\", \"always\", \"unless-stopped\"") catch unreachable,
|
||||
clap.parseParam("--workspaces Run a script in all workspace packages (from the \"workspaces\" field in package.json)") catch unreachable,
|
||||
};
|
||||
|
||||
@@ -1202,6 +1203,16 @@ pub fn parse(allocator: std.mem.Allocator, ctx: Command.Context, comptime cmd: C
|
||||
}
|
||||
}
|
||||
|
||||
if (args.option("--restart")) |restart_policy| {
|
||||
if (Command.RestartPolicy.fromString(restart_policy)) |policy| {
|
||||
ctx.runtime_options.restart_policy = policy;
|
||||
} else {
|
||||
Output.prettyErrorln("<r><red>error<r>: Invalid restart policy: \"{s}\". Valid options are: no, on-failure, always, unless-stopped", .{restart_policy});
|
||||
Output.flush();
|
||||
Global.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.define) |define| {
|
||||
if (define.keys.len > 0)
|
||||
bun.jsc.RuntimeTranspilerCache.is_disabled = true;
|
||||
|
||||
@@ -205,7 +205,7 @@ pub const RunCommand = struct {
|
||||
|
||||
const log = Output.scoped(.RUN, .visible);
|
||||
|
||||
pub fn runPackageScriptForeground(
|
||||
pub fn runPackageScriptOnce(
|
||||
ctx: Command.Context,
|
||||
allocator: std.mem.Allocator,
|
||||
original_script: string,
|
||||
@@ -215,7 +215,7 @@ pub const RunCommand = struct {
|
||||
passthrough: []const string,
|
||||
silent: bool,
|
||||
use_system_shell: bool,
|
||||
) !void {
|
||||
) !u32 {
|
||||
const shell_bin = findShell(env.get("PATH") orelse "", cwd) orelse return error.MissingShell;
|
||||
env.map.put("npm_lifecycle_event", name) catch unreachable;
|
||||
env.map.put("npm_lifecycle_script", original_script) catch unreachable;
|
||||
@@ -252,7 +252,7 @@ pub const RunCommand = struct {
|
||||
Output.prettyErrorln("<r><red>error<r>: Failed to run script <b>{s}<r> due to error <b>{s}<r>", .{ name, @errorName(err) });
|
||||
}
|
||||
|
||||
Global.exit(1);
|
||||
return 1;
|
||||
};
|
||||
|
||||
if (code > 0) {
|
||||
@@ -261,10 +261,10 @@ pub const RunCommand = struct {
|
||||
Output.flush();
|
||||
}
|
||||
|
||||
Global.exit(code);
|
||||
return code;
|
||||
}
|
||||
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
|
||||
const argv = [_]string{
|
||||
@@ -302,7 +302,7 @@ pub const RunCommand = struct {
|
||||
}
|
||||
|
||||
Output.flush();
|
||||
return;
|
||||
return 1;
|
||||
})) {
|
||||
.err => |err| {
|
||||
if (!silent) {
|
||||
@@ -310,7 +310,7 @@ pub const RunCommand = struct {
|
||||
}
|
||||
|
||||
Output.flush();
|
||||
return;
|
||||
return 1;
|
||||
},
|
||||
.result => |result| result,
|
||||
};
|
||||
@@ -334,7 +334,7 @@ pub const RunCommand = struct {
|
||||
Output.flush();
|
||||
}
|
||||
|
||||
Global.exit(exit_code.code);
|
||||
return exit_code.code;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -357,13 +357,64 @@ pub const RunCommand = struct {
|
||||
}
|
||||
|
||||
Output.flush();
|
||||
return;
|
||||
return 1;
|
||||
},
|
||||
|
||||
else => {},
|
||||
}
|
||||
|
||||
return;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Package script runner with restart support
|
||||
pub fn runPackageScriptForeground(
|
||||
ctx: Command.Context,
|
||||
allocator: std.mem.Allocator,
|
||||
original_script: string,
|
||||
name: string,
|
||||
cwd: string,
|
||||
env: *DotEnv.Loader,
|
||||
passthrough: []const string,
|
||||
silent: bool,
|
||||
use_system_shell: bool,
|
||||
) !void {
|
||||
const restart_policy = ctx.runtime_options.restart_policy;
|
||||
|
||||
// If no restart policy, run once
|
||||
if (restart_policy == .no) {
|
||||
const exit_code = try runPackageScriptOnce(ctx, allocator, original_script, name, cwd, env, passthrough, silent, use_system_shell);
|
||||
if (exit_code != 0) {
|
||||
Global.exit(exit_code);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Restart logic - follow Docker model (no hardcoded limits)
|
||||
var restart_count: u32 = 0;
|
||||
|
||||
while (true) {
|
||||
const exit_code = try runPackageScriptOnce(ctx, allocator, original_script, name, cwd, env, passthrough, silent, use_system_shell);
|
||||
|
||||
const should_restart = switch (restart_policy) {
|
||||
.no => false,
|
||||
.on_failure => exit_code != 0,
|
||||
.always => true,
|
||||
.unless_stopped => exit_code != 0,
|
||||
};
|
||||
|
||||
if (!should_restart) {
|
||||
if (exit_code != 0) {
|
||||
Global.exit(exit_code);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
restart_count += 1;
|
||||
|
||||
if (!silent) {
|
||||
Output.prettyln("<d>Restarting script '{s}' (attempt {d})...<r>", .{ name, restart_count + 1 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// When printing error messages from 'bun run', attribute bun overridden node.js to bun
|
||||
@@ -1238,6 +1289,152 @@ pub const RunCommand = struct {
|
||||
Output.flush();
|
||||
}
|
||||
|
||||
fn _bootWithRestart(ctx: Command.Context, path: string, loader: ?bun.options.Loader) bool {
|
||||
const restart_policy = ctx.runtime_options.restart_policy;
|
||||
|
||||
// If no restart policy, run once directly using in-process execution
|
||||
if (restart_policy == .no) {
|
||||
return _bootAndHandleError(ctx, path, loader);
|
||||
}
|
||||
|
||||
// With restart policy, spawn as subprocess to enable clean restarts
|
||||
// This differs from package.json script restarts which run in-process.
|
||||
var restart_count: u32 = 0;
|
||||
|
||||
while (true) {
|
||||
const exit_code = _runFileAsSubprocess(ctx, path) catch |err| {
|
||||
Output.prettyErrorln("<r><red>error<r>: Failed to run <b>{s}<r> due to error <b>{s}<r>", .{
|
||||
std.fs.path.basename(path),
|
||||
@errorName(err),
|
||||
});
|
||||
Global.exit(1);
|
||||
};
|
||||
|
||||
// Note: `unless_stopped` behaves like `on_failure` in CLI context.
|
||||
// In a container orchestrator, "unless-stopped" would persist across restarts,
|
||||
// but in CLI we treat it the same as on-failure for simplicity.
|
||||
const should_restart = switch (restart_policy) {
|
||||
.no => false,
|
||||
.on_failure => exit_code != 0,
|
||||
.always => true,
|
||||
.unless_stopped => exit_code != 0,
|
||||
};
|
||||
|
||||
if (!should_restart) {
|
||||
if (exit_code != 0) {
|
||||
Global.exit(exit_code);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
restart_count += 1;
|
||||
Output.prettyln("<d>Restarting (attempt {d})...<r>", .{restart_count + 1});
|
||||
Output.flush();
|
||||
|
||||
// Add throttling after 5 restarts to prevent tight restart loops
|
||||
if (restart_count >= 5) {
|
||||
std.time.sleep(1 * std.time.ns_per_s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns a subprocess to run a file, enabling clean restarts.
|
||||
/// The parent process handles the restart loop; the subprocess runs without --restart.
|
||||
/// This differs from package.json scripts which use Global.exit for in-process restarts.
|
||||
fn _runFileAsSubprocess(ctx: Command.Context, path: string) !u8 {
|
||||
var arena = bun.ArenaAllocator.init(ctx.allocator);
|
||||
defer arena.deinit();
|
||||
const allocator = arena.allocator();
|
||||
|
||||
// Build command: bun <path> [args...]
|
||||
// IMPORTANT: We don't pass --restart flag to the subprocess to avoid infinite recursion.
|
||||
// The parent process (this function's caller) handles the restart logic.
|
||||
var cmd_args = std.ArrayList([]const u8).init(allocator);
|
||||
const bun_exe = bun.selfExePath() catch return error.FailedToGetSelfExe;
|
||||
try cmd_args.append(bun_exe);
|
||||
try cmd_args.append(path);
|
||||
|
||||
// Add any additional positional args (skip the first one which is the path)
|
||||
// Also skip --restart flag to avoid infinite recursion
|
||||
if (ctx.positionals.len > 1) {
|
||||
var skip_next = false;
|
||||
for (ctx.positionals[1..]) |arg| {
|
||||
if (skip_next) {
|
||||
skip_next = false;
|
||||
continue;
|
||||
}
|
||||
// Skip --restart and its value
|
||||
if (strings.hasPrefixComptime(arg, "--restart=")) continue;
|
||||
if (strings.eqlComptime(arg, "--restart")) {
|
||||
skip_next = true;
|
||||
continue;
|
||||
}
|
||||
try cmd_args.append(arg);
|
||||
}
|
||||
}
|
||||
|
||||
const spawn_result = switch ((bun.spawnSync(&.{
|
||||
.argv = cmd_args.items,
|
||||
.argv0 = null,
|
||||
.envp = null, // Inherit parent environment
|
||||
.cwd = ctx.args.absolute_working_dir orelse "",
|
||||
.stderr = .buffer,
|
||||
.stdout = .buffer,
|
||||
.stdin = .ignore,
|
||||
.windows = if (Environment.isWindows) .{
|
||||
.loop = jsc.EventLoopHandle.init(jsc.MiniEventLoop.initGlobal(null, null)),
|
||||
},
|
||||
}) catch |err| {
|
||||
Output.prettyErrorln("<r><red>error<r>: Failed to spawn process: {s}", .{@errorName(err)});
|
||||
return err;
|
||||
})) {
|
||||
.result => |r| r,
|
||||
.err => |err| {
|
||||
Output.prettyErrorln("<r><red>error<r>: Failed to spawn: {}", .{err});
|
||||
return error.SpawnError;
|
||||
},
|
||||
};
|
||||
|
||||
// Print buffered output
|
||||
if (spawn_result.stdout.items.len > 0) {
|
||||
_ = Output.writer().write(spawn_result.stdout.items) catch {};
|
||||
}
|
||||
if (spawn_result.stderr.items.len > 0) {
|
||||
_ = Output.errorWriter().write(spawn_result.stderr.items) catch {};
|
||||
}
|
||||
Output.flush();
|
||||
|
||||
switch (spawn_result.status) {
|
||||
.exited => |exit_info| {
|
||||
if (exit_info.signal.valid() and exit_info.signal != .SIGINT) {
|
||||
Output.prettyErrorln("<r><red>error<r>: Process terminated by signal {}<r>", .{exit_info.signal.fmt(Output.enable_ansi_colors_stderr)});
|
||||
if (bun.getRuntimeFeatureFlag(.BUN_INTERNAL_SUPPRESS_CRASH_IN_BUN_RUN)) {
|
||||
bun.crash_handler.suppressReporting();
|
||||
}
|
||||
Global.raiseIgnoringPanicHandler(exit_info.signal);
|
||||
}
|
||||
return exit_info.code;
|
||||
},
|
||||
.signaled => |signal| {
|
||||
if (signal.valid() and signal != .SIGINT) {
|
||||
Output.prettyErrorln("<r><red>error<r>: Process terminated by signal {}<r>", .{signal.fmt(Output.enable_ansi_colors_stderr)});
|
||||
}
|
||||
if (bun.getRuntimeFeatureFlag(.BUN_INTERNAL_SUPPRESS_CRASH_IN_BUN_RUN)) {
|
||||
bun.crash_handler.suppressReporting();
|
||||
}
|
||||
Global.raiseIgnoringPanicHandler(signal);
|
||||
},
|
||||
.err => |err| {
|
||||
Output.prettyErrorln("<r><red>error<r>: Failed to run process: {}", .{err});
|
||||
return error.SpawnError;
|
||||
},
|
||||
else => {
|
||||
Output.prettyErrorln("<r><red>error<r>: Unexpected process status", .{});
|
||||
return error.UnexpectedStatus;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn _bootAndHandleError(ctx: Command.Context, path: string, loader: ?bun.options.Loader) bool {
|
||||
Global.configureAllocator(.{ .long_running = true });
|
||||
Run.boot(ctx, ctx.allocator.dupe(u8, path) catch return false, loader) catch |err| {
|
||||
@@ -1324,7 +1521,7 @@ pub const RunCommand = struct {
|
||||
bun.cli.Arguments.loadConfigPath(ctx.allocator, true, "bunfig.toml", ctx, .RunCommand) catch {};
|
||||
}
|
||||
|
||||
_ = _bootAndHandleError(ctx, absolute_script_path.?, null);
|
||||
_ = _bootWithRestart(ctx, absolute_script_path.?, null);
|
||||
return true;
|
||||
}
|
||||
pub fn exec(
|
||||
@@ -1528,7 +1725,7 @@ pub const RunCommand = struct {
|
||||
const loader: bun.options.Loader = this_transpiler.options.loaders.get(path.name.ext) orelse .tsx;
|
||||
if (loader.canBeRunByBun() or loader == .html) {
|
||||
log("Resolved to: `{s}`", .{path.text});
|
||||
return _bootAndHandleError(ctx, path.text, loader);
|
||||
return _bootWithRestart(ctx, path.text, loader);
|
||||
} else {
|
||||
log("Resolved file `{s}` but ignoring because loader is {s}", .{ path.text, @tagName(loader) });
|
||||
}
|
||||
@@ -1536,7 +1733,7 @@ pub const RunCommand = struct {
|
||||
// Support globs for HTML entry points.
|
||||
if (strings.hasSuffixComptime(target_name, ".html")) {
|
||||
if (strings.containsChar(target_name, '*')) {
|
||||
return _bootAndHandleError(ctx, target_name, .html);
|
||||
return _bootWithRestart(ctx, target_name, .html);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,10 +60,11 @@
|
||||
"msgpackr-extract": "3.0.2",
|
||||
"msw": "2.3.0",
|
||||
"mysql2": "3.7.0",
|
||||
"no": "^0.0.1",
|
||||
"node-gyp": "10.0.1",
|
||||
"nodemailer": "6.9.3",
|
||||
"p-queue": "8.1.0",
|
||||
"peechy": "^0.4.310",
|
||||
"peechy": "0.4.310",
|
||||
"pg": "8.11.1",
|
||||
"pg-connection-string": "2.6.1",
|
||||
"pg-gateway": "0.3.0-beta.4",
|
||||
@@ -1082,6 +1083,8 @@
|
||||
|
||||
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
|
||||
|
||||
"coffee-script": ["coffee-script@1.12.7", "", { "bin": { "coffee": "./bin/coffee", "cake": "./bin/cake" } }, "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw=="],
|
||||
|
||||
"color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
@@ -1928,6 +1931,8 @@
|
||||
|
||||
"nlcst-to-string": ["nlcst-to-string@4.0.0", "", { "dependencies": { "@types/nlcst": "^2.0.0" } }, "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA=="],
|
||||
|
||||
"no": ["no@0.0.1", "", { "dependencies": { "coffee-script": "*" } }, "sha512-9fDwk9njTDe89ip96JobYnmtWOGv+EkvyP2ErzxuLPr4WG6fGjl9BWGOj09Bc7sP26p4zvemhWRNdp65T0eX+g=="],
|
||||
|
||||
"no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="],
|
||||
|
||||
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
|
||||
|
||||
350
test/cli/restart.test.ts
Normal file
350
test/cli/restart.test.ts
Normal file
@@ -0,0 +1,350 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDirWithFiles } from "harness";
|
||||
import { resolve } from "path";
|
||||
|
||||
test("--restart flag parsing", () => {
|
||||
// This test just verifies the flag is recognized (will fail if unknown flag)
|
||||
expect(() => {
|
||||
Bun.spawnSync({
|
||||
cmd: [bunExe(), "run", "--restart", "no", "--help"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test("--restart with invalid policy shows error", async () => {
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "run", "--restart", "invalid", "non-existent-file.js"],
|
||||
env: bunEnv,
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stderr, exitCode] = await Promise.all([proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stderr).toContain("Invalid restart policy");
|
||||
expect(stderr).toContain("no, on-failure, always, unless-stopped");
|
||||
});
|
||||
|
||||
test("--restart=no does not restart on success", async () => {
|
||||
const dir = tempDirWithFiles("restart-no", {
|
||||
"success.js": `
|
||||
console.log("This script succeeds");
|
||||
process.exit(0);
|
||||
`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "run", "--restart", "no", resolve(dir, "success.js")],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toContain("This script succeeds");
|
||||
// Should only appear once, not restarted
|
||||
expect(stdout.split("This script succeeds").length - 1).toBe(1);
|
||||
});
|
||||
|
||||
test("--restart=no does not restart on failure", async () => {
|
||||
const dir = tempDirWithFiles("restart-no-fail", {
|
||||
"fail.js": `
|
||||
console.log("This script fails");
|
||||
process.exit(1);
|
||||
`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "run", "--restart", "no", resolve(dir, "fail.js")],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stdout).toContain("This script fails");
|
||||
// Should only appear once, not restarted
|
||||
expect(stdout.split("This script fails").length - 1).toBe(1);
|
||||
});
|
||||
|
||||
test("--restart=on-failure restarts on failure", async () => {
|
||||
const dir = tempDirWithFiles("restart-on-failure", {
|
||||
"counter.js": `
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const counterFile = path.join(__dirname, 'restart-counter.txt');
|
||||
|
||||
let count = 0;
|
||||
if (fs.existsSync(counterFile)) {
|
||||
count = parseInt(fs.readFileSync(counterFile, 'utf8') || '0', 10);
|
||||
}
|
||||
count++;
|
||||
fs.writeFileSync(counterFile, count.toString());
|
||||
|
||||
console.log(\`Attempt \${count}\`);
|
||||
|
||||
// Fail first two attempts, succeed on third
|
||||
if (count < 3) {
|
||||
process.exit(1);
|
||||
} else {
|
||||
process.exit(0);
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const result1 = Bun.spawnSync({
|
||||
cmd: [bunExe(), "run", "--restart", "on-failure", resolve(dir, "counter.js")],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(result1.exitCode).toBe(0); // Should eventually succeed
|
||||
|
||||
// Verify restarts happened by checking the counter file
|
||||
const counterFile = resolve(dir, "restart-counter.txt");
|
||||
const fs = require("fs");
|
||||
if (fs.existsSync(counterFile)) {
|
||||
const finalCount = parseInt(fs.readFileSync(counterFile, "utf8"), 10);
|
||||
expect(finalCount).toBe(3); // Should have run 3 times (2 failures + 1 success)
|
||||
}
|
||||
});
|
||||
|
||||
test("--restart=on-failure does not restart on success", async () => {
|
||||
const dir = tempDirWithFiles("restart-on-success", {
|
||||
"success.js": `
|
||||
console.log("Success script");
|
||||
process.exit(0);
|
||||
`,
|
||||
});
|
||||
|
||||
const result = Bun.spawnSync({
|
||||
cmd: [bunExe(), "run", "--restart", "on-failure", resolve(dir, "success.js")],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
const stdout = result.stdout.toString();
|
||||
expect(stdout).toContain("Success script");
|
||||
// Should only appear once, not restarted
|
||||
expect(stdout.split("Success script").length - 1).toBe(1);
|
||||
});
|
||||
|
||||
test("--restart=always restarts on both success and failure", async () => {
|
||||
const dir = tempDirWithFiles("restart-always", {
|
||||
"counter.js": `
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const counterFile = path.join(__dirname, 'always-counter.txt');
|
||||
|
||||
let count = 0;
|
||||
if (fs.existsSync(counterFile)) {
|
||||
count = parseInt(fs.readFileSync(counterFile, 'utf8') || '0', 10);
|
||||
}
|
||||
count++;
|
||||
fs.writeFileSync(counterFile, count.toString());
|
||||
|
||||
console.log(\`Always attempt \${count}\`);
|
||||
|
||||
// Force exit after 3 attempts to avoid infinite loop in tests
|
||||
if (count >= 3) {
|
||||
process.exit(1); // Exit with failure to break the loop
|
||||
} else {
|
||||
process.exit(0); // Should restart even on success
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "run", "--restart", "always", resolve(dir, "counter.js")],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
// Give it time to restart a few times, then kill
|
||||
setTimeout(() => proc.kill(), 500);
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
// Should have been killed by timeout
|
||||
expect(exitCode).not.toBe(0);
|
||||
|
||||
// Check that restart actually happened by reading the counter file
|
||||
// Wait a bit for filesystem writes to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
const counterFile = resolve(dir, "always-counter.txt");
|
||||
const fs = require("fs");
|
||||
if (fs.existsSync(counterFile)) {
|
||||
const finalCount = parseInt(fs.readFileSync(counterFile, "utf8"), 10);
|
||||
// Should have restarted at least once (count >= 2)
|
||||
expect(finalCount).toBeGreaterThanOrEqual(2);
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
test("--restart=unless-stopped restarts on failure", async () => {
|
||||
const dir = tempDirWithFiles("restart-unless-stopped", {
|
||||
"counter-fail.js": `
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const counterFile = path.join(__dirname, 'unless-stopped-counter.txt');
|
||||
|
||||
let count = 0;
|
||||
if (fs.existsSync(counterFile)) {
|
||||
count = parseInt(fs.readFileSync(counterFile, 'utf8') || '0', 10);
|
||||
}
|
||||
count++;
|
||||
fs.writeFileSync(counterFile, count.toString());
|
||||
|
||||
console.log(\`Unless-stopped fail attempt \${count}\`);
|
||||
|
||||
// Succeed after 3 attempts
|
||||
if (count >= 3) {
|
||||
process.exit(0);
|
||||
} else {
|
||||
process.exit(1);
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
const proc1 = Bun.spawn({
|
||||
cmd: [bunExe(), "run", "--restart", "unless-stopped", resolve(dir, "counter-fail.js")],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
timeout_ms: 10000,
|
||||
});
|
||||
|
||||
const [stdout1, stderr1, exitCode1] = await Promise.all([proc1.stdout.text(), proc1.stderr.text(), proc1.exited]);
|
||||
|
||||
expect(exitCode1).toBe(0);
|
||||
expect(stdout1).toContain("Unless-stopped fail attempt 1");
|
||||
expect(stdout1).toContain("Unless-stopped fail attempt 2");
|
||||
expect(stdout1).toContain("Unless-stopped fail attempt 3");
|
||||
});
|
||||
|
||||
test("--restart=unless-stopped does not restart on success", async () => {
|
||||
const dir = tempDirWithFiles("restart-unless-stopped-success", {
|
||||
"success-stop.js": `
|
||||
console.log("Unless-stopped success");
|
||||
process.exit(0);
|
||||
`,
|
||||
});
|
||||
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "run", "--restart", "unless-stopped", resolve(dir, "success-stop.js")],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toContain("Unless-stopped success");
|
||||
// Should only appear once, not restarted
|
||||
expect(stdout.split("Unless-stopped success").length - 1).toBe(1);
|
||||
});
|
||||
|
||||
test("--restart works with package.json scripts", async () => {
|
||||
const dir = tempDirWithFiles("restart-pkg-script", {
|
||||
"package.json": JSON.stringify({
|
||||
name: "restart-test",
|
||||
scripts: {
|
||||
"fail-script": "node fail-counter.js",
|
||||
"success-script": "node success.js",
|
||||
},
|
||||
}),
|
||||
"fail-counter.js": `
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const counterFile = path.join(__dirname, 'pkg-script-counter.txt');
|
||||
|
||||
let count = 0;
|
||||
if (fs.existsSync(counterFile)) {
|
||||
count = parseInt(fs.readFileSync(counterFile, 'utf8') || '0', 10);
|
||||
}
|
||||
count++;
|
||||
fs.writeFileSync(counterFile, count.toString());
|
||||
|
||||
console.log(\`Package script attempt \${count}\`);
|
||||
|
||||
// Succeed after 2 attempts
|
||||
if (count >= 2) {
|
||||
process.exit(0);
|
||||
} else {
|
||||
process.exit(1);
|
||||
}
|
||||
`,
|
||||
"success.js": `
|
||||
console.log("Package script success");
|
||||
process.exit(0);
|
||||
`,
|
||||
});
|
||||
|
||||
// Test restart on failure with package script
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "run", "--restart", "on-failure", "fail-script"],
|
||||
env: bunEnv,
|
||||
cwd: dir,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
timeout_ms: 10000,
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toContain("Package script attempt 1");
|
||||
expect(stdout).toContain("Package script attempt 2");
|
||||
}, 15000);
|
||||
|
||||
test("--restart flag is available but has no effect on install command", async () => {
|
||||
// Test that --restart flag doesn't break install but has no restart behavior
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "install", "--restart", "no"],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
// Install should work normally and exit successfully
|
||||
expect(exitCode).toBe(0);
|
||||
// Should not show any restart-related behavior or errors
|
||||
expect(stderr).not.toContain("Invalid restart policy");
|
||||
});
|
||||
|
||||
test("multiple --restart flags uses the last one", async () => {
|
||||
const dir = tempDirWithFiles("restart-multiple", {
|
||||
"test.js": `
|
||||
console.log("Multiple restart flags test");
|
||||
process.exit(0);
|
||||
`,
|
||||
});
|
||||
|
||||
// Last --restart flag should win
|
||||
const proc = Bun.spawn({
|
||||
cmd: [bunExe(), "run", "--restart", "always", "--restart", "no", resolve(dir, "test.js")],
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(exitCode).toBe(0);
|
||||
expect(stdout).toContain("Multiple restart flags test");
|
||||
// Should only appear once since last flag was "no"
|
||||
expect(stdout.split("Multiple restart flags test").length - 1).toBe(1);
|
||||
});
|
||||
@@ -65,6 +65,7 @@
|
||||
"msgpackr-extract": "3.0.2",
|
||||
"msw": "2.3.0",
|
||||
"mysql2": "3.7.0",
|
||||
"no": "^0.0.1",
|
||||
"node-gyp": "10.0.1",
|
||||
"nodemailer": "6.9.3",
|
||||
"p-queue": "8.1.0",
|
||||
|
||||
Reference in New Issue
Block a user