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
25 changes: 20 additions & 5 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4048,7 +4048,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 +4158,24 @@ 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) };
// Node.js: `--preserve-symlinks` keeps the link spelling for required
// modules only; `--preserve-symlinks-main` extends it to the entry.
// `Path::set_realpath` stashed the pre-realpath spelling in `.pretty`.
Comment thread
robobun marked this conversation as resolved.
let opts = &self.transpiler.resolver.opts;
let preserve = if is_main_entry_resolve {
opts.preserve_symlinks_main
} else {
opts.preserve_symlinks
};
let path_text = if preserve && 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
7 changes: 6 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,9 @@ 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 stays inert (reserved); SIG_DFL would make it fatal.
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
17 changes: 17 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,17 @@ extern "C" void bun_initialize_process()
sigaction(SIGTERM, &sa, nullptr);
sigaction(SIGINT, &sa, nullptr);
}

// Node.js reserves SIGUSR1 (debugger) so it is never fatal by default; a
// handler rather than SIG_IGN so exec()'d children revert to SIG_DFL.
Comment thread
robobun marked this conversation as resolved.
{
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
11 changes: 11 additions & 0 deletions src/options_types/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -622,3 +622,14 @@ impl Default for RuntimeOptions {
}
}
}

impl RuntimeOptions {
/// `--preserve-symlinks-main` or `NODE_PRESERVE_SYMLINKS_MAIN=1`.
#[inline]
pub fn preserve_symlinks_main_effective(&self) -> bool {
self.preserve_symlinks_main
|| bun_core::env_var::NODE_PRESERVE_SYMLINKS_MAIN
.get()
.unwrap_or(false)
}
}
4 changes: 4 additions & 0 deletions src/resolver/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,9 @@ pub struct BundleOptions {
pub polyfill_node_globals: bool,
pub prefer_offline_install: bool,
pub preserve_symlinks: bool,
/// `--preserve-symlinks-main`: `preserve_symlinks` for the entry only
/// (read by `_resolve` when source is `bun:main`).
Comment thread
robobun marked this conversation as resolved.
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 +278,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
52 changes: 42 additions & 10 deletions src/runtime/cli/run_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,8 @@
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_effective();
// `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 @@ -2560,15 +2562,12 @@
bstr::BStr::new(target_name),
bstr::BStr::new(fs_top_level_dir),
);
// Temporarily honor `--preserve-symlinks-main` / NODE_PRESERVE_SYMLINKS_MAIN
// for this one resolve.
let preserve_symlinks_main = ctx.runtime_options.preserve_symlinks_main_effective();
let resolution: ::core::result::Result<bun_resolver::Result, bun_resolver::Error> = {
let saved_preserve = this_transpiler.resolver.opts.preserve_symlinks;
this_transpiler.resolver.opts.preserve_symlinks =
ctx.runtime_options.preserve_symlinks_main
|| bun_core::env_var::NODE_PRESERVE_SYMLINKS_MAIN
.get()
.unwrap_or(false);
this_transpiler.resolver.opts.preserve_symlinks = preserve_symlinks_main;

Check warning on line 2570 in src/runtime/cli/run_command.rs

View check run for this annotation

Claude / Claude Code Review

Vestigial preserve_symlinks save/restore now redundant; comment is misleading

The comment "Temporarily honor `--preserve-symlinks-main`… for this one resolve" at 2565–2566 no longer describes how the honoring works — this PR added the actual mechanism at line 2611 (the `.pretty` selection), and the `saved_preserve` override's only remaining resolver reader is the directory `abs_real_path` gate at `resolver.rs:6115`, which the comment does not describe. Consider updating or dropping the comment (2608–2609 already documents the real mechanism); if the save/restore is kept f
Comment thread
robobun marked this conversation as resolved.
Outdated
// SAFETY: `Transpiler::init` always sets `fs`; resolver-cache lifetime.
let top_level_dir = unsafe { (*this_transpiler.fs).top_level_dir };
let resolved = match this_transpiler.resolver.resolve(
Expand Down Expand Up @@ -2606,10 +2605,22 @@
.or_else(|| bun_bundler::options::DEFAULT_LOADERS.get(ext).copied())
.unwrap_or(Loader::Tsx);
if loader.can_be_run_by_bun() || loader == Loader::Html || loader == Loader::Md {
bun_core::scoped_log!(RUN_LOG, "Resolved to: `{}`", bstr::BStr::new(path.text));
// `--preserve-symlinks-main`: `set_realpath` stashed the
// link spelling in `.pretty`.
Comment thread
robobun marked this conversation as resolved.
let entry_text =
if preserve_symlinks_main && path.is_symlink && !path.pretty.is_empty() {
path.pretty
} else {
path.text
};
bun_core::scoped_log!(
RUN_LOG,
"Resolved to: `{}`",
bstr::BStr::new(entry_text)
);
// borrowck — `_boot_and_handle_error` takes
// `&mut ctx`; copy `path.text` out of the resolver borrow.
let text: Box<[u8]> = path.text.to_vec().into_boxed_slice();
// `&mut ctx`; copy `entry_text` out of the resolver borrow.
let text: Box<[u8]> = entry_text.to_vec().into_boxed_slice();
return Ok(Self::_boot_and_handle_error(ctx, &text, Some(loader)));
} else {
bun_core::scoped_log!(
Expand Down Expand Up @@ -2870,9 +2881,30 @@
..Default::default()
});

let preserve_symlinks_main = ctx.runtime_options.preserve_symlinks_main_effective();

// Re-derive the canonical absolute path from the open fd (resolves
// symlinks).
let absolute_script_path: Box<[u8]> = {
// symlinks), or under `--preserve-symlinks-main` join against cwd.
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