Skip to content
Open
1 change: 1 addition & 0 deletions src/bundler/transpiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1118,6 +1118,7 @@ pub(crate) fn resolver_bundle_options_subset(
polyfill_node_globals: src.polyfill_node_globals,
prefer_offline_install: src.prefer_offline_install,
preserve_symlinks: src.preserve_symlinks,
preserve_symlinks_main: false,
rewrite_jest_for_tests: src.rewrite_jest_for_tests,
tsconfig_override: src.tsconfig_override.clone(),
production: src.production,
Expand Down
11 changes: 11 additions & 0 deletions src/js/internal/process/pre_execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ function installExitTracing(): void {
let catString: string | null = null;
let filePattern: string | null = null;
let stackTraceLimit: string | null = null;
let heapsnapshotSignal: string | null = null;
let traceEnv = false;
let traceEnvJsStack = false;
let traceExit = false;
Expand Down Expand Up @@ -292,6 +293,10 @@ function installExitTracing(): void {
// keys its native-stack assertions on that string appearing.
} else if (arg === "--trace-exit") {
traceExit = true;
} else if (arg === "--heapsnapshot-signal") {
if (i + 1 < execArgv.length) heapsnapshotSignal = execArgv[++i];
} else if (arg.startsWith("--heapsnapshot-signal=")) {
heapsnapshotSignal = arg.slice("--heapsnapshot-signal=".length);
}
}

Expand Down Expand Up @@ -333,6 +338,12 @@ function installExitTracing(): void {
envTracePrintJsStack = traceEnvJsStack;
installEnvTracing();
}
if (heapsnapshotSignal !== null && process.platform !== "win32") {
require("internal/validators").validateSignalName(heapsnapshotSignal);
process.on(heapsnapshotSignal as NodeJS.Signals, () => {
require("node:v8").writeHeapSnapshot();
});
}
}

