Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions src/jsc/VirtualMachine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ pub struct VirtualMachine {
main: bun_ptr::RawSlice<u8>,
pub main_is_html_entrypoint: bool,
pub main_resolved_path: bun_core::String,
/// The one attempt to fill `main_resolved_path` ran. An empty path then means `main` cannot be opened.
pub main_resolved_path_tried: bool,
pub main_hash: u32,
/// Set if code overrides Bun.main to a custom value.
pub overridden_main: crate::strong::Optional,
Expand Down Expand Up @@ -3400,6 +3402,7 @@ impl VirtualMachine {
self.has_loaded = false;
self.set_main(entry_path);
self.main_resolved_path = bun_core::String::EMPTY;
self.main_resolved_path_tried = false;
self.main_hash = bun_watcher::Watcher::get_hash(entry_path);
self.overridden_main.deinit();

Expand Down Expand Up @@ -5515,6 +5518,7 @@ impl VirtualMachine {
self.has_loaded = false;
self.set_main(entry_path);
self.main_resolved_path = bun_core::String::EMPTY;
self.main_resolved_path_tried = false;
self.main_hash = bun_watcher::Watcher::get_hash(entry_path);
self.overridden_main.deinit();

Expand Down Expand Up @@ -5780,6 +5784,7 @@ impl VirtualMachine {
self.set_main(b"");
self.main_hash = 0;
self.main_resolved_path = bun_core::String::EMPTY;
self.main_resolved_path_tried = false;
self.unhandled_error_counter = 0;
// The finished file's plugins are dropped with its global; the next
// `Bun.plugin()` call reinstalls the runner against the new global.
Expand Down
9 changes: 9 additions & 0 deletions src/jsc/bindings/JSCommonJSModule.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ static bool canPerformFastEnumeration(Structure* s)
}

extern "C" bool Bun__VM__specifierIsEvalEntryPoint(void*, EncodedJSValue);
extern "C" bool Bun__VM__specifierIsEntryPoint(void*, EncodedJSValue);
extern "C" void Bun__VM__setEntryPointEvalResultCJS(void*, EncodedJSValue);
extern "C" void Bun__VM__noteCommonJSEvaluation(void*, EncodedJSValue);

Expand Down Expand Up @@ -237,6 +238,8 @@ static bool evaluateCommonJSModuleOnce(JSC::VM& vm, Zig::GlobalObject* globalObj
// exit 0 silently. Use the out-param overload and rethrow.
WTF::NakedPtr<JSC::Exception> returnedException;
JSValue result = JSC::evaluate(globalObject, code, jsUndefined(), returnedException);
if (moduleObject->isEntryPoint)
globalObject->armNextTickQueueCheckAfterEntryPoint();
Comment thread
robobun marked this conversation as resolved.
if (returnedException) [[unlikely]] {
scope.throwException(globalObject, returnedException.get());
return false;
Expand Down Expand Up @@ -309,6 +312,8 @@ static bool evaluateCommonJSModuleOnce(JSC::VM& vm, Zig::GlobalObject* globalObj
// fn(exports, require, module, __filename, __dirname) { /* code */ }(exports, require, module, __filename, __dirname)
//
JSC::profiledCall(globalObject, ProfilingReason::API, fn, callData, moduleObject, args);
if (moduleObject->isEntryPoint)
globalObject->armNextTickQueueCheckAfterEntryPoint();
RETURN_IF_EXCEPTION(scope, false);
return true;
}
Expand Down Expand Up @@ -1661,6 +1666,8 @@ std::optional<JSC::SourceCode> createCommonJSModule(
}

moduleObject->ignoreESModuleAnnotation = ignoreESModuleAnnotation;
// A graph has no entry point.
moduleObject->isEntryPoint = !graph && moduleObject->filename().isString() && Bun__VM__specifierIsEntryPoint(globalObject->bunVM(), JSValue::encode(moduleObject->filename()));
Comment thread
robobun marked this conversation as resolved.

return commonJSModuleSyntheticSourceCode(sourceOrigin, sourceURL, graph);
}
Expand Down Expand Up @@ -1776,6 +1783,8 @@ std::optional<JSC::SourceCode> createCommonJSModule(
}

moduleObject->ignoreESModuleAnnotation = ignoreESModuleAnnotation;
// A graph has no entry point.
moduleObject->isEntryPoint = !graph && moduleObject->filename().isString() && Bun__VM__specifierIsEntryPoint(globalObject->bunVM(), JSValue::encode(moduleObject->filename()));

return commonJSModuleSyntheticSourceCode(sourceOrigin, sourceURL, graph);
}
Expand Down
2 changes: 2 additions & 0 deletions src/jsc/bindings/JSCommonJSModule.h
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ class JSCommonJSModule final : public JSC::JSDestructibleObject {
JSC::WriteBarrier<JSModuleGraph> m_moduleGraph;

bool ignoreESModuleAnnotation { false };
// The module is the file the process (or the worker) was started with.
bool isEntryPoint { false };
JSC::SourceCode sourceCode = JSC::SourceCode();

static size_t estimatedSize(JSC::JSCell* cell, JSC::VM& vm);
Expand Down
6 changes: 6 additions & 0 deletions src/jsc/bindings/ZigGlobalObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,12 @@ void Zig::GlobalObject::resetOnEachMicrotaskTick()
}
}

// A module that loads before the entry point (a preload) spends the check armed at startup.
void Zig::GlobalObject::armNextTickQueueCheckAfterEntryPoint()
{
vm().setOnEachMicrotaskTick(&checkIfNextTickWasCalledDuringMicrotask);
}

extern "C" size_t Bun__reported_memory_size;

// executionContextId: -1 for main thread
Expand Down
1 change: 1 addition & 0 deletions src/jsc/bindings/ZigGlobalObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ class GlobalObject : public Bun::GlobalScope {
bool m_nodeWorkerEntrySettled { false };

void resetOnEachMicrotaskTick();
void armNextTickQueueCheckAfterEntryPoint();

enum class PromiseFunctions : uint8_t {
BunServe__Plugins__onResolve,
Expand Down
116 changes: 59 additions & 57 deletions src/runtime/api/BunObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,63 @@ fn enable_ansi_colors(_global_this: &JSGlobalObject, _: &JSObject) -> JSValue {
JSValue::from(Output::enable_ansi_colors_stdout() || Output::enable_ansi_colors_stderr())
}

/// `vm.main()` with its symlinks resolved, cached: `Bun.main`. `None` when it cannot be opened.
pub(crate) fn resolved_main_path(vm: &mut VirtualMachine) -> Option<&BunString> {
if vm.main_resolved_path.is_empty() {
if vm.main_resolved_path_tried {
return None;
}
vm.main_resolved_path_tried = true;

// If it's from eval, don't try to resolve it.
if strings::ends_with(vm.main(), b"[eval]") {
return None;
}
if strings::ends_with(vm.main(), b"[stdin]") {
return None;
}

let fd = sys::openat_a(
if cfg!(windows) {
Fd::INVALID
} else {
Fd::cwd()
},
vm.main(),
// Open with the minimum permissions necessary for resolving the file path.
if cfg!(any(target_os = "linux", target_os = "android")) {
sys::O::PATH
} else {
sys::O::RDONLY
},
0,
)
.ok()?;

let _close = scopeguard::guard(fd, |fd: Fd| fd.close());
#[cfg(windows)]
{
let mut wpath = bun_paths::w_path_buffer_pool::get();
let fdpath = bun_sys::get_fd_path_w(fd, &mut wpath).ok()?;
vm.main_resolved_path = BunString::clone_utf16(fdpath);
}
#[cfg(not(windows))]
{
let mut path = bun_paths::path_buffer_pool::get();
let fdpath = bun_sys::get_fd_path(fd, &mut path).ok()?;

// Bun.main === otherId will be compared many times, so let's try to create an atom string if we can.
if let Some(atom) = BunString::try_create_atom(fdpath) {
vm.main_resolved_path = atom;
} else {
vm.main_resolved_path = BunString::clone_utf8(fdpath);
}
}
}

Some(&vm.main_resolved_path)
}

// callconv(jsc.conv) — `SYSV_ABI` on win-x64 (BunObject.cpp:1103). Returns
// plain `JSValue` so the generated thunk is a bare deref+call (no
// `ExceptionValidationScope`).
Expand All @@ -788,63 +845,8 @@ pub(crate) fn get_main(global_this: &JSGlobalObject) -> JSValue {
// Attempt to use the resolved filesystem path
// This makes `eval('require.main === module')` work when the main module is a symlink.
// This behavior differs slightly from Node. Node sets the `id` to `.` when the main module is a symlink.
'use_resolved_path: {
if vm.main_resolved_path.is_empty() {
// If it's from eval, don't try to resolve it.
if strings::ends_with(vm.main(), b"[eval]") {
break 'use_resolved_path;
}
if strings::ends_with(vm.main(), b"[stdin]") {
break 'use_resolved_path;
}

let Ok(fd) = sys::openat_a(
if cfg!(windows) {
Fd::INVALID
} else {
Fd::cwd()
},
vm.main(),
// Open with the minimum permissions necessary for resolving the file path.
if cfg!(any(target_os = "linux", target_os = "android")) {
sys::O::PATH
} else {
sys::O::RDONLY
},
0,
) else {
break 'use_resolved_path;
};

let _close = scopeguard::guard(fd, |fd: Fd| fd.close());
#[cfg(windows)]
{
let mut wpath = bun_paths::w_path_buffer_pool::get();
let Ok(fdpath) = bun_sys::get_fd_path_w(fd, &mut wpath) else {
break 'use_resolved_path;
};
vm.main_resolved_path = BunString::clone_utf16(fdpath);
}
#[cfg(not(windows))]
{
let mut path = bun_paths::path_buffer_pool::get();
let Ok(fdpath) = bun_sys::get_fd_path(fd, &mut path) else {
break 'use_resolved_path;
};

// Bun.main === otherId will be compared many times, so let's try to create an atom string if we can.
if let Some(atom) = BunString::try_create_atom(fdpath) {
vm.main_resolved_path = atom;
} else {
vm.main_resolved_path = BunString::clone_utf8(fdpath);
}
}
}

return vm
.main_resolved_path
.to_js(global_this)
.or_pending_exception();
if let Some(resolved) = resolved_main_path(vm) {
return resolved.to_js(global_this).or_pending_exception();
}

EncodedSlice::from_bytes(vm.main()).to_js(global_this)
Expand Down
21 changes: 15 additions & 6 deletions src/runtime/hw_exports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,18 +157,27 @@ pub(crate) fn specifier_is_eval_entry_point(this: &mut VirtualMachine, specifier
/// origin `uncaughtException`. `main()` compare filters out an ESM entry that `import`s CJS.
// HOST_EXPORT(Bun__VM__noteCommonJSEvaluation, c)
pub(crate) fn note_commonjs_evaluation(this: &mut VirtualMachine, specifier: JSValue) {
if this.entry_point_result.evaluated_as_cjs || this.main().is_empty() {
return;
if !this.entry_point_result.evaluated_as_cjs && specifier_is_entry_point(this, specifier) {
this.entry_point_result.evaluated_as_cjs = true;
}
}

// HOST_EXPORT(Bun__VM__specifierIsEntryPoint, c)
pub(crate) fn specifier_is_entry_point(this: &mut VirtualMachine, specifier: JSValue) -> bool {
if this.main().is_empty() {
return false;
}
let global = this.global();
// A failed conversion just skips the note; must never panic at an FFI
// boundary.
// A failed conversion is "no"; must never panic at an FFI boundary.
let Ok(specifier_str) = bun_core::String::from_js(specifier, global) else {
return;
return false;
};
if specifier_str.eql_utf8(this.main()) {
this.entry_point_result.evaluated_as_cjs = true;
return true;
}
// Under the `node` shim `main()` is a symlink, and the module is keyed by its real path.
crate::api::bun_object::resolved_main_path(this)
.is_some_and(|resolved| specifier_str.eql(resolved))
}

/// `export fn Bun__closeChildIPC(global)` — defers the actual socket close to
Expand Down
Loading
Loading