Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0eb1e15
cli: make the entry point of the node shim the main module
robobun Sep 18, 2026
c5518fe
test: run the symlink cases of the node shim on Windows too
robobun Sep 18, 2026
1c97f17
cli: cite Node's resolveMainPath for the unresolved entry path
robobun Sep 18, 2026
bfeb333
test: cron execution mode under the node shim with a directory entry
robobun Sep 18, 2026
81de69d
test: run the node shim tests one at a time
robobun Sep 18, 2026
9165b16
cli: resolve the node shim's entry right before it loads
robobun Sep 19, 2026
d62e10a
cli: keep the new comments to one line
robobun Sep 19, 2026
31525e2
Merge branch 'main' into the node shim entry branch
robobun Oct 11, 2026
5fb59aa
Merge remote-tracking branch 'origin/main' into robobun/b560be00/node…
robobun Oct 15, 2026
178e580
cli: resolve the entry point of the node shim in boot, with Node's ru…
robobun Oct 15, 2026
f18624f
test: node shim entry under --hot and --watch, a symlinked main modul…
robobun Oct 15, 2026
5104a6c
cli: keep the symlink rule of the imports in the resolver cache; forg…
robobun Oct 16, 2026
9bf381e
Merge remote-tracking branch 'origin/main' into robobun/b560be00/node…
robobun Oct 16, 2026
bc88743
cli: forget what a failed resolve of the node shim's entry put in the…
robobun Oct 17, 2026
98cf4a2
Merge remote-tracking branch 'origin/main' into robobun/b560be00/node…
robobun Oct 17, 2026
5da3625
Merge remote-tracking branch 'origin/main' into robobun/b560be00/node…
robobun Oct 18, 2026
3542549
Merge remote-tracking branch 'origin/main' into robobun/b560be00/node…
robobun Oct 19, 2026
5746260
Merge remote-tracking branch 'origin/main' into robobun/b560be00/node…
robobun Oct 21, 2026
cf054f0
Merge branch 'main' into robobun/b560be00/node-shim-resolve-entry
robobun Oct 28, 2026
c50ff40
resolver: join a main field with the checked join in bust_dir_cache_o…
robobun Oct 28, 2026
c3d9e7c
watcher: watch the directory of the entry point outside the project
robobun Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,9 @@ pub struct VirtualMachine {
/// `RawSlice` carries the BACKREF outlives-holder invariant — read via
/// `main()`.
main: bun_ptr::RawSlice<u8>,
/// `process.argv[1]` when it is not `main`: the `node` shim keeps the path
/// the user gave, like Node.
Comment thread
robobun marked this conversation as resolved.
Resolved
main_for_argv: Option<&'static [u8]>,
pub main_is_html_entrypoint: bool,
pub main_resolved_path: bun_core::String,
pub main_hash: u32,
Expand Down Expand Up @@ -3120,6 +3123,7 @@ impl VirtualMachine {
// `log` is a fresh leaked Box; outlives the VM.
addr_of_mut!((*vm).log).write(NonNull::new(log));
addr_of_mut!((*vm).main).write(bun_ptr::RawSlice::EMPTY);
addr_of_mut!((*vm).main_for_argv).write(None);
addr_of_mut!((*vm).main_hash).write(0);
addr_of_mut!((*vm).main_resolved_path).write(bun_core::String::EMPTY);
addr_of_mut!((*vm).hide_bun_stackframes).write(true);
Expand Down Expand Up @@ -3308,6 +3312,18 @@ impl VirtualMachine {
self.main = bun_ptr::RawSlice::new(path);
}

/// The entry path for `process.argv[1]`: `main()`, unless
/// `set_main_for_argv` gave another path.
Comment thread
robobun marked this conversation as resolved.
Resolved
#[inline]
pub fn main_for_argv(&self) -> &[u8] {
self.main_for_argv.unwrap_or_else(|| self.main())
}

#[inline]
pub fn set_main_for_argv(&mut self, path: &'static [u8]) {
self.main_for_argv = Some(path);
}

/// `eventLoop().waitForPromise(promise)` — spin tick/auto_tick until
/// `promise` settles. Thin forwarder; body lives in
/// [`crate::event_loop::EventLoop::wait_for_promise`].
Expand Down
9 changes: 7 additions & 2 deletions src/runtime/cli/fuzzilli_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use bun_core::{Environment, Global};
use bun_sys::{self as sys, Fd, FdExt, O};

#[cfg(unix)]
use super::run_command::RunCommand;
use super::run_command::{EntryPath, RunCommand};
use crate::Command;

pub(crate) struct FuzzilliCommand;
Expand Down Expand Up @@ -92,7 +92,12 @@ impl FuzzilliCommand {
let temp_path: &[u8] = b"/tmp/bun-fuzzilli-reprl.js";
// The `Run.boot` entry point is hosted on `RunCommand` to avoid the
// higher-tier crate cycle (see run_command.rs §`Run`).
let result = RunCommand::boot(_ctx, temp_path.to_vec().into_boxed_slice(), None);
let result = RunCommand::boot(
_ctx,
temp_path.to_vec().into_boxed_slice(),
EntryPath::Resolved,
None,
);

// `defer fd.close()` — Fd is Copy and has no Drop; close explicitly.
temp_file_fd.close();
Expand Down
65 changes: 59 additions & 6 deletions src/runtime/cli/run_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,18 @@ pub(crate) struct ConfigureEnvOptions {
pub(crate) store_root_fd: bool,
}

/// What the `entry_path` of [`RunCommand::boot`] names.
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum EntryPath {
/// The file to run.
Resolved,
/// A path as `node` takes it: a directory, or a file without its
/// extension, also runs. `Run::start` resolves it to the file to run and
/// keeps the given path as `process.argv[1]`, like Node's `resolveMainPath`:
/// https://github.com/nodejs/node/blob/v26.3.0/lib/internal/modules/run_main.js#L29-L45
Comment thread
robobun marked this conversation as resolved.
Resolved
Unresolved,
}

pub(crate) struct RunCommand;

impl RunCommand {
Expand Down Expand Up @@ -934,6 +946,7 @@ Full documentation is available at <magenta>https://bun.com/docs/cli/run<r>
pub(crate) fn boot(
ctx: &mut ContextData,
entry_path: Box<[u8]>,
entry_kind: EntryPath,
loader: Option<Loader>,
) -> crate::Result<()> {
if !ctx.debug.loaded_bunfig {
Expand Down Expand Up @@ -1111,10 +1124,30 @@ Full documentation is available at <magenta>https://bun.com/docs/cli/run<r>
ctx,
vm,
entry_path: run_entry,
entry_kind,
}
.start()
}

/// Resolve `entry` to the file to run, like `bun <entry>` does. On `None`
/// the module loader reports the failure when it imports `entry`.
Comment thread
robobun marked this conversation as resolved.
Resolved
fn resolve_entry_path(vm: &mut VirtualMachine, entry: &'static [u8]) -> Option<&'static [u8]> {
let top_level_dir = vm.top_level_dir();
// Like the module loader, keep the resolver's messages out of `vm.log`.
let mut log = bun_ast::Log::default();
let resolver = &raw mut vm.transpiler.resolver;
// SAFETY: `resolver` is a field of the live VM. `log` is declared
// before the guard, so the guard restores the log before `log` drops.
let _restore_log = unsafe {
bun_resolver::Resolver::scoped_log(resolver, ::core::ptr::NonNull::from(&mut log))
};
// SAFETY: `vm` is borrowed for this call, so nothing else uses its resolver.
let resolved = unsafe { &mut *resolver }
.resolve(top_level_dir, entry, bun_ast::ImportKind::EntryPointRun)
.ok()?;
Some(resolved.path_const()?.text)
}

/// Entry point for
/// `bun build --compile` executables. Mirrors [`boot`] but routes through
/// `VirtualMachine::init_with_module_graph` and applies the standalone
Expand Down Expand Up @@ -1230,6 +1263,7 @@ Full documentation is available at <magenta>https://bun.com/docs/cli/run<r>
ctx,
vm,
entry_path: entry,
entry_kind: EntryPath::Resolved,
}
.start()
}
Expand All @@ -1250,6 +1284,7 @@ pub struct Run<'a> {
/// reloader stores them too (`boot` leaks the `Box<[u8]>`, cron mode uses
/// the runner arena).
entry_path: &'static [u8],
entry_kind: EntryPath,
}

// `on_unhandled_rejection_before_close` is a plain fn pointer stored on the
Expand Down Expand Up @@ -1299,6 +1334,7 @@ impl Run<'_> {
ctx,
vm,
entry_path: mut entry,
entry_kind,
} = self;
let _api_lock = vm.global().vm().get_api_lock();

Expand Down Expand Up @@ -1431,6 +1467,18 @@ impl Run<'_> {
}
}

