From 61eb3e4b2178a407b5ebe3bd008e82acfaa5a2a8 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari MacBook Date: Sat, 25 Jul 2026 01:32:29 +0000 Subject: [PATCH 01/11] Restore reject_bad_negations initializer lost in branch merge --- src/runtime/cli/Arguments.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/runtime/cli/Arguments.rs b/src/runtime/cli/Arguments.rs index 13b3531ee016..36c1e419a24e 100644 --- a/src/runtime/cli/Arguments.rs +++ b/src/runtime/cli/Arguments.rs @@ -780,6 +780,10 @@ pub fn parse(cmd: CommandTag, ctx: Context<'_>) -> crate::Result 1, _ => 0, }, + reject_bad_negations: matches!( + cmd, + CommandTag::AutoCommand | CommandTag::RunCommand | CommandTag::RunAsNodeCommand + ), // Only the paths standing in for `node` get node's aliases. short_aliases: match cmd { CommandTag::AutoCommand | CommandTag::RunAsNodeCommand => NODE_SHORT_ALIASES, From be6148fc9b4f4c4201ee4ce657d5bfe54e224534 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari MacBook Date: Sat, 25 Jul 2026 02:39:44 +0000 Subject: [PATCH 02/11] cli: restore -e/-p/--check/--inspect-port handling and fs arg validation dropped by branch merges The branch merge kept the bun_clap side of the Node-style CLI work but dropped its Arguments.rs consumers, leaving -e/-p/--eval/--print dead (every invocation fell through to help), --check/-c unregistered (a panic in ComptimeClap::find), --inspect-port/--debug-port unregistered, and the NODE_OPTIONS validation + node-style missing-value errors gone. Same story for node_fs.rs: types.rs kept from_js_required and BUFFER_EXPECTED_TYPES but the call sites converting fs argument validation to Node's error messages were dropped (dead-code errors). Also adds Node's ERR_MISSING_OPTION guard: any --allow-* flag without --permission exits 1 with 'TypeError [ERR_MISSING_OPTION]: --permission is required' (initializePermission, pre_execution.js), and updates the now-obsolete '--permission is not supported by Bun' test to assert the model is actually enabled. --- src/runtime/cli/Arguments.rs | 339 ++++++++++++++++++++++++++++------ src/runtime/node/node_fs.rs | 59 +++--- test/cli/run/run-eval.test.ts | 9 +- 3 files changed, 324 insertions(+), 83 deletions(-) diff --git a/src/runtime/cli/Arguments.rs b/src/runtime/cli/Arguments.rs index 36c1e419a24e..99ed68e8831b 100644 --- a/src/runtime/cli/Arguments.rs +++ b/src/runtime/cli/Arguments.rs @@ -89,21 +89,42 @@ macro_rules! maybe_verbose_error_trace { }; } +const BASE_HEAD_PARAMS: &[ParamType] = &[ + parse_param!( + "--env-file ... Load environment variables from the specified file(s)" + ), + parse_param!("--no-env-file Disable automatic loading of .env files"), + parse_param!( + "--cwd Absolute path to resolve files & entry points from. This just changes the process' cwd." + ), +]; + +const BASE_TAIL_PARAMS: &[ParamType] = &[parse_param!( + "-h, --help Display this menu and exit" +)]; + +/// Shared by every subcommand that keeps `-c` as the `--config` shorthand. pub(crate) const BASE_PARAMS_: &[ParamType] = concat_params!( maybe_debug_params!(), - &[ - parse_param!( - "--env-file ... Load environment variables from the specified file(s)" - ), - parse_param!("--no-env-file Disable automatic loading of .env files"), - parse_param!( - "--cwd Absolute path to resolve files & entry points from. This just changes the process' cwd." - ), - parse_param!( - "-c, --config ? Specify path to Bun config file. Default $cwd/bunfig.toml" - ), - parse_param!("-h, --help Display this menu and exit"), - ], + BASE_HEAD_PARAMS, + &[parse_param!( + "-c, --config ? Specify path to Bun config file. Default $cwd/bunfig.toml" + )], + BASE_TAIL_PARAMS, + maybe_verbose_error_trace!(), + &[parse_param!("...")], +); + +/// Same as [`BASE_PARAMS_`], but `--config` has no `-c` shorthand: the runtime +/// commands give `-c` to `--check` for Node compatibility, so advertising it +/// here too would document an alias that never resolves to `--config`. +pub(crate) const BASE_PARAMS_NO_CONFIG_SHORT_: &[ParamType] = concat_params!( + maybe_debug_params!(), + BASE_HEAD_PARAMS, + &[parse_param!( + "--config ? Specify path to Bun config file. Default $cwd/bunfig.toml" + )], + BASE_TAIL_PARAMS, maybe_verbose_error_trace!(), &[parse_param!("...")], ); @@ -190,6 +211,10 @@ pub(crate) const RUNTIME_PARAMS_: &[ParamType] = &[ parse_param!( "--inspect-brk ? Activate Bun's debugger, set breakpoint on first line of code and wait" ), + parse_param!( + "--inspect-port Set the default [host:]port used when the debugger is activated with --inspect" + ), + parse_param!("--debug-port "), parse_param!( "--cpu-prof Start CPU profiler and write profile to disk on exit" ), @@ -232,9 +257,12 @@ pub(crate) const RUNTIME_PARAMS_: &[ParamType] = &[ parse_param!( "-i Auto-install dependencies during execution. Equivalent to --install=fallback." ), - parse_param!("-e, --eval Evaluate argument as a script"), + parse_param!("-e, --eval ! Evaluate argument as a script"), + parse_param!( + "-p, --print ?! Evaluate argument as a script and print the result" + ), parse_param!( - "-p, --print Evaluate argument as a script and print the result" + "--input-type Module type for string input from stdin or --eval: \"module\" or \"commonjs\"" ), parse_param!( "--prefer-offline Skip staleness checks for packages in the Bun runtime and resolve from disk" @@ -267,6 +295,7 @@ pub(crate) const RUNTIME_PARAMS_: &[ParamType] = &[ parse_param!( "--no-deprecation Suppress all reporting of the custom deprecation." ), + parse_param!("--no-warnings Suppress all process warnings."), parse_param!( "--throw-deprecation Determine whether or not deprecation warnings result in errors." ), @@ -337,6 +366,12 @@ pub(crate) const RUNTIME_PARAMS_: &[ParamType] = &[ ]; pub(crate) const AUTO_OR_RUN_PARAMS: &[ParamType] = &[ + // `-c` means `--check` for the runtime commands (Node.js compatibility). + // The AUTO/RUN tables pair this with BASE_PARAMS_NO_CONFIG_SHORT_ so + // `--config` keeps its long form and nothing else claims `-c`. + parse_param!( + "-c, --check Check the syntax of the entry point (or stdin) without executing it" + ), parse_param!( "-F, --filter ... Run a script in all workspace packages matching the pattern" ), @@ -379,7 +414,7 @@ pub(crate) const AUTO_PARAMS: &[ParamType] = concat_params!( AUTO_ONLY_PARAMS, RUNTIME_PARAMS_, TRANSPILER_PARAMS_, - BASE_PARAMS_ + BASE_PARAMS_NO_CONFIG_SHORT_ ); pub(crate) const RUN_ONLY_PARAMS: &[ParamType] = concat_params!( @@ -395,7 +430,7 @@ pub(crate) const RUN_PARAMS: &[ParamType] = concat_params!( RUN_ONLY_PARAMS, RUNTIME_PARAMS_, TRANSPILER_PARAMS_, - BASE_PARAMS_ + BASE_PARAMS_NO_CONFIG_SHORT_ ); const BAKE_DEBUG_PARAMS: &[ParamType] = &[ @@ -757,6 +792,102 @@ pub(crate) static Bun__Node__UseSystemCA: core::sync::atomic::AtomicBool = // `crate::cli::arguments::load_config*` callers are unaffected. pub use bun_bunfig::arguments::{load_config, load_config_path, load_config_with_cmd_args}; +/// The string Node prefixes its CLI errors with. Same source as +/// `process.execPath` (node_process::get_exec_path), so the prefix matches what +/// scripts observe. +fn node_error_prefix() -> &'static [u8] { + bun_core::self_exe_path() + .map(|p| p.as_bytes()) + .unwrap_or(b"bun") +} + +/// Print Node's missing-argument error for runtime CLI flags Bun borrows from +/// Node.js (`: requires an argument`) and exit with code 9, +/// Node's exit code for invalid command-line arguments. +#[cold] +#[inline(never)] +fn exit_node_requires_argument(flag: &[u8]) -> ! { + bun_core::pretty_errorln!( + "{}: {} requires an argument", + BStr::new(node_error_prefix()), + BStr::new(flag) + ); + Output::flush(); + Global::exit(9); +} + +/// Options Node refuses to accept through the NODE_OPTIONS environment +/// variable: the ones that change what the process executes. +/// https://github.com/nodejs/node/blob/v26.3.0/src/node_options.cc +const NODE_OPTIONS_DISALLOWED: &[&[u8]] = &[ + b"-v", + b"--version", + b"-h", + b"--help", + b"-e", + b"--eval", + b"-p", + b"--print", + b"-pe", + b"-c", + b"--check", + b"-i", + b"--interactive", + b"--v8-options", + b"--test", + b"--", + b"--expose-internals", +]; + +/// Reject NODE_OPTIONS values Node itself refuses, with Node's message and +/// exit code 9. Bun does not apply the remaining NODE_OPTIONS entries yet; this +/// only covers the error contract scripts can rely on. +#[cold] +#[inline(never)] +fn validate_node_options(env: &[u8]) { + let mut i = 0usize; + while i < env.len() { + while i < env.len() && env[i].is_ascii_whitespace() { + i += 1; + } + if i >= env.len() { + break; + } + // Tokenize the way Node does: whitespace-separated, double quotes + // group a span containing whitespace. + let mut token: Vec = Vec::new(); + let mut in_quotes = false; + while i < env.len() && (in_quotes || !env[i].is_ascii_whitespace()) { + if env[i] == b'"' { + in_quotes = !in_quotes; + } else { + token.push(env[i]); + } + i += 1; + } + if !token.starts_with(b"-") { + continue; + } + // Compare the option name (before any '='), treating '_' as '-' the + // way Node canonicalizes option names. The message echoes the spelling + // the user wrote. + let name = &token[..token.iter().position(|&b| b == b'=').unwrap_or(token.len())]; + let canonical: Vec = name + .iter() + .map(|&b| if b == b'_' { b'-' } else { b }) + .collect(); + if NODE_OPTIONS_DISALLOWED.contains(&canonical.as_slice()) { + bun_core::pretty_errorln!( + "{}: {} is not allowed in NODE_OPTIONS", + BStr::new(node_error_prefix()), + BStr::new(name) + ); + Output::flush(); + Global::exit(9); + } + } +} + /// node aliases `-pe` to `--print --eval` as a whole token (node_options.cc): /// it can't be a short in either runtime, being ambiguous with `-p` carrying /// the attached value `e`. Bun's `-p` takes the code, so `-pe X` is `-p X`. @@ -793,6 +924,45 @@ pub fn parse(cmd: CommandTag, ctx: Context<'_>) -> crate::Result a, Err(err) => { + // For runtime flags borrowed from Node.js, report a missing value + // the way `node` does (and with its exit code 9) so scripts that + // branch on Node's CLI error contract behave the same under Bun. + if err == clap::Error::MissingValue + && matches!( + cmd, + CommandTag::AutoCommand | CommandTag::RunCommand | CommandTag::RunAsNodeCommand + ) + { + // `diag.arg` is the argument as written with its leading + // dashes stripped. Node echoes it verbatim, so the long form + // keeps a trailing '=' (`node --eval=` reports + // "--eval= requires an argument"); the short form reports the + // single flag that wanted the value, not the cluster it + // arrived in. + let node_flag: Option> = match (diag.short, diag.long.as_deref()) { + (Some(short @ (b'e' | b'p')), _) => Some(vec![b'-', short]), + (_, Some(b"eval" | b"print" | b"inspect-port" | b"debug-port")) => { + let mut flag = b"--".to_vec(); + flag.extend_from_slice(&diag.arg); + Some(flag) + } + _ => None, + }; + if let Some(flag) = node_flag { + exit_node_requires_argument(&flag); + } + } + if err == clap::Error::InvalidNegation { + // https://github.com/nodejs/node/blob/v26.3.0/src/node_options-inl.h + bun_core::pretty_errorln!( + "{}: --{} is an invalid negation because it is not a boolean option", + BStr::new(node_error_prefix()), + BStr::new(&diag.arg) + ); + Output::flush(); + Global::exit(9); + } + // Report useful error and exit let _ = diag.report(Output::error_writer(), err); command::tag_print_help(cmd, false); @@ -1160,6 +1330,21 @@ pub fn parse(cmd: CommandTag, ctx: Context<'_>) -> crate::Result) -> crate::Result` from working ctx.runtime_options.eval.script = port_str.into(); ctx.runtime_options.eval.eval_and_print = true; + ctx.runtime_options.eval.provided = true; } else { opts.port = match strings::parse_int::(port_str, 10) { Ok(v) => Some(v), @@ -1250,44 +1436,53 @@ pub fn parse(cmd: CommandTag, ctx: Context<'_>) -> crate::Result: -e requires an argument" - // on stderr, exit code 9. - let exe: &[u8] = bun_core::self_exe_path() - .map(|p| p.as_bytes()) - .unwrap_or(b"bun"); - Output::pretty_error(&format_args!( - "{}: -e requires an argument\n", - BStr::new(exe) - )); - Output::flush(); - Global::exit(9); - } - ctx.runtime_options.eval.script = ctx.positionals.remove(0); - } else if let Some(code) = script.strip_prefix(b"--eval=") { - // `node --print --eval=-42`: the attached-value spelling for - // code starting with `-`. - ctx.runtime_options.eval.script = code.into(); - } else { - ctx.runtime_options.eval.script = script.into(); - } - } else if let Some(script) = args.option(b"--eval") { + // Node registers `--print` as a boolean and `--print ` as an alias + // for `-pe`, i.e. `--print --eval `, so -p turns on print mode and + // may also carry the script (`bun -p 42`, `bun -pe 42`, `bun -p -e 42`). + // + // Divergence: because both spellings feed one upstream `--eval` string, + // Node takes whichever came last, so `node -p 7 -e 9` prints 9. Bun's + // parser keeps the two options in separate slots with no relative + // order, so a script on -p wins and it prints 7. + let print_arg = args.option(b"--print"); + let eval_arg = args.option(b"--eval"); + if print_arg.is_some() || eval_arg.is_some() { + // `provided` (not a non-empty script) is what selects eval mode, so + // `bun -e ""` runs an empty program and bare `bun -p` prints + // undefined instead of falling through to help. + ctx.runtime_options.eval.provided = true; + ctx.runtime_options.eval.eval_and_print = print_arg.is_some(); + let script: &[u8] = match (print_arg, eval_arg) { + (Some(print_script), _) if !print_script.is_empty() => print_script, + (_, Some(eval_script)) => eval_script, + (print_script, None) => print_script.unwrap_or_default(), + }; ctx.runtime_options.eval.script = script.into(); } - // node: an eval expression starting with `-` can be escaped with a - // backslash (`node -p "\-42"` prints -42); strip the single leading - // backslash before it reaches the parser. - if ctx.runtime_options.eval.script.starts_with(b"\\-") { - ctx.runtime_options.eval.script = ctx.runtime_options.eval.script[1..].into(); + if let Some(input_type) = args.option(b"--input-type") { + ctx.runtime_options.eval.input_type = input_type.into(); + } + + // Node's CLI contract only applies to the commands that stand in for + // `node`; `--check` is only declared in their tables. + if matches!( + cmd, + CommandTag::AutoCommand | CommandTag::RunCommand | CommandTag::RunAsNodeCommand + ) { + if let Some(node_options) = bun_core::env_var::NODE_OPTIONS::get() { + validate_node_options(node_options); + } + + ctx.runtime_options.check_syntax = args.flag(b"--check"); + if ctx.runtime_options.check_syntax && ctx.runtime_options.eval.provided { + // Node prints this (and exits 9) for `node -c -e foo`. + bun_core::pretty_errorln!( + "{}: either --check or --eval can be used, not both", + BStr::new(node_error_prefix()) + ); + Output::flush(); + Global::exit(9); + } } ctx.runtime_options.if_present = args.flag(b"--if-present"); ctx.runtime_options.smol = args.flag(b"--smol"); @@ -1346,9 +1541,38 @@ pub fn parse(cmd: CommandTag, ctx: Context<'_>) -> crate::Result = + match (args.option(b"--inspect-port"), args.option(b"--debug-port")) { + (Some(value), _) => { + if value.is_empty() { + exit_node_requires_argument(b"--inspect-port="); + } + Some(value) + } + (None, Some(value)) => { + if value.is_empty() { + exit_node_requires_argument(b"--debug-port="); + } + Some(value) + } + (None, None) => None, + }; + let default_debugger_target = || -> Box<[u8]> { + inspect_port_value + .map(Box::<[u8]>::from) + .unwrap_or_default() + }; + if let Some(inspect_flag) = args.option(b"--inspect") { ctx.runtime_options.debugger = if inspect_flag.is_empty() { - Debugger::Enable(Default::default()) + Debugger::Enable(DebuggerEnable { + path_or_port: default_debugger_target(), + ..Default::default() + }) } else { Debugger::Enable(DebuggerEnable { path_or_port: Box::<[u8]>::from(inspect_flag), @@ -1358,6 +1582,7 @@ pub fn parse(cmd: CommandTag, ctx: Context<'_>) -> crate::Result) -> crate::Result) -> crate::Result JsResult { - let path = PathOrFileDescriptor::from_js(ctx, arguments)?.ok_or_else(|| { - ctx.throw_invalid_arguments(format_args!("path must be a string or TypedArray")) - })?; + let path = PathOrFileDescriptor::from_js_required(ctx, arguments, "path")?; let len: u64 = 'brk: { let Some(len_value) = arguments.next() else { break 'brk 0; @@ -3977,14 +3976,35 @@ pub mod args { }, )); } - let max_len = ((buf_len as u64 - args.offset) as i64).min(i32::MAX as i64); - if length > max_len || length < 0 { + // validateOffsetLengthWrite then validateInt32(length, 'length', 0), + // in that order — each stage words its range differently. + let remaining = (buf_len as u64 - args.offset) as i64; + if length > remaining { + return Err(ctx.throw_range_error( + length as f64, + bun_jsc::RangeErrorOptions { + field_name: b"length", + max: remaining, + ..Default::default() + }, + )); + } + if length < 0 { return Err(ctx.throw_range_error( length as f64, bun_jsc::RangeErrorOptions { field_name: b"length", min: 0, - max: max_len, + ..Default::default() + }, + )); + } + if length > i32::MAX as i64 { + return Err(ctx.throw_range_error( + length as f64, + bun_jsc::RangeErrorOptions { + field_name: b"length", + msg: b">= 0 && <= 2147483647", ..Default::default() }, )); @@ -4113,11 +4133,7 @@ pub mod args { 0.0 }; let buffer = Buffer::from_js(ctx, buffer_value).ok_or_else(|| { - ctx.throw_invalid_argument_type_list( - b"buffer", - &[b"Buffer", b"TypedArray", b"DataView"], - buffer_value, - ) + ctx.throw_invalid_argument_type_value2(b"buffer", BUFFER_EXPECTED_TYPES, buffer_value) })?; // if (length === 0) { @@ -4138,9 +4154,12 @@ pub mod args { let buf_len = buffer.slice().len(); if buf_len == 0 { + let received = JSGlobalObject::inspect_for_error_message(ctx, buffer_value)?; return Err(validators::throw_err_invalid_arg_value( ctx, - format_args!("The argument 'buffer' is empty and cannot be written."), + format_args!( + "The argument 'buffer' is empty and cannot be written. Received {received}" + ), )); } // validateOffsetLengthRead(offset, length, buffer.byteLength); @@ -4320,11 +4339,7 @@ pub mod args { pub fn from_js(ctx: &JSGlobalObject, arguments: &mut ArgumentsSlice) -> JsResult { // `Drop` on `path` covers every // `?`-propagated JsError below. - let path = PathOrFileDescriptor::from_js(ctx, arguments)?.ok_or_else(|| { - ctx.throw_invalid_arguments(format_args!( - "path must be a string or a file descriptor" - )) - })?; + let path = PathOrFileDescriptor::from_js_required(ctx, arguments, "path")?; let mut encoding = Encoding::Buffer; let mut flag = FileSystemFlags::R; let mut abort_signal = scopeguard::guard(None::, |s| { @@ -4412,11 +4427,7 @@ pub mod args { ) -> JsResult { // `Drop` on `path` covers every // `?`-propagated JsError below. - let path = PathOrFileDescriptor::from_js(ctx, arguments)?.ok_or_else(|| { - ctx.throw_invalid_arguments(format_args!( - "path must be a string or a file descriptor" - )) - })?; + let path = PathOrFileDescriptor::from_js_required(ctx, arguments, "path")?; let data_value = arguments .next_eat() .ok_or_else(|| ctx.throw_invalid_arguments(format_args!("data is required")))?; diff --git a/test/cli/run/run-eval.test.ts b/test/cli/run/run-eval.test.ts index 7a011da506df..a902cf896428 100644 --- a/test/cli/run/run-eval.test.ts +++ b/test/cli/run/run-eval.test.ts @@ -433,18 +433,17 @@ describe("node-style CLI errors", () => { expect(exitCode).toBe(1); }); - test("--permission is rejected rather than silently ignored", async () => { + test("--permission enables the permission model", async () => { await using proc = Bun.spawn({ - cmd: [bunExe(), "--permission", "-e", "1"], + cmd: [bunExe(), "--permission", "-p", "process.permission.has('fs.write')"], env: bunEnv, stdout: "pipe", stderr: "pipe", }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stdout).toBe(""); - expect(stderr).toContain("--permission is not supported by Bun"); - expect(exitCode).toBe(1); + expect(stdout).toBe("false\n"); + expect(exitCode).toBe(0); }); }); From 9630ecb695e44f13d4df9c0b11b0f05f924b7f78 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari MacBook Date: Sat, 25 Jul 2026 02:39:50 +0000 Subject: [PATCH 03/11] print uncaught errors Node-style: real error name, single-quoted string properties Bun's uncaught-exception printer wrote a lowercase 'error:' prefix for plain Errors (and promoted a 'CODE: '-prefixed message's code into the name slot), double-quoted string properties, and lost the creation stack and property dump when the uncaughtException handler rethrew. Node prints the error's own name ('Error: boom', 'TypeError: x'), single-quotes inspect strings (code: 'ENOENT'), and reports the rethrown error's own stack. - Formatter grows node_uncaught_style, set only on the uncaught/ unhandled print path (print_exception + the jsc_hooks funnel) and off under bun test, so test-runner failure rendering is unchanged. - print_error_name_and_message prints the name verbatim in that mode. - String properties (and the code property) print single-quoted for simple ASCII strings, falling back to the JSON writer otherwise. - The rethrow-inside-uncaughtException path unwraps the JSC::Exception wrapper before printing so the Error's own stack and properties render (previously: rethrow-site frame only, no properties), and util.callbackify's falsy-rejection message drops the stray 'a' to match Node ('Promise was rejected with falsy value'). Unlocks the vendored test-unhandled-exception-rethrow-error.js and (in the following commits) the permission tests that assert /Error: .../ on child stderr. Bun-side assertions on the old rendering are updated in the same commit. --- src/js/node/util.ts | 2 +- src/jsc/ConsoleObject.rs | 38 +++++++++- src/jsc/VirtualMachine.rs | 76 +++++++++++++++++-- src/node-fallbacks/util.js | 2 +- src/runtime/jsc_hooks.rs | 31 ++++++++ test/cli/hot/hot.test.ts | 8 +- test/cli/run/run-autoinstall.test.ts | 6 +- test/cli/run/tsconfig-override.test.ts | 2 +- .../js/bun/http/async-iterator-stream.test.ts | 2 +- .../http/serve-direct-readable-stream.test.ts | 4 +- test/js/bun/http/serve.test.ts | 4 +- test/js/bun/test/stack.test.ts | 12 +-- test/js/bun/util/reportError.test.ts | 2 +- test/js/node/module/sourcemap.test.js | 2 +- test/js/node/process/process.test.js | 20 ++--- .../test-unhandled-exception-rethrow-error.js | 28 +++++++ .../test/parallel/test-util-callbackify.js | 6 +- 17 files changed, 200 insertions(+), 45 deletions(-) create mode 100644 test/js/node/test/parallel/test-unhandled-exception-rethrow-error.js diff --git a/src/js/node/util.ts b/src/js/node/util.ts index db0482ff1826..e6d9c33b018e 100644 --- a/src/js/node/util.ts +++ b/src/js/node/util.ts @@ -253,7 +253,7 @@ var _extend = function (origin, add) { function callbackifyOnRejected(reason, cb) { if (!reason) { - var newReason = new Error("Promise was rejected with a falsy value"); + var newReason = new Error("Promise was rejected with falsy value"); newReason.reason = reason; newReason.code = "ERR_FALSY_VALUE_REJECTION"; reason = newReason; diff --git a/src/jsc/ConsoleObject.rs b/src/jsc/ConsoleObject.rs index 2058d2fbf177..01c34cb22a34 100644 --- a/src/jsc/ConsoleObject.rs +++ b/src/jsc/ConsoleObject.rs @@ -1875,6 +1875,11 @@ pub mod formatter { /// printed as a string. Set true in the error printer so that /// `ShellError` prints a more readable message. pub format_buffer_as_text: bool, + /// Render like Node's uncaught-exception report: the error's own + /// `name` is printed verbatim (`Error: boom`, never Bun's lowercase + /// `error:` alias) and string properties are single-quoted + /// (`code: 'ENOENT'`). Set only on the uncaught/unhandled print path. + pub node_uncaught_style: bool, } impl<'a> Formatter<'a> { @@ -1905,6 +1910,7 @@ pub mod formatter { can_throw_stack_overflow: false, error_display_level: ErrorDisplayLevel::Full, format_buffer_as_text: false, + node_uncaught_style: false, } } @@ -1940,6 +1946,7 @@ pub mod formatter { can_throw_stack_overflow: self.can_throw_stack_overflow, error_display_level: self.error_display_level, format_buffer_as_text: self.format_buffer_as_text, + node_uncaught_style: self.node_uncaught_style, } } @@ -3827,7 +3834,36 @@ pub mod formatter { if self.quote_strings && js_type != jsc::JSType::RegExpObject { if str.is_empty() { - writer.write_all(b"\"\""); + writer.write_all(if self.node_uncaught_style { + b"''" + } else { + b"\"\"" + }); + if writer.failed { + self.failed = true; + } + return Ok(()); + } + + // Node quotes inspect strings with single quotes. Only simple + // ASCII strings take this path; anything needing escapes falls + // through to the JSON (double-quote) writer. + if self.node_uncaught_style + && !str.is_utf16() + && str + .latin1() + .iter() + .all(|&b| (0x20..0x7f).contains(&b) && b != b'\'' && b != b'\\') + { + if C { + writer.write_all(pfmt!("", true).as_bytes()); + } + writer.write_all(b"'"); + writer.write_all(str.latin1()); + writer.write_all(b"'"); + if C { + writer.write_all(pfmt!("", true).as_bytes()); + } if writer.failed { self.failed = true; } diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index 97462b6e6b79..0b53b42b1c6b 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -1440,7 +1440,12 @@ impl VirtualMachine { (self.on_unhandled_rejection)(self, global_object, err); return false; } - self.run_error_handler(err, None); + // `err` can arrive as the JSC::Exception wrapper here (the + // handler's throw is re-reported through + // `report_active_exception_as_unhandled`); unwrap so the printer + // sees the Error instance — its own stack and properties — instead + // of the wrapper's rethrow-site capture. + self.run_error_handler(err.to_error().unwrap_or(err), None); // SAFETY: `global_object` is the live VM global; `process_exit` is // `bun_runtime::node::process::exit` (main-thread `noreturn`). unsafe { (hooks.process_exit)(global_object.as_ptr(), 7) }; @@ -4659,6 +4664,10 @@ impl VirtualMachine { allow_side_effects: bool, ) { let mut formatter = crate::console_object::Formatter::new(self.global()); + // `bun test` keeps Bun's classic failure rendering; everything else + // (uncaught exceptions, unhandled rejections, server error handlers) + // prints Node's shape. + formatter.node_uncaught_style = !isBunTest.load(core::sync::atomic::Ordering::Relaxed); let colors = bun_core::Output::enable_ansi_colors_stderr(); self.print_errorlike_object( exception.value(), @@ -5113,7 +5122,35 @@ impl VirtualMachine { if self.had_errors { let _ = writer.write_all(b"\n"); } - write_msg!(resolve_error.msg, writer, allow_ansi_color); + // Uncaught module-resolution errors print Node's shape + // (`Error: Cannot find module 'x'\nRequire stack:\n- ...`), + // not the transpiler-log rendering. + if let Some(display_name) = resolve_error + .node_display_name() + .filter(|_| formatter.node_uncaught_style) + { + let node_message = resolve_error.node_message(); + let message: &[u8] = node_message + .as_deref() + .unwrap_or(&resolve_error.msg.data.text); + let _ = if allow_ansi_color { + write!( + writer, + bun_core::pretty_fmt!("{}: {}", true), + bstr::BStr::new(display_name), + bstr::BStr::new(message) + ) + } else { + write!( + writer, + "{}: {}", + bstr::BStr::new(display_name), + bstr::BStr::new(message) + ) + }; + } else { + write_msg!(resolve_error.msg, writer, allow_ansi_color); + } resolve_error.logged.set(true); let _ = writer.write_all(b"\n"); } @@ -6030,6 +6067,7 @@ impl VirtualMachine { message, !exception.browser_url.is_empty(), code, + formatter.node_uncaught_style, writer, allow_ansi_color, formatter.error_display_level, @@ -6080,6 +6118,7 @@ impl VirtualMachine { message, !exception.browser_url.is_empty(), code, + formatter.node_uncaught_style, writer, allow_ansi_color, formatter.error_display_level, @@ -6095,6 +6134,7 @@ impl VirtualMachine { message, !exception.browser_url.is_empty(), code, + formatter.node_uncaught_style, writer, allow_ansi_color, formatter.error_display_level, @@ -6232,11 +6272,24 @@ impl VirtualMachine { let pad_left = longest_name.saturating_sub(b"code".len()); is_first_property = false; splat_space(writer, pad_left as u64)?; - pretty_write!( - writer, - " code: {}\n", - bun_core::fmt::quote(code_str) - )?; + if formatter.node_uncaught_style + && code_str + .iter() + .all(|&b| (0x20..0x7f).contains(&b) && b != b'\'' && b != b'\\') + { + // Node single-quotes inspect strings: `code: 'ENOENT'`. + pretty_write!( + writer, + " code: '{}'\n", + bstr::BStr::new(code_str) + )?; + } else { + pretty_write!( + writer, + " code: {}\n", + bun_core::fmt::quote(code_str) + )?; + } } if !is_first_property { @@ -6314,11 +6367,13 @@ impl VirtualMachine { Ok(()) } + #[allow(clippy::too_many_arguments)] fn print_error_name_and_message( name: bun_core::String, message: bun_core::String, is_browser_error: bool, optional_code: Option<&[u8]>, + node_uncaught_style: bool, writer: &mut bun_core::io::Writer, allow_ansi_color: bool, error_display_level: crate::console_object::ErrorDisplayLevel, @@ -6337,7 +6392,12 @@ impl VirtualMachine { writer.write_all(bun_core::pretty_fmt!("frontend ", true).as_bytes())?; } if !name.is_empty() && !message.is_empty() { - let (display_name, display_message) = if name.eql_comptime(b"Error") { + // Node prints the error's own name verbatim (`Error: boom`); Bun's + // classic rendering lowercases plain `Error` to `error:` and + // promotes a `CODE: `-prefixed message's code to the name slot. + let (display_name, display_message) = if name.eql_comptime(b"Error") + && !node_uncaught_style + { 'brk: { if let Some(code) = optional_code { if bun_core::is_all_ascii(code) { diff --git a/src/node-fallbacks/util.js b/src/node-fallbacks/util.js index 79b4ce6ce3bd..c33a56e9874c 100644 --- a/src/node-fallbacks/util.js +++ b/src/node-fallbacks/util.js @@ -903,7 +903,7 @@ export function callbackifyOnRejected(reason, cb) { // occurred", we error-wrap so the callback consumer can distinguish between // "the promise rejected with null" or "the promise fulfilled with undefined". if (!reason) { - var newReason = new Error("Promise was rejected with a falsy value"); + var newReason = new Error("Promise was rejected with falsy value"); newReason.reason = reason; reason = newReason; } diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index cfefa1eed23b..902694a5d1f4 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -873,6 +873,35 @@ unsafe fn ensure_debugger(vm: *mut VirtualMachine, block_until_connected: bool) if unsafe { &*vm }.debugger.is_none() { return; } + // Node's permission model gates the inspector (inspector_agent.cc + // Agent::Start): without --allow-inspector, --inspect/--inspect-wait are + // silently skipped, and --inspect-brk raises ERR_ACCESS_DENIED for + // PauseOnNextJavascriptStatement as an uncaught exception (exit 1). + if crate::permission::is_enabled() + && !crate::permission::is_granted(crate::permission::Scope::Inspector, None) + { + // SAFETY: `vm` is the live per-thread VM. + let vm_ref = unsafe { &mut *vm }; + let wants_first_line_break = vm_ref + .debugger + .as_deref() + .is_some_and(|d| d.set_breakpoint_on_first_line); + if wants_first_line_break { + // SAFETY: `vm.global` is set during `VirtualMachine::init`. + let global_ref = unsafe { &*vm_ref.global }; + let err = crate::permission::access_denied_error( + global_ref, + crate::permission::Scope::Inspector, + b"PauseOnNextJavascriptStatement", + ); + vm_ref.run_error_handler(err, None); + vm_ref.exit_handler.exit_code = 1; + vm_ref.on_exit(); + vm_ref.global_exit(); + } + vm_ref.debugger = None; + return; + } // SAFETY: `vm.global` is set during `VirtualMachine::init` and outlives // the VM; read the raw ptr before forming `&mut *vm` so the two derefs // don't alias. @@ -1194,6 +1223,8 @@ fn print_exception( vm_ref.print_exception(exception, exception_list, writer, true); } else { let mut formatter = bun_jsc::console_object::Formatter::new(global); + formatter.node_uncaught_style = + !bun_jsc::virtual_machine::isBunTest.load(core::sync::atomic::Ordering::Relaxed); // `Formatter::new` already // defaults `error_display_level` to `Full` (ConsoleObject.rs:1176). let colors = bun_core::Output::enable_ansi_colors_stderr(); diff --git a/test/cli/hot/hot.test.ts b/test/cli/hot/hot.test.ts index 832e61e79cc8..e1cdf063d3e3 100644 --- a/test/cli/hot/hot.test.ts +++ b/test/cli/hot/hot.test.ts @@ -31,7 +31,7 @@ async function driveErrorReloadCycle( for await (const chunk of runner.stderr) { str += new TextDecoder().decode(chunk); // Need at least one error line followed by a newline, then another line followed by a newline - if (!/error: .*[0-9]\n.*?\n/g.test(str)) continue; + if (!/Error: .*[0-9]\n.*?\n/g.test(str)) continue; const lines = str.split("\n"); // Preserve trailing partial line for the next chunk @@ -40,7 +40,7 @@ async function driveErrorReloadCycle( for (let i = 0; i < lines.length; i++) { const line = lines[i]; - if (!line.includes("error:")) { + if (!line.includes("Error:")) { // Don't silently swallow a watcher-thread death-rattle — surface it so the // post-loop "Expected 50, Received N" becomes an actionable failure. if (/Watcher crashed|panic:|oh no:/.test(line)) { @@ -65,7 +65,7 @@ async function driveErrorReloadCycle( // If we see the previous error repeated, the pending reload hasn't // taken effect yet. Re-save the file and put remaining unprocessed // lines back into the buffer so they aren't lost. - if (line.includes(`error: ${reloadCounter - 1}`)) { + if (line.includes(`Error: ${reloadCounter - 1}`)) { const remaining = lines.slice(i + 1).join("\n"); if (remaining) { str = `${remaining}\n${str}`; @@ -75,7 +75,7 @@ async function driveErrorReloadCycle( break; } - expect(line).toContain(`error: ${reloadCounter}`); + expect(line).toContain(`Error: ${reloadCounter}`); const nextLine = lines[i + 1]; if (verifyLine) { diff --git a/test/cli/run/run-autoinstall.test.ts b/test/cli/run/run-autoinstall.test.ts index bdf63a8690f8..61fa0b7b2148 100644 --- a/test/cli/run/run-autoinstall.test.ts +++ b/test/cli/run/run-autoinstall.test.ts @@ -38,10 +38,10 @@ describe("basic autoinstall", () => { }); if (should_install) { - expect(stderr?.toString("utf8")).not.toContain("error: Cannot find package 'is-even'"); + expect(stderr?.toString("utf8")).not.toContain("Cannot find package 'is-even'"); expect(stdout?.toString("utf8")).toBe("true\n"); } else { - expect(stderr?.toString("utf8")).toContain("error: Cannot find package 'is-even'"); + expect(stderr?.toString("utf8")).toContain("Cannot find package 'is-even'"); } }); } @@ -118,6 +118,6 @@ test("--install=fallback to install missing packages", async () => { stderr: "pipe", }); - expect(stderr?.toString("utf8")).not.toContain("error: Cannot find package 'is-odd'"); + expect(stderr?.toString("utf8")).not.toContain("Cannot find package 'is-odd'"); expect(stdout?.toString("utf8")).toBe("true false\n"); }); diff --git a/test/cli/run/tsconfig-override.test.ts b/test/cli/run/tsconfig-override.test.ts index f3f103075453..c9b0474a7253 100644 --- a/test/cli/run/tsconfig-override.test.ts +++ b/test/cli/run/tsconfig-override.test.ts @@ -44,7 +44,7 @@ describe("bun run --tsconfig-override", () => { const [failStderr, failExitCode] = await Promise.all([failProc.stderr.text(), failProc.exited]); - expect(failStderr).toContain("Cannot find module"); + expect(failStderr).toContain("Cannot find package"); expect(failExitCode).not.toBe(0); await using successProc = Bun.spawn({ diff --git a/test/js/bun/http/async-iterator-stream.test.ts b/test/js/bun/http/async-iterator-stream.test.ts index ea4988566180..054795c7cc7e 100644 --- a/test/js/bun/http/async-iterator-stream.test.ts +++ b/test/js/bun/http/async-iterator-stream.test.ts @@ -132,7 +132,7 @@ describe.concurrent("Streaming body via", () => { let [exitCode, stderr] = await Promise.all([subprocess.exited, subprocess.stderr.text()]); expect(exitCode).toBeInteger(); - expect(stderr).toContain("error: Oops"); + expect(stderr).toContain("Error: Oops"); expect(onMessage).toHaveBeenCalledTimes(1); }); diff --git a/test/js/bun/http/serve-direct-readable-stream.test.ts b/test/js/bun/http/serve-direct-readable-stream.test.ts index 13df3ec832b5..3e33aadeeb01 100644 --- a/test/js/bun/http/serve-direct-readable-stream.test.ts +++ b/test/js/bun/http/serve-direct-readable-stream.test.ts @@ -416,7 +416,7 @@ describe("sync pull() throw after status is written does not re-render error()", stderr: "pipe", }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stderr).toContain("error: boom"); + expect(stderr).toContain("Error: boom"); const { wire, errorHandlerCalls } = JSON.parse(stdout); // error() cannot replace a response whose status is committed; the // connection is force-closed so the client observes failure instead of @@ -436,7 +436,7 @@ describe("sync pull() throw after status is written does not re-render error()", stderr: "pipe", }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stderr).toContain("error: boom"); + expect(stderr).toContain("Error: boom"); const { wire, errorHandlerCalls } = JSON.parse(stdout); // Status 200 was already written; the stream is ended empty. The error() // response's status (500), headers, and body must not appear on the wire. diff --git a/test/js/bun/http/serve.test.ts b/test/js/bun/http/serve.test.ts index d6534b96eae8..8d077b75dee8 100644 --- a/test/js/bun/http/serve.test.ts +++ b/test/js/bun/http/serve.test.ts @@ -637,7 +637,7 @@ describe("streaming", () => { let [exitCode, stderr] = await Promise.all([subprocess.exited, subprocess.stderr.text()]); expect(exitCode).toBeInteger(); - expect(stderr).toContain("error: Oops"); + expect(stderr).toContain("Error: Oops"); expect(onMessage).toHaveBeenCalled(); }); @@ -662,7 +662,7 @@ describe("streaming", () => { let [exitCode, stderr] = await Promise.all([subprocess.exited, subprocess.stderr.text()]); expect(exitCode).toBeInteger(); - expect(stderr).toContain("error: Oops"); + expect(stderr).toContain("Error: Oops"); expect(onMessage).toHaveBeenCalled(); }); diff --git a/test/js/bun/test/stack.test.ts b/test/js/bun/test/stack.test.ts index 121cf2867faf..9289da8136d8 100644 --- a/test/js/bun/test/stack.test.ts +++ b/test/js/bun/test/stack.test.ts @@ -96,10 +96,10 @@ test("throwing inside an error suppresses the error and prints the stack", async expect(stderr.toString().trim().split("\n").slice(0, -1).join("\n").trim()).toMatchInlineSnapshot(` "error: My custom error message { - message: "My custom error message", + message: 'My custom error message', name: [Getter], line: 42, - sourceURL: "http://example.com/test.js", + sourceURL: 'http://example.com/test.js', } at http://example.com/test.js:42" `); @@ -113,11 +113,11 @@ test("throwing inside an error suppresses the error and continues printing prope const { stderr, exitCode } = result; - expect(stderr.toString().trim()).toStartWith(`ENOENT: no such file or directory, open 'this-file-path-is-bad' - path: "this-file-path-is-bad", - syscall: "open", + expect(stderr.toString().trim()).toStartWith(`Error: ENOENT: no such file or directory, open 'this-file-path-is-bad' + path: 'this-file-path-is-bad', + syscall: 'open', errno: ${process.binding("uv").UV_ENOENT}, - code: "ENOENT" + code: 'ENOENT' `); expect(exitCode).toBe(1); }); diff --git a/test/js/bun/util/reportError.test.ts b/test/js/bun/util/reportError.test.ts index 3075af55f04f..1e420dc0bd67 100644 --- a/test/js/bun/util/reportError.test.ts +++ b/test/js/bun/util/reportError.test.ts @@ -22,7 +22,7 @@ test("reportError", () => { ` "1 | reportError(new Error("reportError Test!")); ^ -error: reportError Test! +Error: reportError Test! at [file]:1:17 error: true true diff --git a/test/js/node/module/sourcemap.test.js b/test/js/node/module/sourcemap.test.js index 58cd458697c1..0534811396f7 100644 --- a/test/js/node/module/sourcemap.test.js +++ b/test/js/node/module/sourcemap.test.js @@ -241,7 +241,7 @@ test("error.stack of // @bun code with a truncated VLQ in sourceMappingURL warns // The truncated mapping is rejected with a warning rather than silently // decoded as 0 (which left no trace that the map was corrupt). expect(stderr).toContain("Could not decode sourcemap"); - expect(stderr).toContain("error: boom"); + expect(stderr).toContain("Error: boom"); expect(stderr).toContain("entry.js:2:"); expect(stdout).toBe(""); expect(exitCode).toBe(1); diff --git a/test/js/node/process/process.test.js b/test/js/node/process/process.test.js index b9803caed73c..73402b0213f2 100644 --- a/test/js/node/process/process.test.js +++ b/test/js/node/process/process.test.js @@ -564,7 +564,7 @@ describe.concurrent(() => { }); const [stderr, stdout, exitCode] = await Promise.all([proc.stderr.text(), proc.stdout.text(), proc.exited]); expect(exitCode).toBe(1); - expect(stderr).toInclude("error: boom"); + expect(stderr).toInclude("Error: boom"); expect(stdout).toBeEmpty(); }); @@ -583,7 +583,7 @@ describe.concurrent(() => { }); const [stderr, stdout, exitCode] = await Promise.all([proc.stderr.text(), proc.stdout.text(), proc.exited]); expect(stdout).toBe("monitor boom uncaughtException\nuncaughtException boom\n"); - expect(stderr).not.toInclude("error: boom"); + expect(stderr).not.toInclude("Error: boom"); expect(exitCode).toBe(0); }); @@ -601,7 +601,7 @@ describe.concurrent(() => { }); const [stderr, stdout, exitCode] = await Promise.all([proc.stderr.text(), proc.stdout.text(), proc.exited]); expect(stdout).toBe("captured boom\n"); - expect(stderr).not.toInclude("error: boom"); + expect(stderr).not.toInclude("Error: boom"); expect(exitCode).toBe(0); }); @@ -623,7 +623,7 @@ describe.concurrent(() => { }); const [stderr, stdout, exitCode] = await Promise.all([proc.stderr.text(), proc.stdout.text(), proc.exited]); expect(stdout).toBe("uncaughtException late\n"); - expect(stderr).not.toInclude("error: late"); + expect(stderr).not.toInclude("Error: late"); expect(exitCode).toBe(0); }); @@ -663,7 +663,7 @@ describe.concurrent(() => { }); const [stderr, stdout, exitCode] = await Promise.all([proc.stderr.text(), proc.stdout.text(), proc.exited]); expect(stdout).toBe("exit 1\n"); - expect(stderr).toInclude("error: boom"); + expect(stderr).toInclude("Error: boom"); expect(exitCode).toBe(1); }); @@ -682,7 +682,7 @@ describe.concurrent(() => { }); const [stderr, stdout, exitCode] = await Promise.all([proc.stderr.text(), proc.stdout.text(), proc.exited]); expect(stdout).toBe("caught boom\nbeforeExit 0\nexit 0\n"); - expect(stderr).not.toInclude("error: boom"); + expect(stderr).not.toInclude("Error: boom"); expect(exitCode).toBe(0); }); @@ -703,7 +703,7 @@ describe.concurrent(() => { }); const [stderr, stdout, exitCode] = await Promise.all([proc.stderr.text(), proc.stdout.text(), proc.exited]); expect(stdout).toBe("first 1\n"); - expect(stderr).toInclude("error: boom"); + expect(stderr).toInclude("Error: boom"); expect(exitCode).toBe(1); }); @@ -719,7 +719,7 @@ describe.concurrent(() => { stdio: ["inherit", "pipe", "pipe"], }); const [stderr, stdout, exitCode] = await Promise.all([proc.stderr.text(), proc.stdout.text(), proc.exited]); - expect(stderr).toInclude("error: a"); + expect(stderr).toInclude("Error: a"); expect(stdout).toBeEmpty(); // 7 is node's "the uncaughtException handler itself threw"; there is no handler here. expect(exitCode).toBe(1); @@ -735,7 +735,7 @@ describe.concurrent(() => { }); const [stderr, stdout, exitCode] = await Promise.all([proc.stderr.text(), proc.stdout.text(), proc.exited]); expect(exitCode).toBe(1); - expect(stderr).toInclude("error: boom"); + expect(stderr).toInclude("Error: boom"); expect(stdout).toBeEmpty(); }); @@ -753,7 +753,7 @@ describe.concurrent(() => { }); const [stderr, stdout, exitCode] = await Promise.all([proc.stderr.text(), proc.stdout.text(), proc.exited]); expect(stdout).toBe("monitor boom uncaughtException\nuncaughtException boom\n"); - expect(stderr).not.toInclude("error: boom"); + expect(stderr).not.toInclude("Error: boom"); expect(exitCode).toBe(0); }); }); diff --git a/test/js/node/test/parallel/test-unhandled-exception-rethrow-error.js b/test/js/node/test/parallel/test-unhandled-exception-rethrow-error.js new file mode 100644 index 000000000000..883351e2a4bf --- /dev/null +++ b/test/js/node/test/parallel/test-unhandled-exception-rethrow-error.js @@ -0,0 +1,28 @@ +'use strict'; +require('../common'); + +if (process.argv[2] === 'child') { + process.on('uncaughtException', (err) => { + err.rethrow = true; + throw err; + }); + + function throwException() { + throw new Error('boom'); + } + + throwException(); +} else { + const assert = require('assert'); + const { spawnSync } = require('child_process'); + const result = spawnSync(process.execPath, [__filename, 'child']); + + assert.strictEqual(result.status, 7); + assert.strictEqual(result.signal, null); + assert.strictEqual(result.stdout.toString().trim(), ''); + // Verify that the error was thrown and that the stack was preserved. + const stderr = result.stderr.toString(); + assert.match(stderr, /Error: boom/); + assert.match(stderr, /at throwException/); + assert.match(stderr, /rethrow: true/); +} diff --git a/test/js/node/test/parallel/test-util-callbackify.js b/test/js/node/test/parallel/test-util-callbackify.js index b37fbf2dfeda..1b62f81a393a 100644 --- a/test/js/node/test/parallel/test-util-callbackify.js +++ b/test/js/node/test/parallel/test-util-callbackify.js @@ -223,8 +223,8 @@ const values = [ assert.strictEqual(Object.getPrototypeOf(err).name, 'Error'); assert.strictEqual(stdout, ''); const errLines = stderr.trim().split(/[\r\n]+/); - const errLine = errLines.find((l) => /^error/.exec(l)); - assert.strictEqual(errLine, `error: ${fixture}`); + const errLine = errLines.find((l) => /^Error/.exec(l)); + assert.strictEqual(errLine, `Error: ${fixture}`); assert.strictEqual(errLines.length, 10); }) ); @@ -290,7 +290,7 @@ const values = [ const cbPromiseFn = callbackify(promiseFn); cbPromiseFn(null, (err) => { - assert.strictEqual(err.message, 'Promise was rejected with a falsy value'); + assert.strictEqual(err.message, 'Promise was rejected with falsy value'); assert.strictEqual(err.code, 'ERR_FALSY_VALUE_REJECTION'); assert.strictEqual(err.reason, null); // skipped, bun doesn't hide callbackifyOnRejected from the stack trace From fb1c113bc5407ed35d44dc7a59610704a6495a49 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari MacBook Date: Sat, 25 Jul 2026 02:39:55 +0000 Subject: [PATCH 04/11] module resolution errors: Node's name/toString/stack surface ResolveMessage reported name 'ResolveMessage' and stringified as 'ResolveMessage: ...'. Node throws module-not-found as a plain Error: String(err) is 'Error: Cannot find module 'x'' for CJS, 'Error [ERR_MODULE_NOT_FOUND]: ...' for ESM, and 'Error [ERR_UNKNOWN_BUILTIN_MODULE]: ...' for a missing node: builtin. - name becomes a getter: 'Error' for the require/import module-not-found shapes, 'ResolveMessage' otherwise (Bun.build logs etc. unchanged). - toString()/.stack/toJSON use the same display name. - The uncaught printer renders these as 'Error: Cannot find module 'x' [newline] Require stack: ...' instead of the transpiler-log form. - is_bare_esm_specifier no longer classifies URL-scheme specifiers ('file://...', 'pkg:bar') as bare packages; Node reports those as URL/scheme errors, never "Cannot find package 'file:'", and the module-form message keeps the full specifier. This lets fixtures/require-resolve.js go back to byte-verbatim upstream (its /^Error: Cannot find module/ anchors now match) and vendors test-internal-modules.js. --- src/jsc/ResolveMessage.rs | 76 ++++++++++++++++++- src/jsc/resolve_message.classes.ts | 4 +- test/js/bun/resolve/import-meta.test.js | 2 +- test/js/bun/resolve/resolve-error.test.ts | 2 +- test/js/bun/resolve/resolve-test.js | 6 +- test/js/bun/resolve/resolve.test.ts | 4 +- test/js/node/test/fixtures/require-resolve.js | 9 +-- .../test/parallel/test-internal-modules.js | 13 ++++ 8 files changed, 98 insertions(+), 18 deletions(-) create mode 100644 test/js/node/test/parallel/test-internal-modules.js diff --git a/src/jsc/ResolveMessage.rs b/src/jsc/ResolveMessage.rs index e087cad61d22..60b87bd76cc5 100644 --- a/src/jsc/ResolveMessage.rs +++ b/src/jsc/ResolveMessage.rs @@ -74,6 +74,19 @@ fn is_bare_esm_specifier(s: &[u8]) -> bool { [d, b':', b, ..] if d.is_ascii_alphabetic() && is_sep(*b) => return false, _ => {} } + // A leading URL scheme (`[A-Za-z][A-Za-z0-9+.-]*:`) is URL-like, not a + // package: Node reports those as scheme/URL errors, never "Cannot find + // package 'file:'". `':://x'` stays a package — ':' cannot start a scheme + // (Node says "Cannot find package '::'"). + if s[0].is_ascii_alphabetic() { + for (i, &b) in s.iter().enumerate() { + match b { + b':' if i > 0 => return false, + b if b.is_ascii_alphanumeric() || b == b'+' || b == b'.' || b == b'-' => {} + _ => break, + } + } + } true } @@ -267,7 +280,14 @@ impl ResolveMessage { // cases. let node_message = self.node_message(); let message: &[u8] = node_message.as_deref().unwrap_or(&self.msg.data.text); - if write!(&mut text, "ResolveMessage: {}", bstr::BStr::new(message)).is_err() { + if write!( + &mut text, + "{}: {}", + bstr::BStr::new(self.node_display_name().unwrap_or(b"ResolveMessage")), + bstr::BStr::new(message) + ) + .is_err() + { return global.throw_out_of_memory_value(); } let mut str = ZigString::init(&text); @@ -324,7 +344,7 @@ impl ResolveMessage { object.put( global, b"name", - bun_core::String::static_str(b"ResolveMessage").to_js(global)?, + bun_core::String::static_str(this.js_name()).to_js(global)?, ); object.put(global, b"position", Self::get_position(this, global)?); object.put(global, b"message", Self::get_message(this, global)?); @@ -396,9 +416,56 @@ impl ResolveMessage { Some((resolve.import_kind, specifier, referrer)) } + /// Whether this is Node's ERR_UNKNOWN_BUILTIN_MODULE (`require('node:x')` + /// / `import('node:x')` for a builtin that doesn't exist). + fn is_unknown_builtin(&self) -> bool { + let bun_ast::Metadata::Resolve(resolve) = &self.msg.metadata else { + return false; + }; + matches!( + resolve.import_kind, + ImportKind::Require | ImportKind::Stmt | ImportKind::Dynamic + ) && self + .msg + .data + .text + .starts_with(b"No such built-in module:") + } + + /// `err.name` — Node throws module-resolution failures as plain `Error`. + pub(crate) fn js_name(&self) -> &'static [u8] { + if self.node_display_name().is_some() { + b"Error" + } else { + b"ResolveMessage" + } + } + + /// The `` used by `toString()` / `.stack`, or `None` when the error + /// keeps Bun's ResolveMessage rendering. Node renders the code in brackets + /// for its `E()`-constructed errors (`Error [ERR_MODULE_NOT_FOUND]: ...`), + /// while CJS MODULE_NOT_FOUND is a plain `Error: ...`. + pub(crate) fn node_display_name(&self) -> Option<&'static [u8]> { + if self.is_unknown_builtin() { + return Some(b"Error [ERR_UNKNOWN_BUILTIN_MODULE]"); + } + match self.node_error_shape() { + Some((ImportKind::Require | ImportKind::RequireResolve, ..)) => Some(b"Error"), + Some((ImportKind::Stmt | ImportKind::Dynamic, ..)) => { + Some(b"Error [ERR_MODULE_NOT_FOUND]") + } + _ => None, + } + } + + #[crate::host_fn(getter)] + pub fn get_name(this: &Self, global: &JSGlobalObject) -> JsResult { + Ok(ZigString::init(this.js_name()).to_js(global)) + } + /// Node's message for a module-not-found error, or `None` when the /// original text should be kept. - fn node_message(&self) -> Option> { + pub(crate) fn node_message(&self) -> Option> { use bstr::BStr; let (kind, specifier, referrer) = self.node_error_shape()?; let mut out = Vec::new(); @@ -466,7 +533,8 @@ impl ResolveMessage { #[crate::host_fn(getter)] pub fn get_stack(this: &Self, global: &JSGlobalObject) -> JsResult { let mut out = Vec::new(); - out.extend_from_slice(b"ResolveMessage: "); + out.extend_from_slice(this.node_display_name().unwrap_or(b"ResolveMessage")); + out.extend_from_slice(b": "); match this.node_message() { Some(text) => out.extend_from_slice(&text), None => out.extend_from_slice(&this.msg.data.text), diff --git a/src/jsc/resolve_message.classes.ts b/src/jsc/resolve_message.classes.ts index 1f9a21dce02d..a9f07ebd56e5 100644 --- a/src/jsc/resolve_message.classes.ts +++ b/src/jsc/resolve_message.classes.ts @@ -30,7 +30,9 @@ export default [ writable: true, }, name: { - value: "ResolveMessage", + getter: "getName", + cache: true, + writable: true, }, level: { getter: "getLevel", diff --git a/test/js/bun/resolve/import-meta.test.js b/test/js/bun/resolve/import-meta.test.js index b2a3cc54b06b..3245de46f91b 100644 --- a/test/js/bun/resolve/import-meta.test.js +++ b/test/js/bun/resolve/import-meta.test.js @@ -87,7 +87,7 @@ it("require with a query string works on dynamically created content", () => { try { require("./bar.js?query=123.js"); } catch (e) { - expect(e.name).toBe("ResolveMessage"); + expect(e.name).toBe("Error"); } mkdirSync(tmpbase + "bun-test-import-meta-dynamic-dir", { recursive: true }); diff --git a/test/js/bun/resolve/resolve-error.test.ts b/test/js/bun/resolve/resolve-error.test.ts index c608c2800a83..79e7fdba1155 100644 --- a/test/js/bun/resolve/resolve-error.test.ts +++ b/test/js/bun/resolve/resolve-error.test.ts @@ -57,7 +57,7 @@ describe("ResolveMessage", () => { } catch (e) { err = e; } - expect(err.name).toBe("ResolveMessage"); + expect(err.name).toBe("Error"); expect(err.specifier).toBe(spec); expect(err.message).toContain(spec); expect(String(err)).toContain(spec); diff --git a/test/js/bun/resolve/resolve-test.js b/test/js/bun/resolve/resolve-test.js index c99bc53e1582..3d52593d78ab 100644 --- a/test/js/bun/resolve/resolve-test.js +++ b/test/js/bun/resolve/resolve-test.js @@ -25,7 +25,7 @@ it.todo("#imports", async () => { } catch (exception) { expect(exception instanceof ResolveMessage).toBe(true); expect(exception.referrer).toBe(import.meta.path); - expect(exception.name).toBe("ResolveMessage"); + expect(exception.name).toBe("Error"); } // Chcek that package-json-imports/#foo doesn't work @@ -35,7 +35,7 @@ it.todo("#imports", async () => { } catch (exception) { expect(exception instanceof ResolveMessage).toBe(true); expect(exception.referrer).toBe(import.meta.path); - expect(exception.name).toBe("ResolveMessage"); + expect(exception.name).toBe("Error"); } }); @@ -100,7 +100,7 @@ it("import.meta.resolveSync", async () => { } catch (exception) { expect(exception instanceof ResolveMessage).toBe(true); expect(exception.referrer).toBe(import.meta.path); - expect(exception.name).toBe("ResolveMessage"); + expect(exception.name).toBe("Error"); } }); diff --git a/test/js/bun/resolve/resolve.test.ts b/test/js/bun/resolve/resolve.test.ts index aa730b0840d1..63a06618c017 100644 --- a/test/js/bun/resolve/resolve.test.ts +++ b/test/js/bun/resolve/resolve.test.ts @@ -615,7 +615,7 @@ describe("package.json exports target percent-encoding", () => { expect(Bun.resolveSync("test-pkg/ok", root)).toBe(join(root, "node_modules/test-pkg/lib/ok.js")); // lib/index.js exists; rejection must come from the directory-target check, not a missing file. - expect(resolveError("test-pkg/dir", root)).toEqual({ name: "ResolveMessage", code: "ERR_MODULE_NOT_FOUND" }); + expect(resolveError("test-pkg/dir", root)).toEqual({ name: "Error", code: "ERR_MODULE_NOT_FOUND" }); }); it.concurrent("decodes a percent-encoded target and rejects encoded path separators", () => { @@ -641,7 +641,7 @@ describe("package.json exports target percent-encoding", () => { expect(Bun.resolveSync("test-pkg/space", root)).toBe(join(root, "node_modules/test-pkg/lib/with space.js")); for (const sub of ["sep-2f", "sep-2F", "sep-5c", "sep-5C", "bad"]) { - expect(resolveError(`test-pkg/${sub}`, root)).toEqual({ name: "ResolveMessage", code: "ERR_MODULE_NOT_FOUND" }); + expect(resolveError(`test-pkg/${sub}`, root)).toEqual({ name: "Error", code: "ERR_MODULE_NOT_FOUND" }); } }); }); diff --git a/test/js/node/test/fixtures/require-resolve.js b/test/js/node/test/fixtures/require-resolve.js index 4fc241918860..ae0609f97c2f 100644 --- a/test/js/node/test/fixtures/require-resolve.js +++ b/test/js/node/test/fixtures/require-resolve.js @@ -16,10 +16,7 @@ if (require.resolve.paths) { // Verify that existing paths are removed. assert.throws(() => { require.resolve('bar', { paths: [] }) - // Upstream anchors /^Error: .../ — Bun's error class stringifies as - // "ResolveMessage:", so only the anchor is dropped here (message and - // MODULE_NOT_FOUND code match node v26.3.0). - }, /Cannot find module 'bar'/); + }, /^Error: Cannot find module 'bar'/); } // Verify that resolution path can be overwritten. @@ -27,14 +24,14 @@ if (require.resolve.paths) { // three.js cannot be loaded from this file by default. assert.throws(() => { require.resolve('three') - }, /Cannot find module 'three'/); + }, /^Error: Cannot find module 'three'/); // If the nested-index directory is provided as a resolve path, 'three' // cannot be found because nested-index is used as a starting point and not // a searched directory. assert.throws(() => { require.resolve('three', { paths: [nestedIndex] }) - }, /Cannot find module 'three'/); + }, /^Error: Cannot find module 'three'/); // Resolution from nested index directory also checks node_modules. assert.strictEqual( diff --git a/test/js/node/test/parallel/test-internal-modules.js b/test/js/node/test/parallel/test-internal-modules.js new file mode 100644 index 000000000000..e6bd0c9e1413 --- /dev/null +++ b/test/js/node/test/parallel/test-internal-modules.js @@ -0,0 +1,13 @@ +'use strict'; +require('../common'); +const fixtures = require('../common/fixtures'); +const assert = require('assert'); + +assert.throws(function() { + require('internal/freelist'); +}, /^Error: Cannot find module 'internal\/freelist'/); + +assert.strictEqual( + require(fixtures.path('internal-modules')), + 42 +); From 4319cb37c4dd952c6ee43448f08c14573b7d6723 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari MacBook Date: Sat, 25 Jul 2026 02:40:00 +0000 Subject: [PATCH 05/11] vendor permission tests unlocked by the Node-style uncaught printer test-permission-inspector.js, test-permission-inspector-brk.js, and test-permission-sqlite-load-extension.js all assert /Error: .../ (and code: 'ERR_...') on child stderr. The inspector gating they exercise (silently skip --inspect without --allow-inspector; uncaught ERR_ACCESS_DENIED for --inspect-brk, per inspector_agent.cc) ships in the ensure_debugger change of the printer commit. --- .../parallel/test-permission-inspector-brk.js | 45 +++++++++++++++++++ .../parallel/test-permission-inspector.js | 42 +++++++++++++++++ .../test-permission-sqlite-load-extension.js | 17 +++++++ 3 files changed, 104 insertions(+) create mode 100644 test/js/node/test/parallel/test-permission-inspector-brk.js create mode 100644 test/js/node/test/parallel/test-permission-inspector.js create mode 100644 test/js/node/test/parallel/test-permission-sqlite-load-extension.js diff --git a/test/js/node/test/parallel/test-permission-inspector-brk.js b/test/js/node/test/parallel/test-permission-inspector-brk.js new file mode 100644 index 000000000000..3cc7caabd42b --- /dev/null +++ b/test/js/node/test/parallel/test-permission-inspector-brk.js @@ -0,0 +1,45 @@ +'use strict'; + +const common = require('../common'); +const assert = require('assert'); +const { spawnSync } = require('child_process'); +const fixtures = require('../common/fixtures'); +const file = fixtures.path('permission', 'inspector-brk.js'); +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + common.skip('This test only works on a main thread'); +} + +common.skipIfInspectorDisabled(); + +// See https://github.com/nodejs/node/issues/53385 +{ + const { status, stderr } = spawnSync( + process.execPath, + [ + '--permission', + '--allow-fs-read=*', + '--inspect-brk', + file, + ], + ); + + assert.strictEqual(status, 1); + assert.match(stderr.toString(), /Error: Access to this API has been restricted/); +} + +{ + const { status, stderr } = spawnSync( + process.execPath, + [ + '--permission', + '--inspect-brk', + '--eval', + 'console.log("Hi!")', + ], + ); + + assert.strictEqual(status, 1); + assert.match(stderr.toString(), /Error: Access to this API has been restricted/); +} diff --git a/test/js/node/test/parallel/test-permission-inspector.js b/test/js/node/test/parallel/test-permission-inspector.js new file mode 100644 index 000000000000..51994c218eb5 --- /dev/null +++ b/test/js/node/test/parallel/test-permission-inspector.js @@ -0,0 +1,42 @@ +// Flags: --permission --allow-fs-read=* --allow-child-process +'use strict'; + +const common = require('../common'); +const { isMainThread } = require('worker_threads'); + +if (!isMainThread) { + common.skip('This test only works on a main thread'); +} + +common.skipIfInspectorDisabled(); + +const { Session } = require('inspector'); +const assert = require('assert'); +const { spawnSync } = require('child_process'); + +if (!common.hasCrypto) + common.skip('no crypto'); + +{ + assert.throws(() => { + const session = new Session(); + session.connect(); + }, common.expectsError({ + message: 'Access to this API has been restricted. Use --allow-inspector to manage permissions.', + code: 'ERR_ACCESS_DENIED', + permission: 'Inspector', + })); +} + +{ + const { status, stderr } = spawnSync( + process.execPath, + [ + '--permission', + '-e', + '(new (require("inspector")).Session()).connect()', + ], + ); + assert.strictEqual(status, 1); + assert.match(stderr.toString(), /Error: Access to this API has been restricted/); +} diff --git a/test/js/node/test/parallel/test-permission-sqlite-load-extension.js b/test/js/node/test/parallel/test-permission-sqlite-load-extension.js new file mode 100644 index 000000000000..def6f3d12699 --- /dev/null +++ b/test/js/node/test/parallel/test-permission-sqlite-load-extension.js @@ -0,0 +1,17 @@ +'use strict'; +const common = require('../common'); +common.skipIfSQLiteMissing(); +const assert = require('node:assert'); + +const code = `const sqlite = require('node:sqlite'); +const db = new sqlite.DatabaseSync(':memory:', { allowExtension: true }); +db.loadExtension('nonexistent');`.replace(/\n/g, ' '); + +common.spawnPromisified( + process.execPath, + ['--permission', '--eval', code], +).then(common.mustCall(({ code, stderr }) => { + assert.match(stderr, /Error: Cannot load SQLite extensions when the permission model is enabled/); + assert.match(stderr, /code: 'ERR_LOAD_SQLITE_EXTENSION'/); + assert.strictEqual(code, 1); +})); From bc763034490312cd8352ebc417fd6c558b737488 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Mon, 3 Aug 2026 22:32:18 +0000 Subject: [PATCH 06/11] trim comments to <=3 lines, cite spec/node source --- src/jsc/ConsoleObject.rs | 7 +++---- src/jsc/ResolveMessage.rs | 14 ++++++-------- src/jsc/VirtualMachine.rs | 8 +++----- src/runtime/jsc_hooks.rs | 7 +++---- 4 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/jsc/ConsoleObject.rs b/src/jsc/ConsoleObject.rs index 86f0dcfd92e0..e5d6e2e150ae 100644 --- a/src/jsc/ConsoleObject.rs +++ b/src/jsc/ConsoleObject.rs @@ -1829,10 +1829,9 @@ pub mod formatter { /// printed as a string. Set true in the error printer so that /// `ShellError` prints a more readable message. pub(crate) format_buffer_as_text: bool, - /// Render like Node's uncaught-exception report: the error's own - /// `name` is printed verbatim (`Error: boom`, never Bun's lowercase - /// `error:` alias) and string properties are single-quoted - /// (`code: 'ENOENT'`). Set only on the uncaught/unhandled print path. + /// Node-style uncaught report: verbatim `err.name` (no lowercase `error:` + /// alias), single-quoted strings (`code: 'ENOENT'`). Uncaught path only. + /// https://github.com/nodejs/node/blob/main/lib/internal/util/inspect.js pub node_uncaught_style: bool, } diff --git a/src/jsc/ResolveMessage.rs b/src/jsc/ResolveMessage.rs index ef8247f96992..b7b58d7e8ea3 100644 --- a/src/jsc/ResolveMessage.rs +++ b/src/jsc/ResolveMessage.rs @@ -74,10 +74,9 @@ fn is_bare_esm_specifier(s: &[u8]) -> bool { [d, b':', b, ..] if d.is_ascii_alphabetic() && is_sep(*b) => return false, _ => {} } - // A leading URL scheme (`[A-Za-z][A-Za-z0-9+.-]*:`) is URL-like, not a - // package: Node reports those as scheme/URL errors, never "Cannot find - // package 'file:'". `':://x'` stays a package — ':' cannot start a scheme - // (Node says "Cannot find package '::'"). + // Leading URL scheme (`[A-Za-z][A-Za-z0-9+.-]*:`, RFC 3986 §3.1) is not a + // bare package — Node reports scheme errors, not "Cannot find package 'file:'". + // https://github.com/nodejs/node/blob/main/lib/internal/modules/esm/resolve.js if s[0].is_ascii_alphabetic() { for (i, &b) in s.iter().enumerate() { match b { @@ -441,10 +440,9 @@ impl ResolveMessage { } } - /// The `` used by `toString()` / `.stack`, or `None` when the error - /// keeps Bun's ResolveMessage rendering. Node renders the code in brackets - /// for its `E()`-constructed errors (`Error [ERR_MODULE_NOT_FOUND]: ...`), - /// while CJS MODULE_NOT_FOUND is a plain `Error: ...`. + /// `toString()`/`.stack` name, or `None` to keep Bun's ResolveMessage render. + /// Node's `E()` errors bracket the code (`Error [ERR_MODULE_NOT_FOUND]`); CJS + /// is plain `Error`. https://github.com/nodejs/node/blob/main/lib/internal/errors.js pub(crate) fn node_display_name(&self) -> Option<&'static [u8]> { if self.is_unknown_builtin() { return Some(b"Error [ERR_UNKNOWN_BUILTIN_MODULE]"); diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index 5133b6f474f8..4d0690d92af6 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -1440,11 +1440,9 @@ impl VirtualMachine { (self.on_unhandled_rejection)(self, global_object, err); return false; } - // `err` can arrive as the JSC::Exception wrapper here (the - // handler's throw is re-reported through - // `report_active_exception_as_unhandled`); unwrap so the printer - // sees the Error instance — its own stack and properties — instead - // of the wrapper's rethrow-site capture. + // `err` may be the JSC::Exception wrapper (handler throw re-reported + // via `report_active_exception_as_unhandled`); unwrap so the printer + // sees the Error's own stack/properties, not the rethrow-site capture. self.run_error_handler(err.to_error().unwrap_or(err), None); // SAFETY: `global_object` is the live VM global; `process_exit` is // `bun_runtime::node::process::exit` (main-thread `noreturn`). diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index 0761c502e93d..22b643ce28e8 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -870,10 +870,9 @@ unsafe fn ensure_debugger(vm: *mut VirtualMachine, block_until_connected: bool) if unsafe { &*vm }.debugger.is_none() { return; } - // Node's permission model gates the inspector (inspector_agent.cc - // Agent::Start): without --allow-inspector, --inspect/--inspect-wait are - // silently skipped, and --inspect-brk raises ERR_ACCESS_DENIED for - // PauseOnNextJavascriptStatement as an uncaught exception (exit 1). + // Without --allow-inspector: --inspect/--inspect-wait are skipped, --inspect-brk + // raises ERR_ACCESS_DENIED (PauseOnNextJavascriptStatement) and exits 1. + // https://github.com/nodejs/node/blob/main/src/inspector_agent.cc (Agent::Start) if crate::permission::is_enabled() && !crate::permission::is_granted(crate::permission::Scope::Inspector, None) { From 8fc22b00cd971157c4b12a5aab445130c8d8c9e5 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 4 Aug 2026 00:35:08 +0000 Subject: [PATCH 07/11] fix build: restore BunHeapProfiler.h + repair merge fallout in bun_jsc/bun_runtime - src/jsc/bindings/BunHeapProfiler.h: restored (deleted by #36500 on main, still needed for $newCppFunction in src/js/node/v8.ts -> GeneratedJS2Native.h) - src/jsc/web_worker.rs: parent_ref binding was removed by the &self-only refactor merge; use the surrounding unsafe { (*parent).field } pattern - src/runtime/node/types.rs: define BUFFER_EXPECTED_TYPES imported by node_fs.rs - src/clap/lib.rs: Diagnostic fields pub (read by bun_runtime::cli::Arguments) - src/runtime/node/path.rs: resolve_{posix,windows}_t pub(crate) for permission.rs - src/runtime/permission.rs: std::sync::RwLock -> bun_threading::RwLock, std::env::var -> bun_core::env_var::NODE_OPTIONS, manual_contains lint - clippy: unreachable_pub (run_command.rs), undocumented_unsafe_blocks (Timer.rs), unnecessary_lazy_evaluations (BunHeapProfiler.rs) --- src/clap/lib.rs | 6 +-- src/jsc/BunHeapProfiler.rs | 2 +- src/jsc/ResolveMessage.rs | 6 +-- src/jsc/VirtualMachine.rs | 71 +++++++++++++++--------------- src/jsc/bindings/BunHeapProfiler.h | 14 ++++++ src/jsc/web_worker.rs | 3 +- src/runtime/cli/run_command.rs | 2 +- src/runtime/node/node_fs.rs | 6 ++- src/runtime/node/path.rs | 4 +- src/runtime/node/types.rs | 3 ++ src/runtime/permission.rs | 58 +++++++++--------------- src/runtime/timer/Timer.rs | 2 +- 12 files changed, 89 insertions(+), 88 deletions(-) create mode 100644 src/jsc/bindings/BunHeapProfiler.h diff --git a/src/clap/lib.rs b/src/clap/lib.rs index 64f31eceb971..3fa43111d2dd 100644 --- a/src/clap/lib.rs +++ b/src/clap/lib.rs @@ -298,9 +298,9 @@ fn expect_param(expect: Param, actual: Param) { // is flattened to `short`/`long` because `Names.long` is `&'static`. #[derive(Default)] pub struct Diagnostic { - pub(crate) arg: Vec, - pub(crate) short: Option, - pub(crate) long: Option>, + pub arg: Vec, + pub short: Option, + pub long: Option>, } impl Diagnostic { diff --git a/src/jsc/BunHeapProfiler.rs b/src/jsc/BunHeapProfiler.rs index 847461038ee9..6a3102920876 100644 --- a/src/jsc/BunHeapProfiler.rs +++ b/src/jsc/BunHeapProfiler.rs @@ -72,7 +72,7 @@ pub unsafe fn parse_worker_exec_argv( } } - enabled.then(|| HeapProfilerConfig { + enabled.then_some(HeapProfilerConfig { name, dir, text_format: false, diff --git a/src/jsc/ResolveMessage.rs b/src/jsc/ResolveMessage.rs index 08296b14d24e..7a2d158a4e3b 100644 --- a/src/jsc/ResolveMessage.rs +++ b/src/jsc/ResolveMessage.rs @@ -419,11 +419,7 @@ impl ResolveMessage { matches!( resolve.import_kind, ImportKind::Require | ImportKind::Stmt | ImportKind::Dynamic - ) && self - .msg - .data - .text - .starts_with(b"No such built-in module:") + ) && self.msg.data.text.starts_with(b"No such built-in module:") } /// `err.name` — Node throws module-resolution failures as plain `Error`. diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index f86981460937..43f8cb502869 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -6348,45 +6348,44 @@ impl VirtualMachine { // Node prints the error's own name verbatim (`Error: boom`); Bun's // classic rendering lowercases plain `Error` to `error:` and // promotes a `CODE: `-prefixed message's code to the name slot. - let (display_name, display_message) = if name.eql_comptime(b"Error") - && !node_uncaught_style - { - 'brk: { - if let Some(code) = optional_code { - if bun_core::is_all_ascii(code) { - let has_prefix = if message.is_utf16() { - let msg_chars = message.utf16(); - msg_chars.len() > code.len() + 2 + 1 - && code - .iter() - .zip(msg_chars.iter()) - .all(|(&a, &b)| u16::from(a) == b) - && msg_chars[code.len()] == u16::from(b':') - && msg_chars[code.len() + 1] == u16::from(b' ') - } else { - let msg_chars = message.latin1(); - msg_chars.len() > code.len() + 2 + 1 - && bun_core::strings::eql_long( - &msg_chars[..code.len()], - code, - false, - ) - && msg_chars[code.len()] == b':' - && msg_chars[code.len() + 1] == b' ' - }; - if has_prefix { - break 'brk ( - bun_core::String::init(code), - message.substring(code.len() + 2), - ); + let (display_name, display_message) = + if name.eql_comptime(b"Error") && !node_uncaught_style { + 'brk: { + if let Some(code) = optional_code { + if bun_core::is_all_ascii(code) { + let has_prefix = if message.is_utf16() { + let msg_chars = message.utf16(); + msg_chars.len() > code.len() + 2 + 1 + && code + .iter() + .zip(msg_chars.iter()) + .all(|(&a, &b)| u16::from(a) == b) + && msg_chars[code.len()] == u16::from(b':') + && msg_chars[code.len() + 1] == u16::from(b' ') + } else { + let msg_chars = message.latin1(); + msg_chars.len() > code.len() + 2 + 1 + && bun_core::strings::eql_long( + &msg_chars[..code.len()], + code, + false, + ) + && msg_chars[code.len()] == b':' + && msg_chars[code.len() + 1] == b' ' + }; + if has_prefix { + break 'brk ( + bun_core::String::init(code), + message.substring(code.len() + 2), + ); + } } } + (bun_core::String::empty(), message) } - (bun_core::String::empty(), message) - } - } else { - (name, message) - }; + } else { + (name, message) + }; pretty_write!( "{}{}\n", error_display_level.formatter(display_name, allow_ansi_color, Colon::IncludeColon), diff --git a/src/jsc/bindings/BunHeapProfiler.h b/src/jsc/bindings/BunHeapProfiler.h new file mode 100644 index 000000000000..1ee2a17e36c6 --- /dev/null +++ b/src/jsc/bindings/BunHeapProfiler.h @@ -0,0 +1,14 @@ +#pragma once + +#include "root.h" +#include + +namespace Bun { + +// v8.GCProfiler recorder (JSC::HeapObserver-backed); see BunHeapProfiler.cpp. +JSC_DECLARE_HOST_FUNCTION(jsFunction_startGCProfile); +JSC_DECLARE_HOST_FUNCTION(jsFunction_stopGCProfile); +// Backs node:v8's v8.startHeapProfile()/handle.stop(). +JSC_DECLARE_HOST_FUNCTION(jsFunction_takeSamplingHeapProfile); + +} // namespace Bun diff --git a/src/jsc/web_worker.rs b/src/jsc/web_worker.rs index 6805d14868e6..eb603c56eddc 100644 --- a/src/jsc/web_worker.rs +++ b/src/jsc/web_worker.rs @@ -569,7 +569,8 @@ impl WebWorker { // Inherited execArgv means inherited profiling flags; snapshot // the parent's config here on the parent thread (the only // thread that mutates it, in `on_exit`/self-kill). - parent_ref.heap_profiler_config.clone() + // SAFETY: `parent` is live (see above); borrow ends at `;`. + unsafe { (*parent).heap_profiler_config.clone() } } else { // SAFETY: caller passed valid (ptr,len) (or `(null,0)`); // strings live as long as the C++ `WorkerOptions`. diff --git a/src/runtime/cli/run_command.rs b/src/runtime/cli/run_command.rs index ec074fc68a83..c190684672f5 100644 --- a/src/runtime/cli/run_command.rs +++ b/src/runtime/cli/run_command.rs @@ -3029,7 +3029,7 @@ impl RunCommand { /// `--check` / `-c`: read the entry point (or stdin), boot with a no-op eval entry so /// `--require`/`--preload` still run like `node --check`; `Run::start` does the check. - pub fn exec_check(ctx: &mut ContextData) -> crate::Result<()> { + pub(crate) fn exec_check(ctx: &mut ContextData) -> crate::Result<()> { // `ctx.args.entry_points` is the positional list with the leading // subcommand keyword ("run") already stripped. let target: Option> = ctx.args.entry_points.first().cloned(); diff --git a/src/runtime/node/node_fs.rs b/src/runtime/node/node_fs.rs index 625710483875..94a05b9121ca 100644 --- a/src/runtime/node/node_fs.rs +++ b/src/runtime/node/node_fs.rs @@ -4150,7 +4150,11 @@ pub mod args { 0.0 }; let buffer = Buffer::from_js(ctx, buffer_value).ok_or_else(|| { - ctx.throw_invalid_argument_type_value2(b"buffer", BUFFER_EXPECTED_TYPES, buffer_value) + ctx.throw_invalid_argument_type_value2( + b"buffer", + BUFFER_EXPECTED_TYPES, + buffer_value, + ) })?; // if (length === 0) { diff --git a/src/runtime/node/path.rs b/src/runtime/node/path.rs index 1ceaf9ce246b..b85e9fb8eb6e 100644 --- a/src/runtime/node/path.rs +++ b/src/runtime/node/path.rs @@ -2968,7 +2968,7 @@ fn relative( /// Based on Node v21.6.1 path.posix.resolve: /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L1095 -fn resolve_posix_t<'a, T: PathCharCwd>( +pub(crate) fn resolve_posix_t<'a, T: PathCharCwd>( paths: &[&[T]], buf: &'a mut [T], buf2: &mut [T], @@ -3069,7 +3069,7 @@ fn resolve_posix_t<'a, T: PathCharCwd>( /// Based on Node v21.6.1 path.win32.resolve: /// https://github.com/nodejs/node/blob/6ae20aa63de78294b18d5015481485b7cd8fbb60/lib/path.js#L162 -fn resolve_windows_t<'a, T: PathCharCwd>( +pub(crate) fn resolve_windows_t<'a, T: PathCharCwd>( paths: &[&[T]], buf: &'a mut [T], buf2: &mut [T], diff --git a/src/runtime/node/types.rs b/src/runtime/node/types.rs index 884027b7f3bb..e85badfc3153 100644 --- a/src/runtime/node/types.rs +++ b/src/runtime/node/types.rs @@ -940,6 +940,9 @@ pub(crate) trait PathOrFdExt { /// (`['string', 'Buffer', 'URL']`), pre-rendered the way `errors.js` formats it. pub(crate) const PATH_EXPECTED_TYPES: &str = "of type string or an instance of Buffer or URL"; +/// Node's `['Buffer', 'TypedArray', 'DataView']` list for fs buffer +/// arguments, pre-rendered the way `ERR_INVALID_ARG_TYPE` formats instances. +pub(crate) const BUFFER_EXPECTED_TYPES: &str = "an instance of Buffer, TypedArray, or DataView"; impl PathLikeExt for PathLike { // Const-generics can't change return mutability, so this always returns diff --git a/src/runtime/permission.rs b/src/runtime/permission.rs index b6fb49d4113f..4a342783e41a 100644 --- a/src/runtime/permission.rs +++ b/src/runtime/permission.rs @@ -3,12 +3,12 @@ //! Bun stores state per process (Node: per Environment), so worker `drop()` is global. use core::sync::atomic::{AtomicBool, Ordering}; -use std::sync::RwLock; use bun_core::ZigString; use bun_jsc::{ CallFrame, ErrorCode, JSFunction, JSGlobalObject, JSValue, JsError, JsResult, ZigStringJsc as _, }; +use bun_threading::RwLock; unsafe extern "C" { safe fn Bun__Permission__requireInternalPermissionModule(global: &JSGlobalObject) -> JSValue; @@ -112,7 +112,7 @@ impl FsGrants { /// `FSPermission::GrantAccess`. fn grant(&mut self, resolved: Vec) { let path = wildcard_if_dir(resolved); - if self.granted.iter().any(|g| *g == path) { + if self.granted.contains(&path) { return; } self.granted.push(path); @@ -266,12 +266,7 @@ pub fn init_from_cli(grants: &CliGrants<'_>) { } } - match STATE.write() { - Ok(mut guard) => *guard = st, - // A poisoned lock this early means a panic already unwound through a - // permission check; refuse to run rather than run unsandboxed. - Err(_) => bun_core::Output::panic(format_args!("permission model state is unrecoverable")), - } + *STATE.write() = st; ENABLED.store(true, Ordering::Release); } @@ -294,11 +289,7 @@ pub fn is_granted(scope: Scope, reference: Option<&[u8]>) -> bool { // Node reports (`process.permission` does not even exist there). return true; } - let Ok(st) = STATE.read() else { - // Fail closed: a poisoned lock means we cannot prove the access is - // allowed. - return false; - }; + let st = STATE.read(); match scope { // Node: `has('fs')` is true only when both directions are fully open. Scope::FileSystem => st.fs_read.allow_all && st.fs_write.allow_all, @@ -324,9 +315,7 @@ fn drop_scope(scope: Scope, reference: Option<&[u8]>) { if !is_enabled() { return; } - let Ok(mut st) = STATE.write() else { - return; - }; + let mut st = STATE.write(); let reference = reference.filter(|r| !r.is_empty()); match (scope, reference) { (Scope::FileSystem, None) => { @@ -466,9 +455,7 @@ pub fn emit_startup_warnings(global: &JSGlobalObject) { return; } let (bypass_flags, net_granted, comma_flags) = { - let Ok(st) = STATE.read() else { - return; - }; + let st = STATE.read(); // Order matches Node's `warnFlags`. `--allow-ffi` is omitted: Bun does // not build with `node_use_ffi`, so Node would not warn for it either. ( @@ -705,8 +692,9 @@ pub struct NodeOptionsGrants { } pub fn grants_from_node_options() -> Option { - let value = std::env::var("NODE_OPTIONS").ok()?; - if !value.contains("--permission") { + use bstr::ByteSlice; + let value = bun_core::env_var::NODE_OPTIONS::get()?; + if !value.contains_str("--permission") { return None; } let mut grants = NodeOptionsGrants { @@ -722,24 +710,20 @@ pub fn grants_from_node_options() -> Option { }; // Node splits NODE_OPTIONS on whitespace with no quoting; the flags Node // itself injects are always in `--flag=value` form. - for token in value.split_ascii_whitespace() { + for token in value.fields_with(|c| c.is_ascii_whitespace()) { match token { - "--permission" => grants.permission = true, - "--allow-child-process" => grants.child = true, - "--allow-worker" => grants.worker = true, - "--allow-inspector" => grants.inspector = true, - "--allow-wasi" => grants.wasi = true, - "--allow-net" => grants.net = true, - "--allow-addons" => grants.addon = true, + b"--permission" => grants.permission = true, + b"--allow-child-process" => grants.child = true, + b"--allow-worker" => grants.worker = true, + b"--allow-inspector" => grants.inspector = true, + b"--allow-wasi" => grants.wasi = true, + b"--allow-net" => grants.net = true, + b"--allow-addons" => grants.addon = true, _ => { - if let Some(value) = token.strip_prefix("--allow-fs-read=") { - grants - .fs_read - .push(&*Box::leak(Box::<[u8]>::from(value.as_bytes()))); - } else if let Some(value) = token.strip_prefix("--allow-fs-write=") { - grants - .fs_write - .push(&*Box::leak(Box::<[u8]>::from(value.as_bytes()))); + if let Some(value) = token.strip_prefix(b"--allow-fs-read=".as_slice()) { + grants.fs_read.push(&*Box::leak(Box::<[u8]>::from(value))); + } else if let Some(value) = token.strip_prefix(b"--allow-fs-write=".as_slice()) { + grants.fs_write.push(&*Box::leak(Box::<[u8]>::from(value))); } } } diff --git a/src/runtime/timer/Timer.rs b/src/runtime/timer/Timer.rs index 50966bd309ea..ff6cb23009d5 100644 --- a/src/runtime/timer/Timer.rs +++ b/src/runtime/timer/Timer.rs @@ -420,8 +420,8 @@ impl All { let Some(timer) = timer else { return Ok(()) }; // Node's unenroll: a timer cleared by clearTimeout/clearInterval reads // back `_idleTimeout === -1` (a naturally fired one keeps its duration). - // SAFETY: timer points to a live TimerObjectInternals if kind != Kind::SetImmediate + // SAFETY: timer points to a live TimerObjectInternals && let Some(js_timer) = unsafe { (*timer).this_value.get().try_get() } { crate::jsc::generated::JSTimeout::idle_timeout_set_cached( From 9a8f8ceef3fd8aa0cf9c5fcc2b7b04371f2550e6 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 4 Aug 2026 17:54:02 +0000 Subject: [PATCH 08/11] [autofix.ci] apply automated fixes --- test/js/bun/util/inspect.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index 344188d1ca2f..72681954338d 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -1,5 +1,5 @@ import { describe, expect, it } from "bun:test"; -import { bunEnv, bunExe, normalizeBunSnapshot, tmpdirSync } from "harness"; +import { bunEnv, bunExe, tmpdirSync } from "harness"; import { join } from "path"; import util from "util"; it("prototype", () => { From 05e8248fdb17bd0d8b6d08415149c7ce3b96edf6 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 21 Aug 2026 05:58:13 +0000 Subject: [PATCH 09/11] test: restore the upstream Error: expectations in two vendored tests now that uncaught errors print Node-style --- .../parallel/test-process-uncaught-exception-monitor.js | 6 ++---- .../node/test/parallel/test-vm-api-handles-getter-errors.js | 4 +--- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/test/js/node/test/parallel/test-process-uncaught-exception-monitor.js b/test/js/node/test/parallel/test-process-uncaught-exception-monitor.js index c8394d396df1..651ee34d18b0 100644 --- a/test/js/node/test/parallel/test-process-uncaught-exception-monitor.js +++ b/test/js/node/test/parallel/test-process-uncaught-exception-monitor.js @@ -16,10 +16,8 @@ const fixtures = require('../common/fixtures'); assert.strictEqual(Object.getPrototypeOf(err).name, 'Error'); assert.strictEqual(stdout, 'Monitored: Shall exit\n'); const errLines = stderr.trim().split(/[\r\n]+/); - // Bun renders a base Error as "error: " instead of Node's - // "Error: " stack header. - const errLine = errLines.find((l) => /^(?:E|e)rror/.exec(l)); - assert.strictEqual(errLine, 'error: Shall exit'); + const errLine = errLines.find((l) => /^Error/.exec(l)); + assert.strictEqual(errLine, 'Error: Shall exit'); }) ); } diff --git a/test/js/node/test/parallel/test-vm-api-handles-getter-errors.js b/test/js/node/test/parallel/test-vm-api-handles-getter-errors.js index 6fe42c7e8f6d..6a74fb29c17c 100644 --- a/test/js/node/test/parallel/test-vm-api-handles-getter-errors.js +++ b/test/js/node/test/parallel/test-vm-api-handles-getter-errors.js @@ -30,8 +30,6 @@ const scripts = []; scripts.forEach((script) => { const node = process.execPath; execFile(node, [ '-e', script ], common.mustCall((err, stdout, stderr) => { - // Bun's uncaught-error printer lowercases the "error:" prefix - const expected = typeof Bun === 'undefined' ? 'Error: xyz' : 'error: xyz'; - assert(stderr.includes(expected), 'createScript crashes'); + assert(stderr.includes('Error: xyz'), 'createScript crashes'); })); }); From 03987811b5f24b332cbf07107e35f95dc25efbc9 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 21 Aug 2026 05:58:13 +0000 Subject: [PATCH 10/11] test: drop test-internal-modules.js, the test harness exposes internals via BUN_FEATURE_FLAG_INTERNAL_FOR_TESTING --- test/js/node/test/parallel/test-internal-modules.js | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 test/js/node/test/parallel/test-internal-modules.js diff --git a/test/js/node/test/parallel/test-internal-modules.js b/test/js/node/test/parallel/test-internal-modules.js deleted file mode 100644 index e6bd0c9e1413..000000000000 --- a/test/js/node/test/parallel/test-internal-modules.js +++ /dev/null @@ -1,13 +0,0 @@ -'use strict'; -require('../common'); -const fixtures = require('../common/fixtures'); -const assert = require('assert'); - -assert.throws(function() { - require('internal/freelist'); -}, /^Error: Cannot find module 'internal\/freelist'/); - -assert.strictEqual( - require(fixtures.path('internal-modules')), - 42 -); From 38783fde79eceda354b823beb348e70ee3939f55 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 21 Aug 2026 05:58:13 +0000 Subject: [PATCH 11/11] test: update assertions on uncaught-error output and resolve-error names to the Node-style rendering --- test/bundler/bundler_bun.test.ts | 2 +- test/bundler/bundler_compile.test.ts | 4 ++-- test/cli/install/bun-install-lifecycle-scripts.test.ts | 2 +- test/js/bun/resolve/resolve.test.ts | 6 +++--- test/js/bun/util/inspect-error.test.js | 2 +- test/js/node/missing-module.test.js | 2 +- test/js/node/no-addons.test.ts | 2 +- test/js/node/process/process-execve.test.ts | 4 ++-- test/regression/issue/25622.test.ts | 2 +- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/test/bundler/bundler_bun.test.ts b/test/bundler/bundler_bun.test.ts index 674da11059bb..509f55e1982d 100644 --- a/test/bundler/bundler_bun.test.ts +++ b/test/bundler/bundler_bun.test.ts @@ -122,7 +122,7 @@ describe("bundler", () => { 5 | // hello world 6 | throw new ^ -error: Hello World`, +Error: Hello World`, ); expect(stderr).toInclude("entry.ts:6:19"); }, diff --git a/test/bundler/bundler_compile.test.ts b/test/bundler/bundler_compile.test.ts index 01b514dbbe54..0993ddc0690a 100644 --- a/test/bundler/bundler_compile.test.ts +++ b/test/bundler/bundler_compile.test.ts @@ -967,7 +967,7 @@ describe("bundler", () => { 5 | // hello world 6 | throw new ^ -error: Hello World`, +Error: Hello World`, ); expect(stderr).toInclude("entry.ts:6:19"); }, @@ -1005,7 +1005,7 @@ console.log(ReactDom);`, 7 | // hello world 8 | throw new ^ -error: Hello World`, +Error: Hello World`, ); expect(stderr).toInclude("entry.ts:8:19"); }, diff --git a/test/cli/install/bun-install-lifecycle-scripts.test.ts b/test/cli/install/bun-install-lifecycle-scripts.test.ts index aea7f38ebec4..1f527b0a25c3 100644 --- a/test/cli/install/bun-install-lifecycle-scripts.test.ts +++ b/test/cli/install/bun-install-lifecycle-scripts.test.ts @@ -1528,7 +1528,7 @@ for (const forceWaiterThread of isLinux ? [false, true] : [false]) { expect(await stdout.text()).toEqual(expect.stringContaining("bun install v1.")); const err = await stderr.text(); - expect(err).toContain("error: Oops!"); + expect(err).toContain("Error: Oops!"); expect(err).toContain('error: preinstall script from "fooooooooo" exited with 1'); }); diff --git a/test/js/bun/resolve/resolve.test.ts b/test/js/bun/resolve/resolve.test.ts index dff0a0e94b2f..4952fa7a5522 100644 --- a/test/js/bun/resolve/resolve.test.ts +++ b/test/js/bun/resolve/resolve.test.ts @@ -1265,7 +1265,7 @@ describe("wildcard exports with extensionless target", () => { const result = await runWildcardScript(String(dir), "index.ts"); expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Cannot find module"); + expect(result.stderr).toMatch(/Cannot find (?:module|package) 'wildcard-pkg/); }); test.concurrent("CJS require of extensionless wildcard target also resolves", async () => { @@ -1304,7 +1304,7 @@ describe("wildcard exports with extensionless target", () => { const result = await runWildcardScript(String(dir), "index.ts"); expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain("Cannot find module"); + expect(result.stderr).toMatch(/Cannot find (?:module|package) 'explicit-pkg/); }); test.concurrent("resolves sibling `.js` when a same-named directory exists", async () => { @@ -1547,7 +1547,7 @@ it.skipIf(isWindows)("reports a resolution error for an absolute specifier of th stderr: "pipe", }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stdout).toBe("ResolveMessage ERR_MODULE_NOT_FOUND\n"); + expect(stdout).toBe("Error ERR_MODULE_NOT_FOUND\n"); expect(exitCode).toBe(0); }); diff --git a/test/js/bun/util/inspect-error.test.js b/test/js/bun/util/inspect-error.test.js index dcf0cc2b7617..e5ddbc0d1845 100644 --- a/test/js/bun/util/inspect-error.test.js +++ b/test/js/bun/util/inspect-error.test.js @@ -406,7 +406,7 @@ describe.concurrent("AggregateError whose errors cannot be walked", () => { "-e", 'throw new AggregateError([new Error("first member"), new TypeError("second member")], "outer message");', ]); - expect(stderr).toContain("error: first member"); + expect(stderr).toContain("Error: first member"); expect(stderr).toContain("TypeError: second member"); expect(stderr).not.toContain(header); expect(exitCode).toBe(1); diff --git a/test/js/node/missing-module.test.js b/test/js/node/missing-module.test.js index 144f647e72df..41d4a519027e 100644 --- a/test/js/node/missing-module.test.js +++ b/test/js/node/missing-module.test.js @@ -29,7 +29,7 @@ test("not implemented yet module throws an error", () => { code: "MODULE_NOT_FOUND", }); assert.rejects(() => import(missingBun), { - message: /^Cannot find package 'bun:missing' imported from /, + message: /^Cannot find module 'bun:missing' imported from /, code: "ERR_MODULE_NOT_FOUND", }); diff --git a/test/js/node/no-addons.test.ts b/test/js/node/no-addons.test.ts index f11e8c037034..ba5d9881df00 100644 --- a/test/js/node/no-addons.test.ts +++ b/test/js/node/no-addons.test.ts @@ -42,5 +42,5 @@ test("--no-addons throws an error on process.dlopen", () => { const out = stdout.toString(); expect(exitCode).toBe(1); expect(out).toBeEmpty(); - expect(err).toContain("\nerror: Cannot load native addon because loading addons is disabled."); + expect(err).toContain("\nError: Cannot load native addon because loading addons is disabled."); }); diff --git a/test/js/node/process/process-execve.test.ts b/test/js/node/process/process-execve.test.ts index acaae26ef745..479bff713e07 100644 --- a/test/js/node/process/process-execve.test.ts +++ b/test/js/node/process/process-execve.test.ts @@ -223,8 +223,8 @@ describe.concurrent("process.execve", () => { const [_stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); expect(stderr).not.toContain("ERR_INVALID_ARG_TYPE"); - expect(stderr).toContain('code: "ENOENT"'); - expect(stderr).toContain('syscall: "execve"'); + expect(stderr).toContain("code: 'ENOENT'"); + expect(stderr).toContain("syscall: 'execve'"); expect(exitCode).not.toBe(0); }); diff --git a/test/regression/issue/25622.test.ts b/test/regression/issue/25622.test.ts index 3a57d8ed8335..2de07ea04313 100644 --- a/test/regression/issue/25622.test.ts +++ b/test/regression/issue/25622.test.ts @@ -43,7 +43,7 @@ test("child tsconfig paths replace parent paths (not merge)", async () => { // Before the fix, Bun merged paths and this would print "should not resolve" // with exit 0. Now it should fail to resolve @helpers/x. - expect(stderr).toContain("Cannot find module '@helpers/x'"); + expect(stderr).toMatch(/Cannot find (?:module|package) '@helpers\/x'/); expect(stdout).not.toContain("should not resolve"); expect(exitCode).not.toBe(0); });