export default {};
28 changes: 22 additions & 6 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2304,7 +2304,9 @@ impl VirtualMachine {
// execArgv. (The JS side re-reads `process.execArgv`, so an explicit
// empty execArgv under a traced parent stays a no-op there.)
fn is_bootstrap_flag(arg: &[u8]) -> bool {
arg.starts_with(b"--trace-") || arg.starts_with(b"--stack-trace-limit")
arg.starts_with(b"--trace-")
|| arg.starts_with(b"--stack-trace-limit")
|| arg.starts_with(b"--heapsnapshot-signal")
}
let needs_pre_execution = bun_core::argv().into_iter().any(is_bootstrap_flag)
|| self
Expand Down Expand Up @@ -4048,7 +4050,8 @@ impl VirtualMachine {
return Err(crate::CrateError::ModuleNotFound);
}

let is_special_source = source == MAIN_FILE_NAME || Macro::is_macro_path(source);
let is_main_entry_resolve = source == MAIN_FILE_NAME;
let is_special_source = is_main_entry_resolve || Macro::is_macro_path(source);
let mut query_string: &[u8] = b"";
let normalized_specifier = normalize_specifier_for_resolution(specifier, &mut query_string);
let top_level_dir = self.top_level_dir();
Expand Down Expand Up @@ -4157,10 +4160,23 @@ impl VirtualMachine {
let result_path = result
.path_const()
.ok_or(crate::CrateError::ModuleNotFound)?;
// SAFETY: `result_path.text` borrows the resolver's arena, which
// outlives `ResolveFunctionResult` (see the struct's lifetime-erasure
// note).
ret.path = unsafe { bun_ptr::detach_lifetime(result_path.text) };
// `--preserve-symlinks-main`: the synthetic `bun:main` wrapper resolving
// the entry module must keep the symlink spelling. `finalize_result`
// stores the pre-realpath text in `.pretty` when it rewrites `.text`
// (see `Path::set_realpath`), so read it back for this one resolve.
Comment thread
robobun marked this conversation as resolved.
Outdated
let path_text = if is_main_entry_resolve
&& self.transpiler.resolver.opts.preserve_symlinks_main
&& result_path.is_symlink
&& !result_path.pretty.is_empty()
{
result_path.pretty
} else {
result_path.text
};
// SAFETY: `result_path.text`/`.pretty` borrow the resolver's arena,
// which outlives `ResolveFunctionResult` (see the struct's
// lifetime-erasure note).
ret.path = unsafe { bun_ptr::detach_lifetime(path_text) };
Comment thread
robobun marked this conversation as resolved.
ret.result = Some(result);

Ok(())
Expand Down
8 changes: 7 additions & 1 deletion src/jsc/bindings/BunProcess.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1395,6 +1395,9 @@ extern "C" bool Bun__shouldIgnoreOneDisconnectEventListener(JSC::JSGlobalObject*
extern "C" void Bun__ensureSignalHandler();
extern "C" bool Bun__isMainThreadVM();
extern "C" void Bun__onPosixSignal(int signalNumber);
#if !OS(WINDOWS)
extern "C" void Bun__noopSignalHandler(int);
#endif

__attribute__((noinline)) static void forwardSignal(int signalNumber)
{
Expand Down Expand Up @@ -1587,7 +1590,10 @@ static void onDidChangeListeners(EventEmitter& eventEmitter, const Identifier& e
if (signalToContextIdsMap->find(signalNumber) != signalToContextIdsMap->end() && eventEmitter.listenerCount(eventName) == 0) {

#if !OS(WINDOWS)
if (void (*oldHandler)(int) = signal(signalNumber, SIG_DFL); oldHandler != forwardSignal) {
// SIGUSR1 is reserved (inert) by default; restoring SIG_DFL would
// make it fatal. Restore the startup no-op handler instead.
Comment thread
robobun marked this conversation as resolved.
Outdated
void (*restoreTo)(int) = signalNumber == SIGUSR1 ? Bun__noopSignalHandler : SIG_DFL;
if (void (*oldHandler)(int) = signal(signalNumber, restoreTo); oldHandler != forwardSignal) {
// Don't uninstall the old handler if it's not the one we installed.
signal(signalNumber, oldHandler);
}
Expand Down
20 changes: 20 additions & 0 deletions src/jsc/bindings/c-bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,12 @@ extern "C" void Bun__setCTRLHandler(BOOL add)

extern "C" int32_t bun_is_stdio_null[3] = { 0, 0, 0 };

#if !OS(WINDOWS)
extern "C" void Bun__noopSignalHandler(int)
{
}
#endif

extern "C" void bun_initialize_process()
{
// Disable printf() buffering. We buffer it ourselves.
Expand Down Expand Up @@ -653,6 +659,20 @@ extern "C" void bun_initialize_process()
sigaction(SIGTERM, &sa, nullptr);
sigaction(SIGINT, &sa, nullptr);
}

// Node.js reserves SIGUSR1 for the debugger and never lets it terminate
// the process by default. Bun does not start an inspector on SIGUSR1, but
// still makes the signal inert so ops tooling that signals a node-compatible
// process does not kill it. A handler (not SIG_IGN) is used so the
// disposition resets to SIG_DFL across exec() for child processes.
Comment thread
robobun marked this conversation as resolved.
Outdated
{
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sa.sa_handler = Bun__noopSignalHandler;
sigaction(SIGUSR1, &sa, nullptr);
}
#elif OS(WINDOWS)
for (int fd = 0; fd <= 2; ++fd) {
auto handle = reinterpret_cast<HANDLE>(uv_get_osfhandle(fd));
Expand Down
5 changes: 5 additions & 0 deletions src/resolver/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,10 @@ pub struct BundleOptions {
pub polyfill_node_globals: bool,
pub prefer_offline_install: bool,
pub preserve_symlinks: bool,
/// `--preserve-symlinks-main`: applies `preserve_symlinks` semantics to
/// the main entry module only. Consumed by the VM's `_resolve` when the
/// source is the synthetic `bun:main` wrapper.
Comment thread
robobun marked this conversation as resolved.
Outdated
pub preserve_symlinks_main: bool,
pub rewrite_jest_for_tests: bool,
pub tsconfig_override: Option<Box<[u8]>>,
pub production: bool,
Expand Down Expand Up @@ -275,6 +279,7 @@ impl Default for BundleOptions {
polyfill_node_globals: false,
prefer_offline_install: false,
preserve_symlinks: false,
preserve_symlinks_main: false,
rewrite_jest_for_tests: false,
tsconfig_override: None,
output_dir: Box::default(),
Expand Down
12 changes: 10 additions & 2 deletions src/resolver/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1665,8 +1665,16 @@
module_type_from_ext(name.ext).unwrap_or(options::ModuleType::Unknown);
}

if let Some(entries) = dir.get_entries_ref(self.generation) {
if let Some(query) = entries.get(name.filename) {
// `--preserve-symlinks` (Node.js semantics): keep the symlink
// spelling as the resolved path so `__filename` / the module
// cache key is the link, not the realpath. `dir.abs_real_path`
// is already left empty under this flag (see `dir_info_uncached`),
// so skipping this block is the only remaining realpath source.
Comment thread
robobun marked this conversation as resolved.
Outdated
if !self.opts.preserve_symlinks
&& let Some(entries) = dir.get_entries_ref(self.generation)
&& let Some(query) = entries.get(name.filename)
{
{

Check warning on line 1677 in src/resolver/resolver.rs

View check run for this annotation

Claude / Claude Code Review

Redundant nested block after let-chain refactor

The let-chain refactor kept the second opening brace from the old nested `if let Some(...) { if let Some(...) {`, leaving a redundant `{ { ... } }` block at lines 1676–1677 that scopes nothing. Delete the inner `{` and its matching `}` so the body sits directly under the let-chain.
Comment thread
robobun marked this conversation as resolved.
Outdated
// SAFETY: entries_mutex held; rfs points at the process-global RealFS.
let symlink_path =
unsafe { query.entry().symlink(self.rfs_ptr(), self.store_fd) };
Expand Down
1 change: 1 addition & 0 deletions src/runtime/cli/Arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ pub(crate) const RUNTIME_PARAMS_: &[ParamType] = &[
parse_param!("--trace-exit"),
parse_param!("--expose-internals"),
parse_param!("--stack-trace-limit <STR>"),
parse_param!("--heapsnapshot-signal <STR>"),
];

pub(crate) const AUTO_OR_RUN_PARAMS: &[ParamType] = &[
Expand Down
35 changes: 33 additions & 2 deletions src/runtime/cli/run_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,11 @@ Full documentation is available at <magenta>https://bun.com/docs/cli/run<r>
vm.argv = std::mem::take(&mut ctx.passthrough);
// `InitOptions` has no `store_fd` field, so set it on the resolver directly.
vm.transpiler.resolver.store_fd = ctx.debug.hot_reload != cli::command::HotReload::None;
vm.transpiler.resolver.opts.preserve_symlinks_main =
ctx.runtime_options.preserve_symlinks_main
|| bun_core::env_var::NODE_PRESERVE_SYMLINKS_MAIN
.get()
.unwrap_or(false);
// `vm.dns_result_order` is a `u8` until the b2-cycle widens
// it to `bun_dns::Order`; the enum is `#[repr(u8)]` so `as u8` is exact.
vm.dns_result_order =
Expand Down Expand Up @@ -2870,9 +2875,35 @@ impl RunCommand {
..Default::default()
});

let preserve_symlinks_main = ctx.runtime_options.preserve_symlinks_main
|| bun_core::env_var::NODE_PRESERVE_SYMLINKS_MAIN
.get()
.unwrap_or(false);
Comment thread
robobun marked this conversation as resolved.
Outdated

// Re-derive the canonical absolute path from the open fd (resolves
// symlinks).
let absolute_script_path: Box<[u8]> = {
// symlinks). With `--preserve-symlinks-main`, Node.js keeps the
// symlink spelling for the entry module's `__filename`, so skip the
// realpath and resolve `target` against cwd instead.
Comment thread
robobun marked this conversation as resolved.
Outdated
let absolute_script_path: Box<[u8]> = if preserve_symlinks_main {
let mut cwd_buf = PathBuffer::uninit();
let Ok(cwd) = bun_core::getcwd(&mut cwd_buf) else {
let _ = bun_sys::close(fd);
return false;
};
let cwd_len = cwd.as_bytes().len();
cwd_buf[cwd_len] = paths::SEP;
let joined = paths::resolve_path::join_abs_string_buf::<paths::platform::Auto>(
&cwd_buf[..cwd_len + 1],
&mut script_name_buf.0,
&[target],
);
let joined = strings::without_trailing_slash(joined);
if joined.is_empty() {
let _ = bun_sys::close(fd);
return false;
}
joined.to_vec().into_boxed_slice()
} else {
let resolved = match bun_sys::get_fd_path(fd, &mut script_name_buf) {
Ok(p) => p,
Err(_) => {
Expand Down
Loading
Loading