// `vm.main()` must be the key the module loader gives the entry module.
// Resolve only now: the resolver has its final options and, under
// `--watch` and `--hot`, its watcher, as it has for every import.
Comment thread
robobun marked this conversation as resolved.
Resolved
if entry_kind == EntryPath::Unresolved
&& vm.module_loader.eval_source.is_none()
&& let Some(resolved) = RunCommand::resolve_entry_path(vm, entry)
&& resolved != entry
{
vm.set_main_for_argv(entry);
entry = resolved;
}
Comment thread
robobun marked this conversation as resolved.
Resolved

match vm.load_entry_point(entry) {
Ok(promise) => {
// SAFETY: `promise` is a live GC cell returned by the module loader.
Expand Down Expand Up @@ -1710,7 +1758,7 @@ impl RunCommand {
// owned copy by value.
let owned: Box<[u8]> = path.to_vec().into_boxed_slice();

if let Err(err) = Self::boot(ctx, owned, loader) {
if let Err(err) = Self::boot(ctx, owned, EntryPath::Resolved, loader) {
Self::boot_failed_exit(ctx, paths::basename(path), &err);
}
true
Expand Down Expand Up @@ -2869,7 +2917,7 @@ impl RunCommand {
// `basename(target_name)` (= "-"), not `basename(entry_path)`
// (= "[stdin]"), in the error message.
let owned: Box<[u8]> = entry_path.to_vec().into_boxed_slice();
if let Err(err) = Self::boot(ctx, owned, None) {
if let Err(err) = Self::boot(ctx, owned, EntryPath::Resolved, None) {
Self::boot_failed_exit(ctx, b"-", &err);
}
Ok(true)
Expand Down Expand Up @@ -2919,7 +2967,7 @@ impl RunCommand {
let entry: Box<[u8]> = entry_point_buf[..cwd_len + EVAL_TRIGGER.len()]
.to_vec()
.into_boxed_slice();
Self::boot(ctx, entry, None)
Self::boot(ctx, entry, EntryPath::Resolved, None)
}

/// `node` argv0 emulation. Port of `execAsIfNode`.
Expand Down Expand Up @@ -2954,7 +3002,7 @@ impl RunCommand {
let entry: Box<[u8]> = entry_point_buf[..cwd_len + EVAL_TRIGGER.len()]
.to_vec()
.into_boxed_slice();
return Self::boot(ctx, entry, None);
return Self::boot(ctx, entry, EntryPath::Resolved, None);
}

if ctx.positionals.is_empty() {
Expand Down Expand Up @@ -2997,7 +3045,7 @@ impl RunCommand {
// `Global::configure_allocator` and (b) uses the
// `Output.err(err, "Failed to run script \"...\"")` form.
let basename: Box<[u8]> = paths::basename(&normalized).to_vec().into_boxed_slice();
if let Err(err) = Self::boot(ctx, normalized, None) {
if let Err(err) = Self::boot(ctx, normalized, EntryPath::Unresolved, None) {
Self::exec_as_if_node_boot_failed(ctx, &basename, err);
}
Ok(())
Expand Down Expand Up @@ -4036,7 +4084,12 @@ impl BunXFastPath {
::core::slice::from_raw_parts_mut(raw.cast::<u8>(), bun_paths::PATH_MAX_WIDE * 2)
};
let utf8 = strings::convert_utf16_to_utf8_in_buffer(out_buf, wpath);
if let Err(err) = RunCommand::boot(ctx, utf8.to_vec().into_boxed_slice(), None) {
if let Err(err) = RunCommand::boot(
ctx,
utf8.to_vec().into_boxed_slice(),
EntryPath::Resolved,
None,
) {
// SAFETY: `ctx.log` was set in `create_context_data`.
let _ = unsafe { &mut *ctx.log }.print(std::ptr::from_mut(Output::error_writer()));
Output::err(
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/node/node_process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ mod _impl {
if worker.is_some_and(|w| w.eval_mode()) {
args_list.push(BunString::static_("[worker eval]"));
} else {
args_list.push(BunString::borrow_utf8(vm.main()));
args_list.push(BunString::borrow_utf8(vm.main_for_argv()));
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/runtime/shell/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1701,7 +1701,7 @@ impl Interpreter {
// SAFETY: `bun_vm()` on a JS event loop returns the live
// `*VirtualMachine` owning that loop.
let vm = unsafe { &*vm_ptr };
let main = vm.main();
let main = vm.main_for_argv();
if !main.is_empty() {
if int == 0 {
out.extend_from_slice(main);
Expand Down
Loading
Loading