From ff835a3281821510cdf7f4d0a90953a372dc16f4 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 15 May 2026 23:14:41 -0700 Subject: [PATCH 1/2] Split resolver_body into modules; type the extern-Rust pointers The Zig->Rust port wrapped the entire resolver implementation (Resolver, Result, MatchResult, options, allocators, ...) in a single 7,664-line pub mod __phase_a_body { ... } inside src/resolver/lib.rs. There is no name for a module that wraps a crate's whole body that isn't redundant, which is the tell that the wrapper shouldn't exist. Split it into sibling files following the crate's existing convention (data_url.rs / dir_info.rs / package_json.rs): - options.rs (357 lines): BundleOptions, Packages, ExternalModules, Framework, ExtOrder, ... - result.rs (578 lines): Result, MatchResult, PathPair, DebugLogs, PendingResolution, LoadResult, ... - resolver.rs (6,640 lines): Resolver struct + impl, threadlocal Bufs scratch, the local shim modules - allocators.rs (6 lines): re-exports referenced cross-file - standalone_module_graph.rs (30 lines): the StandaloneModuleGraph trait lib.rs shrinks 10,273 -> 2,615 lines. Public API surface is byte-identical (bun_resolver::Resolver, ::Result, ::options, ... all unchanged). Also un-erase the extern "Rust" link-time pointers where the declaring crate already names the type. The port applied 'type-erase across the crate boundary' uniformly to every #[no_mangle] upward call, but extern "Rust" carries full Rust types and most declaring crates already depend on the crate that defines the parameter types. Where visible, use the typed pointer with the Zig pointer shape (NonNull for *T, Option> for ?*T): - __bun_resolver_init_package_manager: log *mut Log -> NonNull, install *const () -> Option>, env *mut c_void -> NonNull> - BundleOptions.install: *const () -> Option> - Resolver.log: *mut Log -> NonNull - __bun_jsc_enable_hot_module_reloading_for_bundler: *mut () -> NonNull> The implementation-side cast::() calls -- the tell that the erasure was unnecessary -- are removed. Sites where the type is genuinely not visible (bun_event_loop -> bun_jsc::VirtualMachine, bun_js_parser -> bun_bundler::Transpiler) are left as-is: those are the real layering boundary the pattern is for. --- src/bundler/bundle_v2.rs | 60 +- src/bundler/options.rs | 11 +- src/bundler/transpiler.rs | 121 +- src/install/PackageManager.rs | 10 +- .../PackageManager/PackageManagerOptions.rs | 12 +- src/install/auto_installer.rs | 37 +- src/jsc/AsyncModule.rs | 26 +- src/jsc/VirtualMachine.rs | 17 +- src/jsc/hot_reloader.rs | 15 +- src/resolver/allocators.rs | 6 + src/resolver/dir_info.rs | 12 +- src/resolver/lib.rs | 7709 +---------------- src/resolver/options.rs | 357 + src/resolver/resolver.rs | 6640 ++++++++++++++ src/resolver/result.rs | 578 ++ src/resolver/standalone_module_graph.rs | 30 + src/router/lib.rs | 30 +- src/runtime/api/filesystem_router.rs | 11 +- src/runtime/bake/production.rs | 5 +- src/runtime/cli/repl_command.rs | 9 +- src/runtime/cli/run_command.rs | 60 +- src/runtime/jsc_hooks.rs | 63 +- 22 files changed, 7880 insertions(+), 7939 deletions(-) create mode 100644 src/resolver/allocators.rs create mode 100644 src/resolver/options.rs create mode 100644 src/resolver/resolver.rs create mode 100644 src/resolver/result.rs create mode 100644 src/resolver/standalone_module_graph.rs diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 3b1b59b4e9c..36d62c31e62 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -1,9 +1,7 @@ // ══════════════════════════════════════════════════════════════════════════ -// B-2 un-gated header — real `BundleV2` struct definition. -// resolver↔bundler cycle broken in O; `bun_resolver` is now a direct dep, so -// `Transpiler` (which embeds `Resolver`) is referenceable here. Method bodies -// remain in the gated `bv2_impl` module below until `LinkerContext`, -// `ParseTask`, `ThreadPool`, and the JSBundler/api TYPE_ONLY split land. +// `BundleV2` struct definition. `bun_resolver` is a direct dep, so +// `Transpiler` (which embeds `Resolver`) is referenceable here. Most method +// bodies live in the `bv2_impl` module below. // ══════════════════════════════════════════════════════════════════════════ use crate::mal_prelude::*; @@ -52,7 +50,7 @@ use crate::ungate_support::{EventLoop, UseDirective}; use crate::{Index, IndexInt, LinkerContext}; use bun_ast::SideEffects; -// ── re-exports for the B-1 inline `pub mod bundle_v2 { … }` shim surface ── +// ── re-exports so callers can reference these via `bundle_v2::…` ── /// `BundleThread` (BundleThread.zig) — owns the worker pool + completion /// queue for `BundleV2`. Re-exported so callers reference `bundle_v2::BundleThread`. pub use crate::BundleThread::BundleThread; @@ -103,8 +101,8 @@ pub struct BundleV2<'a> { /// When Bun Bake is used, the resolved framework is passed here. pub framework: Option, pub graph: Graph, - // Real `LinkerContext<'a>` (un-gated B-2). Borrows the same arena lifetime - // as `transpiler` (Zig stored both as raw pointers into the bundler heap). + // `LinkerContext<'a>` borrows the same arena lifetime as `transpiler` + // (Zig stored both as raw pointers into the bundler heap). pub linker: LinkerContext<'a>, // The hot reloader (`jsc::hot_reloader::NewHotReloader`) owns the // boxed `Watcher`; bundler only ever calls `Watcher::add_file` on it. @@ -153,16 +151,6 @@ pub struct BundleV2<'a> { pub requested_exports: ArrayHashMap, } -// ────────────────────────────────────────────────────────────────────────── -// B-2 un-gated impl: lifecycle entry points (`init` skeleton, scan-counter -// machinery, `on_parse_task_complete`, `deinit_without_freeing_arena`). Method -// bodies are real where lower-tier surfaces exist; sub-regions that touch -// still-gated modules (`ThreadPool`, full `dispatch::DevServerVTable`, -// `ServerComponentParseTask`, `Watcher`) are ``-gated inline so -// the call shape is preserved verbatim and un-gates by deletion once those -// land. See `bv2_impl` below for the full reference bodies. -// ────────────────────────────────────────────────────────────────────────── - bun_core::declare_scope!(Bundle, visible); bun_core::declare_scope!(scan_counter, visible); @@ -300,10 +288,7 @@ impl<'a> BundleV2<'a> { // removed — canonical bodies live in the later impl blocks below. } // ══════════════════════════════════════════════════════════════════════════ -// Phase-A draft body — gated until lower-tier crate surfaces solidify. -// (`bun_fs`/`bun_str`/`bun_node_fallbacks` crate aliases, full `dispatch` -// vtable slot set, `api::JSBundler` TYPE_ONLY split, `LinkerContext`, -// `ParseTask`, `ThreadPool`, OUT_DIR codegen for HmrRuntime embeds.) +// `BundleV2` method bodies + supporting types. // ══════════════════════════════════════════════════════════════════════════ pub mod bv2_impl { @@ -610,7 +595,7 @@ pub mod bv2_impl { /// Mirrors src/bake/bake.zig:936 `server_virtual_source` / :942 `client_virtual_source`. /// `bun_ast::Source` is not `const`-constructible (owns a `fs::Path`), so these - /// are lazy statics. PERF(port): was `pub const` — verify in Phase B. + /// are lazy statics. PERF(port): was `pub const` in Zig. pub static SERVER_VIRTUAL_SOURCE: std::sync::LazyLock = std::sync::LazyLock::new(|| { let mut s = bun_ast::Source::default(); @@ -1489,18 +1474,23 @@ pub mod bv2_impl { /// `BundleV2` (Zig: `Watcher.enableHotModuleReloading(this, null)` in /// `BundleV2.init` — bundle_v2.zig:994). The bundler can't name the /// reloader generic (T6), so this is a definer-prefixed extern hook. - fn __bun_jsc_enable_hot_module_reloading_for_bundler(bv2: *mut ()); + /// `'static` matches the impl side: the only caller (`bun build + /// --watch`) leaks the CLI arena, so the pointee is process-lifetime. + fn __bun_jsc_enable_hot_module_reloading_for_bundler( + bv2: core::ptr::NonNull>, + ); } /// `Watcher.enableHotModuleReloading(this, null)` for `bun build --watch`. #[inline] pub fn enable_hot_module_reloading_for_bundler(bv2: *mut super::BundleV2<'_>) { // SAFETY: link-time-resolved Rust-ABI fn in `bun_jsc::hot_reloader`. - // Not `safe fn`: the callee re-types the erased `*mut ()` as - // `*mut BundleV2<'static>` and dereferences it, so `bv2` must point to - // a live `BundleV2` whose backing allocation outlives the watcher - // (sole caller is `BundleV2::init` with the leaked CLI arena). - unsafe { __bun_jsc_enable_hot_module_reloading_for_bundler(bv2.cast()) } + // Not `safe fn`: the callee dereferences `bv2`, so it must point to a + // live `BundleV2` whose backing allocation outlives the watcher (sole + // caller is `BundleV2::init` with the leaked CLI arena — `'static`). + let bv2 = core::ptr::NonNull::new(bv2.cast::>()) + .expect("BundleV2 watcher: bv2 is non-null"); + unsafe { __bun_jsc_enable_hot_module_reloading_for_bundler(bv2) } } /// Bytecode generation entry point for the linker. Mirrors the Zig @@ -1588,7 +1578,7 @@ pub mod bv2_impl { /// case the returned reference is valid only for the bundle pass and the /// consuming `Path` must not outlive it. /// All call sites in this file satisfy one of these; this is the documented - /// Phase-A ARENA convention (PORTING.md §Type Mapping: arena-owned struct + /// arena-erasure convention (PORTING.md §Type Mapping: arena-owned struct /// fields use erased lifetimes). #[inline(always)] pub(crate) unsafe fn interned_slice(s: &[u8]) -> &'static [u8] { @@ -2897,7 +2887,7 @@ pub mod bv2_impl { this.transpiler.options.ignore_dce_annotations; // SAFETY: `transpiler.options.{banner,footer,public_path,metafile_*}` are // owned by the `'a`-lifetime `Transpiler` which outlives `this.linker`; - // `LinkerOptions` stores `&'static [u8]` as a Phase-A lifetime erasure + // `LinkerOptions` stores `&'static [u8]` as an arena-erased lifetime // (see `interned_slice` contract — these are bundle-pass-interned). this.linker.options.banner = unsafe { interned_slice(&this.transpiler.options.banner) }; this.linker.options.footer = unsafe { interned_slice(&this.transpiler.options.footer) }; @@ -5413,7 +5403,7 @@ pub mod bv2_impl { // SAFETY: `alloc_slice_copy` returns into the bundler arena which outlives // this function. Erase the `&self` lifetime via `*const` so the borrow on // `self.arena()` does not extend across the `&mut self` calls below - // (Phase-A arena-erasure convention; see also `path.pretty` ~L4770). + // (arena-erasure convention; see also `path.pretty` ~L4770). break 'reachable_files unsafe { &*std::ptr::from_ref::<[Index]>(self.arena().alloc_slice_copy(&js_files)) }; @@ -5970,7 +5960,7 @@ pub mod bv2_impl { if let Some(fw) = &self.framework { if fw.server_components.is_some() { - // PERF(port): was comptime bool dispatch — profile in Phase B + // PERF(port): was comptime bool dispatch — profile if hot. let is_server = ctx.target.is_server_side(); let src = if is_server { &bake::SERVER_VIRTUAL_SOURCE @@ -6848,7 +6838,7 @@ pub mod bv2_impl { js_parser_options.bundle = true; // SAFETY: `alloc_str` returns a `&mut str` into the bundler arena, which - // outlives this AST. `E::EString.data` is `&'static [u8]` per the Phase-A + // outlives this AST. `E::EString.data` is `&'static [u8]` per the // arena-erasure convention. See `interned_slice` contract. let unique_key: &'static [u8] = unsafe { interned_slice( @@ -7598,7 +7588,7 @@ pub mod bv2_impl { } impl ExternalFreeFunctionAllocator { - // TODO(port): std.mem.Allocator vtable equivalent — Phase B will define bun_alloc::Allocator trait impl + // TODO(refactor): could implement `bun_alloc::Allocator` instead of the manual vtable. pub fn create( free_callback: unsafe extern "C" fn(*mut c_void), diff --git a/src/bundler/options.rs b/src/bundler/options.rs index 2ce4d504614..c026c4dd991 100644 --- a/src/bundler/options.rs +++ b/src/bundler/options.rs @@ -414,7 +414,7 @@ pub trait LoaderExt: Copy { fn stdin_name_map() -> LoaderEnumMap { let mut map: LoaderEnumMap = EnumMap::from_array([b"" as &[u8]; 21]); - // TODO(port): EnumMap::from_array length must match variant count; verify in Phase B + // TODO(port): EnumMap::from_array length must match variant count. map[Loader::Jsx] = b"input.jsx"; map[Loader::Js] = b"input.js"; map[Loader::Ts] = b"input.ts"; @@ -1401,7 +1401,7 @@ pub struct BundleOptions<'a> { /// unrelated to `'a`; a typed reference forced an `unsafe { &*(p as *const _) }` /// lifetime-extension cast at every call site (PORTING.md §Forbidden). /// The sole consumer (`PackageManager::init_with_runtime` via the resolver's - /// erased `*const ()`) only reads through it. + /// `BundleOptions.install`) only reads through it. pub install: Option>, pub inlining: bool, @@ -1506,8 +1506,8 @@ impl<'a> BundleOptions<'a> { /// /// PERF(port): Zig's `transpiler.* = from.*` is a shallow struct copy /// (slices alias the parent's arena). The Rust port owns these as `Box`, - /// so a per-worker clone allocates. Profile in Phase B; the hot fields - /// (`define`, `loaders`, `conditions`) are O(dozens) entries. + /// so a per-worker clone allocates. Profile if it shows up on a hot path; + /// the hot fields (`define`, `loaders`, `conditions`) are O(dozens) entries. pub fn for_worker(&self) -> BundleOptions<'a> { debug_assert!( self.defines_loaded, @@ -1790,7 +1790,7 @@ impl<'a> BundleOptions<'a> { ); // TODO(port): many fields below have Zig defaults via `= ...`; in Rust we initialize - // each explicitly. Phase B: add a `Default`-ish builder. + // each explicitly. Could add a `Default`-ish builder. let mut opts = BundleOptions { footer: Cow::Borrowed(b""), banner: Cow::Borrowed(b""), @@ -1977,7 +1977,6 @@ impl<'a> BundleOptions<'a> { opts.env.behavior = api::DotEnvBehavior::LoadAll; if transform.extension_order.is_empty() { // we must also support require'ing .node files - // TODO(port): comptime concat — Phase B: precompute as static slices static EXT_WITH_NODE: &[&[u8]] = &[ b".tsx", b".ts", b".jsx", b".cts", b".cjs", b".js", b".mjs", b".mts", b".json", b".node", diff --git a/src/bundler/transpiler.rs b/src/bundler/transpiler.rs index 6f466e26b39..2b979bfe9fd 100644 --- a/src/bundler/transpiler.rs +++ b/src/bundler/transpiler.rs @@ -129,7 +129,7 @@ pub struct Transpiler<'a> { pub log: *mut bun_ast::Log, // TODO(port): arena — bundler is an AST crate per PORTING.md so we // thread an arena, but callers usually pass `bun.default_allocator`. - // Phase B: confirm whether this should be removed (global mimalloc) or kept. + // Confirm whether this should be removed (global mimalloc) or kept. pub arena: &'a Arena, pub result: options::TransformResult, pub resolver: Resolver<'a>, @@ -144,9 +144,8 @@ pub struct Transpiler<'a> { pub router: Option>, pub source_map: options::SourceMapOption, - // B-2 un-gated: real `crate::linker::Linker` so - // `ModuleLoader::transpile_source_code` (jsc_hooks.rs) can call - // `transpiler.linker.link()` / read `import_counter`. Back-pointers wired + // `ModuleLoader::transpile_source_code` (jsc_hooks.rs) calls + // `transpiler.linker.link()` / reads `import_counter`. Back-pointers wired // by `configure_linker` below; `set_log` keeps `linker.log` in sync. pub linker: crate::linker::Linker, pub timer: SystemTimer, @@ -169,9 +168,8 @@ impl<'a> Transpiler<'a> { self.linker.log = log; // SAFETY: caller (`ThreadPool::Worker::create`) passes the per-worker // arena-allocated `Log`, which outlives this `Transpiler<'a>`. Zig - // aliased the same `*Log` into `resolver.log`; `Resolver.log` is a - // `*mut` so the raw pointer copies straight across. - self.resolver.log = log; + // aliased the same `*Log` into `resolver.log`. + self.resolver.log = core::ptr::NonNull::new(log).expect("set_log: log is non-null"); } /// Port of `transpiler.zig:102 setAllocator`. @@ -310,10 +308,11 @@ impl<'a> Transpiler<'a> { ) }; let resolver_opts = resolver_bundle_options_subset(&options); + let log_nn = core::ptr::NonNull::new(log).expect("Transpiler::for_worker: log is non-null"); // SAFETY: see fn doc — `Resolver::for_worker` widens // `standalone_module_graph` / `env_loader` lifetimes. let resolver: Resolver<'a> = - unsafe { Resolver::for_worker(&from.resolver, log, resolver_opts) }; + unsafe { Resolver::for_worker(&from.resolver, log_nn, resolver_opts) }; Transpiler { options, @@ -363,7 +362,7 @@ impl<'a> Transpiler<'a> { // `linker.log` here so all four aliases agree. let log = self.log; self.options.log = log; - self.resolver.log = log; + self.resolver.log = core::ptr::NonNull::new(log).expect("wire_after_move: log is non-null"); self.resolver.fs = self.fs; // Spec ThreadPool.zig:310 `transpiler.linker.resolver = &transpiler.resolver`. // Only reseat the back-pointers — do NOT `Linker::init` here: that @@ -630,7 +629,7 @@ impl<'a> Transpiler<'a> { } // ══════════════════════════════════════════════════════════════════════════ -// B-2 un-gated: `configure_linker*` / `run_env_loader` — unblocks +// `configure_linker*` / `run_env_loader` — used by // `RunCommand::configure_env_for_run` (runtime/cli/run_command.rs:527), // `bun_install::configure_env_for_run`, `JSBundleCompletionTask`, // `JSTranspiler`, and `bun.js.rs:: bun_main_shell_entry`. @@ -791,14 +790,12 @@ impl<'a> Transpiler<'a> { } // ══════════════════════════════════════════════════════════════════════════ -// B-2 un-gated: `ParseResult` / `AlreadyBundled` / `ParseOptions` + -// `Transpiler::parse*` — real types so `ModuleLoader::transpile_source_code` -// (jsc_hooks.rs) and `AsyncModule` / `JSTranspiler` can name them. The body -// of `parse_maybe_return_file_only_allow_shared_buffer` does the source-load -// step (virtual / client-entry / `node:` fallback) for real and gates the -// per-loader transpile branches behind `` until the lower-tier -// surfaces (`cache::Fs::read_file*`, `js_parser::Options::init`, -// `cache::JavaScript::parse`) un-gate. +// `ParseResult` / `AlreadyBundled` / `ParseOptions` + `Transpiler::parse*` +// — used by `ModuleLoader::transpile_source_code` (jsc_hooks.rs) and +// `AsyncModule` / `JSTranspiler`. The body of +// `parse_maybe_return_file_only_allow_shared_buffer` does the source-load +// step (virtual / client-entry / `node:` fallback) and dispatches to the +// per-loader transpile branches. // ══════════════════════════════════════════════════════════════════════════ use crate::cache::RuntimeTranspilerCacheExt as _; @@ -876,7 +873,7 @@ pub struct ParseResult { /// Owns the bytes that `source.contents` points into when they came from /// `cache::Fs::read_file_with_allocator` (non-shared-buffer path) or a /// decoded `data:` URL. `bun_ast::Source.contents` is `&'static [u8]` - /// (Phase-A `Str` convention) so the backing must live at least as long as + /// (the AST crate's `Str` convention) so the backing must live at least as long as /// the `ParseResult`; threading it here means it drops when the result is /// recycled instead of leaking via `mem::forget` (PORTING.md §Forbidden). /// `Contents::Empty`/`SharedBuffer` for the virtual-source / shared-buffer @@ -982,13 +979,12 @@ pub struct ParseOptions<'a> { use bun_options_types::schema::api; -// ── B-3 type unification (parse_maybe Js/Ts arm) ───────────────────────── -// `ModuleType`, `Define`, `RuntimeTranspilerCache` are now single nominal -// types shared between `bun_js_parser` and this crate (canonical defs live in -// the lower-tier crate; bundler re-exports). The by-value conversion shims -// for those are gone — `to_parser_module_type` is an identity fn and -// `parse_maybe` threads `self.options.define` / `runtime_transpiler_cache` -// directly. +// ── type unification (parse_maybe Js/Ts arm) ───────────────────────────── +// `ModuleType`, `Define`, `RuntimeTranspilerCache` are single nominal types +// shared between `bun_js_parser` and this crate (canonical defs live in the +// lower-tier crate; bundler re-exports). There are no by-value conversion +// shims — `to_parser_module_type` is an identity fn and `parse_maybe` +// threads `self.options.define` / `runtime_transpiler_cache` directly. // // D042 UNIFIED: `crate::options_impl::jsx::Pragma` IS // `js_ast::parser::options::JSX::Pragma` (both re-export @@ -1007,7 +1003,7 @@ pub fn to_parser_jsx_pragma( p } -// B-3 UNIFIED: `crate::options_impl::ModuleType` IS `js_ast::parser::options::ModuleType` +// `crate::options_impl::ModuleType` IS `js_ast::parser::options::ModuleType` // (both re-export `bun_options_types::bundle_enums::ModuleType`). Identity shim // kept so existing call sites compile unchanged; inlines to a move. #[inline(always)] @@ -1124,14 +1120,9 @@ pub(crate) fn resolver_bundle_options_subset( } }), global_cache: src.global_cache, - // Spec `options.zig:1753`: `?*const Api.BunInstall` → resolver's - // FORWARD_DECL `*const ()`. Bundler now stores `Option>` - // (PORTING.md §Forbidden: no `&*(p as *const _)` lifetime-extension at - // call sites), so this is a plain pointer-to-pointer cast. - install: src - .install - .map(|p| p.as_ptr().cast::<()>().cast_const()) - .unwrap_or(core::ptr::null()), + // Spec `options.zig:1753`: `?*const Api.BunInstall` — both sides store + // `Option>`, so this is a straight copy. + install: src.install, load_package_json: src.load_package_json, load_tsconfig_json: src.load_tsconfig_json, main_field_extension_order: ropts::owned_string_list(src.main_field_extension_order), @@ -1168,12 +1159,10 @@ pub(crate) fn resolver_bundle_options_subset( impl<'a> Transpiler<'a> { /// Port of `transpiler.zig:Transpiler.init`. /// - /// Un-gated B-2 so [`init_runtime_state`](../runtime/jsc_hooks.rs) - /// (spec `VirtualMachine.zig:1241`) can write `vm.transpiler`. Both - /// lower-tier constructors are now live: + /// Called by [`init_runtime_state`](../runtime/jsc_hooks.rs) (spec + /// `VirtualMachine.zig:1241`) to write `vm.transpiler`. Builds on: /// * [`options::BundleOptions::from_api`] — `bun_bundler::options` - /// * [`Resolver::init1`] — `bun_resolver` (its `mod options` is now - /// `pub` so this crate can build the FORWARD_DECL subset) + /// * [`Resolver::init1`] — `bun_resolver` /// /// PORT NOTE: `log` / `env_loader_` are raw pointers (not `&'a mut`) to /// match the un-gated struct field types — Zig aliased the same `*Log` @@ -1330,7 +1319,11 @@ impl<'a> Transpiler<'a> { outbase, ..Default::default() }); - core::ptr::addr_of_mut!((*p).resolver).write(Resolver::init1(log, fs, resolver_opts)); + core::ptr::addr_of_mut!((*p).resolver).write(Resolver::init1( + core::ptr::NonNull::new(log).expect("Transpiler::init_in_place: log is non-null"), + fs, + resolver_opts, + )); core::ptr::addr_of_mut!((*p).fs).write(fs); core::ptr::addr_of_mut!((*p).output_files).write(Vec::new()); core::ptr::addr_of_mut!((*p).resolve_results).write(resolve_results); @@ -1467,8 +1460,8 @@ impl<'a> Transpiler<'a> { // SAFETY: `source_backing` is moved into the returned // `ParseResult` (or drops on `return None`); the re-borrow is // sound for the lifetime of `source.contents`' consumers, which - // never outlive the `ParseResult`. Phase B threads a real - // lifetime once `bun_ast::Source.contents` becomes `Cow`. + // never outlive the `ParseResult`. A real lifetime can be + // threaded once `bun_ast::Source.contents` becomes `Cow`. let contents: &'static [u8] = unsafe { bun_ptr::detach_lifetime_ref::<[u8]>(source_backing.as_slice()) }; break 'brk bun_ast::Source::init_path_string(path.text, contents); @@ -1506,7 +1499,7 @@ impl<'a> Transpiler<'a> { if let Some(file_fd_ptr) = this_parse.file_fd_ptr { *file_fd_ptr = entry.fd; } - // PORT NOTE: `Source.contents: &'static [u8]` (Phase-A `Str` + // PORT NOTE: `Source.contents: &'static [u8]` (the AST crate's `Str` // convention). The bytes live either in the per-thread shared // buffer (`USE_SHARED_BUFFER` → `Contents::SharedBuffer`, no-op // drop) or in `this_parse.arena` (`Contents::Arena`, no-op drop — @@ -1522,7 +1515,8 @@ impl<'a> Transpiler<'a> { // `source.contents` (it is moved into the returned `ParseResult`, // and the only consumers are the parser/printer which run before // the result drops). `contents_is_recycled = true` records that - // the bytes are externally-owned; Phase B threads `'bump`. + // the bytes are externally-owned; threading `'bump` would remove + // the erasure. let contents: &'static [u8] = unsafe { bun_ptr::detach_lifetime_ref::<[u8]>(source_backing.as_slice()) }; match bun_ast::Source::init_recycled_file(bun_ast::PathContentsPair { @@ -1628,9 +1622,9 @@ impl<'a> Transpiler<'a> { .trim_unused_imports .unwrap_or(loader.is_typescript()); opts.features.no_macros = self.options.no_macros; - // B-3 UNIFIED: `bun_ast::RuntimeTranspilerCache` IS - // `bun_ast::RuntimeTranspilerCache`; thread the pointer - // directly. Spec transpiler.zig:899/957 copies the same + // `bun_ast::RuntimeTranspilerCache` is the single nominal + // type on both sides; thread the pointer directly. + // Spec transpiler.zig:899/957 copies the same // `?*RuntimeTranspilerCache` raw pointer to BOTH // `opts.features` and the returned `ParseResult`. Derive both // from a single reborrow so they share one provenance tag — @@ -1661,8 +1655,9 @@ impl<'a> Transpiler<'a> { // Spec transpiler.zig:925 forwards `transpiler.options // .bundler_feature_flags`. Zig aliased a `*const StringSet`; // `Features.bundler_feature_flags` is currently owned - // (`Option>`), so clone by value until B-3 - // changes the parser-side field to `Option<&'a StringSet>`. + // (`Option>`), so clone by value. + // TODO(refactor): change the parser-side field to + // `Option<&'a StringSet>` to avoid the clone. // The clone drops with `opts` — no leak. opts.features.bundler_feature_flags = self .options @@ -1677,7 +1672,7 @@ impl<'a> Transpiler<'a> { opts.features.is_macro_runtime = target == crate::options_impl::Target::BunMacro; // Spec transpiler.zig:943: `opts.features.replace_exports = - // this_parse.replace_exports`. B-3 UNIFIED — + // this_parse.replace_exports`. // `bun_ast::runtime::ReplaceableExport` IS // `js_ast::Runtime::ReplaceableExport`, so the inner // `StringArrayHashMap` moves directly into the newtype. @@ -1701,7 +1696,7 @@ impl<'a> Transpiler<'a> { } opts.macro_context = self.macro_context.as_mut(); - // B-3 UNIFIED: `crate::defines::Define` IS + // `crate::defines::Define` IS // `bun_js_parser::defines::Define`. Hand the parser the real // table so user `--define` values apply at parse time. // SAFETY: `self.options.define` is `Box` owned by the @@ -1888,8 +1883,8 @@ fn parse_data_loader( keep_json_and_toml_as_one_statement: bool, ) -> Option { // PERF(port): was `inline .toml, .yaml, .json, .jsonc, .json5 - // => |kind|` — comptime monomorphization per loader; profile in - // Phase B. + // => |kind|` — comptime monomorphization per loader; profile if it + // shows up on a hot path. // // PORT NOTE: `bun_parsers::*` parse into the T2 value AST // (`bun_ast::Expr`); lift into the full T4 @@ -2184,7 +2179,7 @@ fn parse_md_loader( // arena. Arena-copy the heap `Box<[u8]>` and let it drop; // PORTING.md §Forbidden patterns bars `Box::leak` here. // SAFETY: ARENA — `arena` outlives the returned - // `ParseResult.ast` (Phase-A `Str` convention erases + // `ParseResult.ast` (the AST crate's `Str` convention erases // `'bump` to `'static` for `E::String.data`). Ok(h) => unsafe { bun_ptr::detach_lifetime(arena.alloc_slice_copy(&h)) }, Err(_) => { @@ -2278,18 +2273,16 @@ fn parse_unsupported_loader(loader: options::Loader, path: &bun_paths::fs::Path< } // ══════════════════════════════════════════════════════════════════════════ -// B-2 un-gated: `Transpiler::print` / `print_with_source_map` — final step of -// `ModuleLoader::transpile_source_code` (jsc_hooks.rs spec :525-539). The -// `bun_js_printer` entry points (`print_ast` / `print_common_js` / `Options` / -// `SourceMapHandler` / `Format` / `WriterTrait`) are now real types; un-gate -// the dispatch shim so `RuntimeTranspilerStore` / `AsyncModule` link. +// `Transpiler::print` / `print_with_source_map` — final step of +// `ModuleLoader::transpile_source_code` (jsc_hooks.rs spec :525-539); the +// dispatch shim that `RuntimeTranspilerStore` / `AsyncModule` link against. // // PORT NOTE: `comptime format: js_printer.Format` demoted to a runtime arg — // `bun_js_printer::Format` doesn't derive `ConstParamTy` (and can't be added -// from this crate). All un-gated callers pass a literal anyway; the inner +// from this crate). All callers pass a literal anyway; the inner // `print_ast::<_, ASCII_ONLY, ENABLE_SOURCE_MAP>` keeps both real comptime // bools, so codegen monomorphizes the printer body identically. -// PERF(port): outer `match format` is one extra branch — profile in Phase B. +// PERF(port): outer `match format` is one extra branch — profile if hot. // ══════════════════════════════════════════════════════════════════════════ use bun_js_printer as js_printer; @@ -2303,7 +2296,7 @@ use js_printer::analyze_transpiled_module; /// Map the bundler-local `Target` (options.rs:489) to the lower-tier /// `bun_ast::Target` consumed by `js_printer::Options`. /// The two enums are variant-for-variant identical but nominally distinct; -/// Phase B-3 collapses them (see lib.rs `pub mod options` shadow note). +/// TODO(refactor): collapse them (see lib.rs `pub mod options` shadow note). #[inline] fn to_bundle_enums_target(t: crate::options_impl::Target) -> bun_ast::Target { use bun_ast::Target as T; @@ -2356,7 +2349,7 @@ impl<'a> Transpiler<'a> { // take the column out (the printer never reads `tree.symbols`; it // walks `symbols` exclusively — `rg tree.symbols js_printer/lib.rs` is // empty). `init_with_one_list` boxes the single inner list. - // PERF(port): one extra alloc vs Zig's borrowed-slice — profile Phase B. + // PERF(port): one extra alloc vs Zig's borrowed-slice — profile if hot. let symbols = bun_ast::symbol::Map::init_with_one_list(core::mem::take(&mut ast.symbols)); // `runtime_imports` is now forwarded — after Round-G `Ast.runtime_imports` diff --git a/src/install/PackageManager.rs b/src/install/PackageManager.rs index c3259c3e5bf..5e8516ce104 100644 --- a/src/install/PackageManager.rs +++ b/src/install/PackageManager.rs @@ -363,7 +363,7 @@ pub struct PackageManager { pub ast_arena: bun_alloc::Arena, // TODO(port): lifetime — LIFETIMES.tsv classifies this BORROW_PARAM → `&'a mut bun_ast::Log` // (struct gets `<'a>`). Kept as raw ptr because PackageManager is a leaked singleton stored - // in a `static`; threading `<'a>` through the global holder is deferred to Phase B. + // in a `static`; threading `<'a>` through the global holder is a TODO(refactor). pub log: *mut bun_ast::Log, pub resolve_tasks: ResolveTaskQueue, pub timestamp_for_manifest_cache_control: u32, @@ -813,7 +813,7 @@ mod holder { // Zig uses `var ptr: *PackageManager = undefined` then assigns via allocatePackageManager() // and later writes `manager.* = ...` in-place. OnceLock> can't express // allocate-then-fill (no `&mut` after set). Keep a raw ptr for now. - // TODO(port): in-place init — reconcile with OnceLock> in Phase B. + // TODO(port): in-place init — reconcile with OnceLock>. // PORTING.md §Global mutable state: ptr written once on main thread, read // from worker threads → AtomicPtr (Release/Acquire pairs the publish). pub static RAW_PTR: core::sync::atomic::AtomicPtr = @@ -2335,9 +2335,9 @@ pub fn init_with_runtime( log: &mut bun_ast::Log, // Spec PackageManager.zig:983 `bun_install: ?*Api.BunInstall` — used read-only // (PackageManagerOptions.zig:load lines 224-380 only ever reads `config.*`). - // Upstream storage is `Option<&api::BunInstall>` (options.rs) / `*const ()` - // (resolver opts); taking `&mut` here would force a const→mut provenance - // launder at the resolver call site. + // Upstream storage is `Option>` (bundler + resolver + // opts); taking `&mut` here would force a const→mut provenance launder at + // the resolver call site. bun_install: Option<&Api::BunInstall>, cli: CommandLineArguments, env: &mut dot_env::Loader<'static>, diff --git a/src/install/PackageManager/PackageManagerOptions.rs b/src/install/PackageManager/PackageManagerOptions.rs index f7aba580c8f..476e786b2ec 100644 --- a/src/install/PackageManager/PackageManagerOptions.rs +++ b/src/install/PackageManager/PackageManagerOptions.rs @@ -12,8 +12,8 @@ use bun_install::{Features, Npm}; // PORT NOTE: `string` fields are `[]const u8` borrowed from CLI args / bunfig config, // which live for the process lifetime. There is no `deinit` on Options. Mapped to -// `&'static [u8]` per PORTING.md (no lifetime params on structs in Phase A). -// TODO(port): lifetime — if any source is not truly 'static, revisit in Phase B. +// `&'static [u8]` per PORTING.md (no lifetime params on structs). +// TODO(port): lifetime — if any source is not truly 'static, add a lifetime parameter. pub struct Options { pub log_level: LogLevel, @@ -378,9 +378,9 @@ pub fn open_global_bin_dir( // PORT NOTE: Zig borrowed `[]const u8` from `Api.BunInstall` (process-lifetime // arena). Rust `BunInstall` owns `Box<[u8]>`; Options stores `&'static [u8]` -// per Phase-A "no struct lifetime params". Park a clone for the lifetime of -// the install command (matches Zig's never-reset config arena) via the named -// hand-off helper. +// per the "no struct lifetime params" porting convention. Park a clone for the +// lifetime of the install command (matches Zig's never-reset config arena) via +// the named hand-off helper. #[inline] fn leak_static(s: &[u8]) -> &'static [u8] { bun_core::heap::release(s.to_vec().into_boxed_slice()) @@ -395,7 +395,7 @@ impl Options { // Spec PackageManagerOptions.zig:224 `bun_install_: ?*Api.BunInstall` — // every access below is a read of `config.*`; no field is ever written. // Taking `&` (not `&mut`) keeps provenance coherent with the bundler/ - // resolver storage (`Option<&api::BunInstall>` / `*const ()`). + // resolver storage (`Option>`). bun_install_: Option<&Api::BunInstall>, subcommand: Subcommand, ) -> Result<(), bun_alloc::AllocError> { diff --git a/src/install/auto_installer.rs b/src/install/auto_installer.rs index 235e8bcf0e5..ca2e1f8fcfe 100644 --- a/src/install/auto_installer.rs +++ b/src/install/auto_installer.rs @@ -462,36 +462,31 @@ impl hooks::AutoInstaller for PackageManager { // upcast to the `dyn AutoInstaller` trait object the resolver stores. // // SAFETY (callee contract): -// • `log` is the resolver's `*mut bun_ast::Log` (Transpiler-owned, +// • `log` is the resolver's `NonNull` (Transpiler-owned, // process-lifetime; `init_with_runtime` stores it raw). -// • `install` is the type-erased `Option<&Api::BunInstall>` projected from -// `BundleOptions.install` (`*const ()` — null ⇔ None). The pointee is the -// CLI-owned `Box` (process-lifetime). -// • `env` is the type-erased `*mut DotEnv::Loader` (Transpiler-owned, +// • `install` is `BundleOptions.install` (`?*Api.BunInstall`). The pointee is +// the CLI-owned `Box` (process-lifetime), read-only. +// • `env` is the resolver's unwrapped `env_loader` (Transpiler-owned, // process-lifetime). `init_with_runtime` stores it as -// `NonNull>`; the lifetime erasure matches Zig's raw -// `*DotEnv.Loader`. +// `NonNull>`; the `'static` lifetime matches Zig's +// untracked `*DotEnv.Loader`. #[unsafe(no_mangle)] pub unsafe fn __bun_resolver_init_package_manager( - log: *mut bun_ast::Log, - install: *const (), - env: *mut core::ffi::c_void, + mut log: core::ptr::NonNull, + install: Option>, + mut env: core::ptr::NonNull>, ) -> core::ptr::NonNull { // Zig: `bun.HTTPThread.init(&.{})` — idempotent. bun_http::http_thread::init(&Default::default()); - // SAFETY: `install` is either null or points at a live `Api::BunInstall` + // SAFETY: when `Some`, `install` points at a live `Api::BunInstall` // (see `run_command::wire_transpiler_from_ctx`); read-only borrow. - let bun_install: Option<&crate::bun_schema::api::BunInstall> = unsafe { - install - .cast::() - .as_ref() - }; - // SAFETY: caller guarantees `log` / `env` are non-null process-lifetime - // pointers (resolver `.expect`s `env_loader` before calling). - let log_ref: &mut bun_ast::Log = unsafe { &mut *log }; - let env_ref: &mut bun_dotenv::Loader<'static> = - unsafe { &mut *env.cast::>() }; + let bun_install: Option<&crate::bun_schema::api::BunInstall> = + install.map(|p| unsafe { p.as_ref() }); + // SAFETY: caller guarantees `log` / `env` point at process-lifetime + // Transpiler-owned storage with no aliasing `&mut` live across this call. + let log_ref: &mut bun_ast::Log = unsafe { log.as_mut() }; + let env_ref: &mut bun_dotenv::Loader<'static> = unsafe { env.as_mut() }; let pm: *mut PackageManager = crate::package_manager::init_with_runtime( log_ref, diff --git a/src/jsc/AsyncModule.rs b/src/jsc/AsyncModule.rs index 42bf6f57e9f..4aa10c09f29 100644 --- a/src/jsc/AsyncModule.rs +++ b/src/jsc/AsyncModule.rs @@ -1,14 +1,4 @@ //! Port of `src/jsc/AsyncModule.zig`. -//! -//! B-2 un-gate: real `AsyncModule` / `Queue` / `InitOpts` types compile against -//! the `lib.rs` stub surface so `ModuleLoader` can re-export them and -//! `VirtualMachine.modules` can widen from `()` → `Queue`. `fulfill()` and the -//! `Bun__onFulfillAsyncModule` extern are real (called from -//! `RuntimeTranspilerStore::run_from_js_thread`). The package-manager-driven -//! bodies (`Queue::poll_modules` / `resolve_error` / `download_error` / -//! `resume_loading_module`) are preserved verbatim from the Phase-A draft -//! `bun_install::PackageManager` runTasks / `MultiArrayList` column accessors / -//! `bun_bundler::linker` that aren't wired yet. use bun_collections::{ByteVecExt, VecExt}; use core::ffi::c_void; @@ -1265,24 +1255,28 @@ impl AsyncModule { // raw-ptr aliasing matches the Zig `*VirtualMachine` field accesses // (`transpiler.log`/`resolver.log`/`linker.log` are themselves raw // `*mut Log` aliased deliberately — see `Transpiler::set_log`). - let old_log = unsafe { (*jsc_vm).log }; + // `vm.log` is set unconditionally in `init` and never cleared, so the + // `expect` is infallible. + let old_log: core::ptr::NonNull = + unsafe { (*jsc_vm).log }.expect("vm.log set in init"); + let log_nn = core::ptr::NonNull::new(log).expect("AsyncModule log is non-null"); let log_ptr: *mut bun_ast::Log = log; // SAFETY: see above — single-thread VM; raw-ptr field stores. unsafe { (*jsc_vm).transpiler.linker.log = log_ptr; (*jsc_vm).transpiler.log = log_ptr; - (*jsc_vm).transpiler.resolver.log = log_ptr; + (*jsc_vm).transpiler.resolver.log = log_nn; (*jsc_vm).package_manager().log = log_ptr; } let _restore = scopeguard::guard((jsc_vm, old_log), |(jsc_vm, old_log)| { - // SAFETY: same per-thread VM; restoring the original `*mut Log` - // values stored above. + // SAFETY: same per-thread VM; restoring the original log pointers + // stored above. unsafe { - let old_log_ptr = old_log.map(|p| p.as_ptr()).unwrap_or(core::ptr::null_mut()); + let old_log_ptr = old_log.as_ptr(); (*jsc_vm).transpiler.linker.log = old_log_ptr; (*jsc_vm).transpiler.log = old_log_ptr; - (*jsc_vm).transpiler.resolver.log = old_log_ptr; + (*jsc_vm).transpiler.resolver.log = old_log; (*jsc_vm).package_manager().log = old_log_ptr; } }); diff --git a/src/jsc/VirtualMachine.rs b/src/jsc/VirtualMachine.rs index e2fb4c99a64..a4b0bcf3c49 100644 --- a/src/jsc/VirtualMachine.rs +++ b/src/jsc/VirtualMachine.rs @@ -2,15 +2,6 @@ //! //! Today, Bun is one VM per thread, so the name "VirtualMachine" sort of makes //! sense. If that changes, this should be renamed `ScriptExecutionContext`. -//! -//! ────────────────────────────────────────────────────────────────────────── -//! B-2 un-gate: real `VirtualMachine` struct with the core field set -//! (`global`, `event_loop`, `jsc_vm`, `transpiler`, `source_mappings`, -//! `rare_data`, `counters`, `active_tasks`, …) + lifecycle accessors. Fields -//! and methods that name `bun_runtime` / `bun_webcore` types (forward-dep -//! cycle on `bun_jsc`) are preserved verbatim from the Phase-A draft inside -//! `` blocks below; un-gate piecewise as the cycle breaks. -//! ────────────────────────────────────────────────────────────────────────── use core::cell::Cell; use core::ffi::{c_char, c_int, c_void}; @@ -1510,9 +1501,7 @@ impl VirtualMachine { _ => break, }; for hook in hooks { - // SAFETY: ctx/func were registered together by the N-API - // caller (`CleanupHook::init`). - unsafe { (hook.func)(hook.ctx) }; + (hook.func)(hook.ctx); } } // Zig `defer rare_data.cleanup_hooks.clearAndFree(...)` — `mem::take` @@ -4236,7 +4225,7 @@ impl VirtualMachine { let old_log: NonNull = jsc_vm.log.expect("vm.log set in init"); let mut log = bun_ast::Log::default(); jsc_vm.log = NonNull::new(&raw mut log); - jsc_vm.transpiler.resolver.log = &raw mut log; + jsc_vm.transpiler.resolver.log = NonNull::from(&mut log); // TODO(b2-cycle): `transpiler.linker.log` / `resolver.package_manager.log` // — gated bundler fields. // PORT NOTE: Zig `defer { restore old_log }` — fires on every exit @@ -4255,7 +4244,7 @@ impl VirtualMachine { // thread); `old_log` outlives the VM (Box::leak in `init`). let jsc_vm = self.vm.get().as_mut(); jsc_vm.log = Some(self.old_log); - jsc_vm.transpiler.resolver.log = &raw mut *self.old_log.as_ptr(); + jsc_vm.transpiler.resolver.log = self.old_log; } } let _restore = RestoreLog { diff --git a/src/jsc/hot_reloader.rs b/src/jsc/hot_reloader.rs index 580c7de6b28..de3bce70a7c 100644 --- a/src/jsc/hot_reloader.rs +++ b/src/jsc/hot_reloader.rs @@ -1285,7 +1285,7 @@ where // index `len` (overlapping the just-written SEP) // and then slices `len + changed_name.len + 1` // bytes — this includes one byte past the copy. - // Porting verbatim; flag for Phase B review. + // Ported verbatim. // TODO(port): verify intended off-by-one in Zig source _on_file_update_path_buf [file_path_without_trailing_slash.len() @@ -1461,15 +1461,16 @@ type BundlerWatcher = NewHotReloader, bun_event_loop::AnyEventLoop<'static>, true>; /// CYCLEBREAK extern hook: called from `BundleV2::init` (T5) when -/// `cli_watch_flag` is set (bundle_v2.zig:993). Erased via `*mut ()` because -/// the bundler crate can't name `NewHotReloader`. +/// `cli_watch_flag` is set (bundle_v2.zig:993). Defined here (not in +/// `bun_bundler`) because the bundler crate can't name `NewHotReloader`. #[unsafe(no_mangle)] -fn __bun_jsc_enable_hot_module_reloading_for_bundler(bv2: *mut ()) { - // SAFETY: `bv2` is the `&mut *Box>` formed in +fn __bun_jsc_enable_hot_module_reloading_for_bundler( + bv2: core::ptr::NonNull>, +) { + // SAFETY contract: `bv2` is the `&mut *Box>` formed in // `BundleV2::init`; the lifetime is `'static` for the only caller (build // command leaks the CLI arena), and the box is leaked under --watch. - let bv2 = bv2.cast::>(); - BundlerWatcher::enable_hot_module_reloading(bv2, None); + BundlerWatcher::enable_hot_module_reloading(bv2.as_ptr(), None); } pub use crate::MarkedArrayBuffer as Buffer; diff --git a/src/resolver/allocators.rs b/src/resolver/allocators.rs new file mode 100644 index 00000000000..4bf2772e7fc --- /dev/null +++ b/src/resolver/allocators.rs @@ -0,0 +1,6 @@ +//! ── bun_alloc::allocators re-export ────────────────────────────────────── +//! `Result`/`ItemStatus` live at the `bun_alloc` crate root (re-exported via +//! `bun_alloc::allocators`); add the `Status` alias the resolver body spells. + +pub use bun_alloc::ItemStatus as Status; +pub use bun_alloc::allocators::*; diff --git a/src/resolver/dir_info.rs b/src/resolver/dir_info.rs index eeebfed4af0..980eddf5c5d 100644 --- a/src/resolver/dir_info.rs +++ b/src/resolver/dir_info.rs @@ -413,13 +413,13 @@ pub trait HashMapExt { fn get_or_put( &mut self, key: &[u8], - ) -> core::result::Result; + ) -> core::result::Result; fn put( &mut self, - result: &mut crate::__phase_a_body::allocators::Result, + result: &mut crate::allocators::Result, value: DirInfo, ) -> core::result::Result<*mut DirInfo, bun_core::Error>; - fn mark_not_found(&mut self, result: crate::__phase_a_body::allocators::Result); + fn mark_not_found(&mut self, result: crate::allocators::Result); fn remove(&mut self, key: &[u8]) -> bool; fn values_mut(&mut self) -> core::slice::IterMut<'_, DirInfo>; } @@ -428,14 +428,14 @@ impl HashMapExt for HashMap { fn get_or_put( &mut self, key: &[u8], - ) -> core::result::Result { + ) -> core::result::Result { // Dot-syntax picks inherent `BSSMapInner::get_or_put` (inherent > trait); not recursive. self.get_or_put(key).map_err(Into::into) } #[inline] fn put( &mut self, - result: &mut crate::__phase_a_body::allocators::Result, + result: &mut crate::allocators::Result, value: DirInfo, ) -> core::result::Result<*mut DirInfo, bun_core::Error> { // Spec bun_alloc.zig:615 `put(self: *Self, result: *Result, value) !*ValueType` — @@ -452,7 +452,7 @@ impl HashMapExt for HashMap { .map_err(Into::into) } #[inline] - fn mark_not_found(&mut self, result: crate::__phase_a_body::allocators::Result) { + fn mark_not_found(&mut self, result: crate::allocators::Result) { // Inherent `BSSMapInner::mark_not_found` (inherent > trait); not recursive. self.mark_not_found(result) } diff --git a/src/resolver/lib.rs b/src/resolver/lib.rs index 4c62b0fe522..3f4dc18e21f 100644 --- a/src/resolver/lib.rs +++ b/src/resolver/lib.rs @@ -39,8 +39,9 @@ pub mod node_fallbacks; pub mod package_json; pub mod tsconfig_json; -// ── B-2 un-gated surface ────────────────────────────────────────────────── -// Real types now live in `__phase_a_body` below; the header re-exports them so +// ── Re-exported resolver surface ────────────────────────────────────────── +// Real types live in the `resolver` / `result` / `options` / +// `standalone_module_graph` sibling modules; the header re-exports them so // dependents see the same paths as the old stub surface. /// Re-export real `GlobalCache`. @@ -75,22 +76,22 @@ pub(crate) fn path_string_static(ps: &bun_core::PathString) -> &'static [u8] { unsafe { bun_ptr::Interned::assume(ps.slice()) }.as_bytes() } -// Re-export the un-gated Phase-A body. `Resolver`, `Result`, `MatchResult`, -// `PathPair`, `DebugLogs`, `SideEffects`, etc. are defined there. -pub use __phase_a_body::StandaloneModuleGraph; -pub use __phase_a_body::options; -pub use __phase_a_body::{ - AnyResolveWatcher, BrowserMapPathKind, Bufs, DebugLogs, DebugMeta, DirEntryResolveQueueItem, - Dirname, FlushMode, LoadResult, MatchResult, MatchResultUnion, PathPair, PendingResolution, - PendingResolutionTag, Resolver, Result, ResultFlags, ResultUnion, RootPathPair, - SideEffectsData, -}; +// Re-export the resolver implementation. `Resolver`, `Result`, `MatchResult`, +// `PathPair`, `DebugLogs`, `SideEffects`, etc. are defined in the `resolver` / +// `result` / `standalone_module_graph` sibling modules. /// Re-export so dependents can spell `bun_resolver::install_types::AutoInstaller`. pub use ::bun_install_types::resolver_hooks as install_types; +pub use resolver::{AnyResolveWatcher, BrowserMapPathKind, Bufs, Dirname, Resolver, RootPathPair}; +pub use result::{ + DebugLogs, DebugMeta, DirEntryResolveQueueItem, FlushMode, LoadResult, MatchResult, + MatchResultUnion, PathPair, PendingResolution, PendingResolutionTag, Result, ResultFlags, + ResultUnion, SideEffectsData, +}; +pub use standalone_module_graph::StandaloneModuleGraph; /// Minimal real subset of `src/resolver/fs.zig` so `bun_resolver::fs::X` paths -/// resolve for downstream crates during B-2. Full Phase-A draft remains in -/// `fs.rs` (gated) until bun_alloc::BSSStringList / bun_output land. +/// resolve for downstream crates. The full draft remains in `fs.rs` (gated) +/// until bun_alloc::BSSStringList / bun_output land. pub mod fs { use core::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use std::io::Write as _; @@ -951,8 +952,7 @@ pub mod fs { pub fn get_or_put( &mut self, key: &[u8], - ) -> core::result::Result - { + ) -> core::result::Result { self.inner() .get_or_put(key) .map_err(|_| bun_core::err!("OutOfMemory")) @@ -962,7 +962,7 @@ pub mod fs { } pub fn put( &mut self, - result: &mut crate::__phase_a_body::allocators::Result, + result: &mut crate::allocators::Result, value: EntriesOption, ) -> core::result::Result<*mut EntriesOption, bun_core::Error> { // PORT NOTE: `BSSMapInner::put` mutates `result.index` to record placement; callers @@ -973,7 +973,7 @@ pub mod fs { .map(|v| std::ptr::from_mut::(v)) .map_err(|_| bun_core::err!("OutOfMemory")) } - pub fn mark_not_found(&mut self, result: crate::__phase_a_body::allocators::Result) { + pub fn mark_not_found(&mut self, result: crate::allocators::Result) { self.inner().mark_not_found(result) } pub fn remove(&mut self, key: &[u8]) -> bool { @@ -1705,7 +1705,7 @@ pub mod fs { } // ── `file_system` namespace shim ───────────────────────────────────── - // The Phase-A resolver body addresses types via `Fs::file_system::*` (the + // The resolver body addresses types via `Fs::file_system::*` (the // Zig nesting was `FileSystem.RealFS.EntriesOption` etc.). Re-export the // flat types under the nested module paths the body expects. /// Re-exports from the full `fs.rs` port: `BOM` (detect/strip tables) and @@ -2606,7667 +2606,10 @@ pub mod cache { pub use ::bun_paths::{is_package_path, is_package_path_not_absolute}; -pub mod __phase_a_body { - use super::{is_package_path, is_package_path_not_absolute}; - - use core::ptr::NonNull; - use std::io::Write as _; - - // ── Cross-crate type surface ────────────────────────────────────────────── - // Higher-tier symbols are reached through lower-tier crates: - // • install value types + AutoInstaller trait — bun_install_types (MOVE_DOWN) - // • HardcodedModule alias table — bun_resolve_builtins - // • StandaloneModuleGraph — trait below; impl in bun_standalone_graph - // • perf / crash_handler — real bun_perf / bun_crash_handler - use ::bun_install_types::resolver_hooks as Install; - use ::bun_install_types::resolver_hooks::{ - AutoInstaller, EnqueueResult, Features as InstallFeatures, PreinstallState, Resolution, - TaskCallbackContext, WakeHandler, - }; - use ::bun_semver as Semver; - // Re-exported so downstream (bun_bundler) can name the trait in - // `Transpiler::get_package_manager`'s return type without a direct - // `bun_install_types` dep (LAYERING: pass-through, no new edge). - pub use ::bun_install_types::resolver_hooks::AutoInstaller as PackageManagerTrait; - - // LAYERING: `PackageManager.initWithRuntime` (Zig resolver.zig:540) lives in - // `bun_install`, which depends on this crate. The lazy-init body is defined - // `#[no_mangle]` in `bun_install::auto_installer` and resolved at link time - // (same pattern as `__bun_regex_*` / `__BUN_RUNTIME_HOOKS`). `install` is the - // type-erased `?*Api.BunInstall` (`self.opts.install`); `env` is the - // type-erased `*DotEnv.Loader` (lifetime-erased — the install crate stores it - // as a raw `NonNull>`). - unsafe extern "Rust" { - /// SAFETY (genuine FFI precondition — NOT a `safe fn` candidate): impl - /// reborrows `&mut *log` / `&mut *env` and reads `*install` if non-null. - /// All three must point at process-lifetime Transpiler-owned storage; the - /// returned `NonNull` names the `'static` `PackageManager` singleton. - fn __bun_resolver_init_package_manager( - log: *mut bun_ast::Log, - install: *const (), - env: *mut core::ffi::c_void, - ) -> NonNull; - } - use crate::cache::Set as CacheSet; - use ::bun_resolve_builtins::{Alias as HardcodedAlias, Cfg as HardcodedAliasCfg}; - - /// Resolver's view of a compiled-standalone-binary module graph. The concrete - /// `bun_standalone_graph::Graph` (which depends on `bun_bundler`) implements - /// this; the resolver holds a trait object so it stays below both in the dep - /// graph. The path-prefix predicate lives in - /// `bun_options_types::standalone_path` (MOVE_DOWN) and is callable without a - /// graph instance. - pub trait StandaloneModuleGraph: Send + Sync { - /// Look up `name` (already known to be under the standalone virtual root) - /// and return the embedded file's canonical name slice if present. - fn find_assume_standalone_path(&self, name: &[u8]) -> Option<&[u8]>; - /// Look up `name` (any path — checks the standalone virtual-root prefix - /// first) and return the embedded file's canonical name slice if present. - /// Spec `StandaloneModuleGraph.find`. - fn find(&self, name: &[u8]) -> Option<&[u8]>; - /// `StandaloneModuleGraph.base_public_path_with_default_suffix` — the - /// virtual-root prefix used for embedded modules (e.g. `/$bunfs/root/`). - /// Baked-in `'static` constant; surfaced here so low-tier callers - /// (worker entry-point resolution) don't need the concrete graph type. - fn base_public_path_with_default_suffix(&self) -> &'static [u8]; - /// `StandaloneModuleGraph.compile_exec_argv` — the `--compile-exec-argv` - /// string baked into a `bun build --compile` binary. Exposed via the trait - /// so `process.execArgv` (lower-tier `bun_jsc` callers holding only the - /// trait object) can read it without downcasting to the concrete graph. - fn compile_exec_argv(&self) -> &[u8]; - } - - /// `Dependency` namespace as the body spells it (Zig: `Dependency.Version` / - /// `Dependency.Behavior`). Re-exports the canonical `bun_install_types` items. - pub mod Dependency { - pub use ::bun_install_types::resolver_hooks::{ - Behavior, Dependency, DependencyVersion as Version, DependencyVersionTag, - }; - pub mod version { - pub use ::bun_install_types::resolver_hooks::DependencyVersionTag as Tag; - } - } - - /// Transitional re-export module: `package_json.rs` and a few external crates - /// still spell these paths via `__forward_decls`; the items are now real - /// re-exports of `bun_install_types` (no local stubs). - pub(crate) mod __forward_decls { - pub(crate) use crate::cache::{Entry as FsCacheEntry, Fs as FsCache, Set as CacheSet}; - pub(crate) use ::bun_install_types::resolver_hooks as Install; - pub(crate) use ::bun_install_types::resolver_hooks::Resolution; - } - // bun_paths shim — value-dispatched join helpers over `resolve_path::Platform`. - // `dirname` (`Option`-returning, `std.fs.path.dirname` semantics) and - // `PosixToWinNormalizer` are the real `::bun_paths` items — brought in by the - // glob / explicit re-export below, no local re-implementation. - mod bun_paths { - pub(super) use ::bun_paths::resolve_path::PosixToWinNormalizer; - pub(super) use ::bun_paths::resolve_path::is_sep_any; - pub(super) use ::bun_paths::*; - - /// Value-dispatch over `Platform` to the const-generic `PlatformT` - /// monomorphizations in `resolve_path`. The resolver body threads - /// `Platform::AUTO` / `Platform::Loose` at runtime (carried over from Zig's - /// `comptime _platform: Platform` callsites that took a function param). - macro_rules! dispatch_platform { - ($p:expr, |$P:ident| $body:expr) => {{ - use ::bun_paths::resolve_path::{self as rp, platform}; - match $p { - rp::Platform::Loose => { - type $P = platform::Loose; - $body - } - rp::Platform::Windows => { - type $P = platform::Windows; - $body - } - rp::Platform::Posix => { - type $P = platform::Posix; - $body - } - rp::Platform::Nt => { - type $P = platform::Nt; - $body - } - } - }}; - } - pub(super) fn dirname_platform(p: &[u8], platform: Platform) -> &[u8] { - dispatch_platform!(platform, |P| ::bun_paths::resolve_path::dirname::

(p)) - } - /// Port of `bun.path.joinAbsStringBuf` (value-dispatched). - pub(super) fn join_abs_string_buf<'b>( - cwd: &'b [u8], - buf: &'b mut [u8], - parts: &[&[u8]], - platform: Platform, - ) -> &'b [u8] { - dispatch_platform!( - platform, - |P| ::bun_paths::resolve_path::join_abs_string_buf::

(cwd, buf, parts) - ) - } - pub(super) fn join_abs(cwd: &[u8], platform: Platform, part: &[u8]) -> &'static [u8] { - // PORT NOTE: `resolve_path::join_abs` ties the result lifetime to `cwd`, but the - // returned slice always points into the threadlocal `PARSER_JOIN_INPUT_BUFFER` - // (or is `cwd` itself when `parts.is_empty()`, which never happens here — we - // pass exactly one part). Re-erase to `'static` so the resolver can hold it - // across `&mut self` calls. - let s = dispatch_platform!(platform, |P| ::bun_paths::resolve_path::join_abs::

( - cwd, part - )); - // SAFETY: see PORT NOTE — slice borrows threadlocal storage, valid 'static per-thread. - unsafe { bun_ptr::detach_lifetime(s) } - } - pub(super) fn join(parts: &[&[u8]], platform: Platform) -> &'static [u8] { - dispatch_platform!(platform, |P| ::bun_paths::resolve_path::join::

(parts)) - } - pub(super) fn join_string_buf<'b>( - buf: &'b mut [u8], - parts: &[&[u8]], - platform: Platform, - ) -> &'b [u8] { - dispatch_platform!( - platform, - |P| ::bun_paths::resolve_path::join_string_buf::

(buf, parts) - ) - } - /// Zig `bun.pathLiteral` — compile-time platform-separator literal. Zig - /// rewrites `/` → `\` at comptime; Rust can't transform a borrowed - /// `&'static [u8]` in a const fn, so this is a macro that emits a fresh - /// const array with the swap applied. Result is `&'static [u8; N]` - /// (coerces to `&[u8]`). - #[macro_export] - #[doc(hidden)] - macro_rules! __resolver_path_literal { - ($p:expr) => {{ - const __IN: &[u8] = $p; - const __N: usize = __IN.len(); - const fn __swap(input: &[u8]) -> [u8; __N] { - let mut out = [0u8; __N]; - let mut i = 0; - while i < __N { - out[i] = if cfg!(windows) && input[i] == b'/' { - b'\\' - } else { - input[i] - }; - i += 1; - } - out - } - const __OUT: [u8; __N] = __swap(__IN); - &__OUT - }}; - } - pub(super) use __resolver_path_literal as path_literal; - pub(super) fn windows_filesystem_root(p: &[u8]) -> &[u8] { - ::bun_paths::resolve_path::windows_filesystem_root(p) - } - } - // bun_core::strings shim — re-export the canonical `immutable/paths` helpers - // (`without_trailing_slash_windows_path` / `path_contains_node_modules_folder` / - // `without_leading_path_separator` / `char_is_any_slash`) instead of locally - // re-implementing them. The previous local copies diverged from the spec - // (single-strip vs. while-loop, `is_sep_any` vs. platform `SEP`). - mod strings { - pub(super) use bun_paths::strings::paths::{ - char_is_any_slash, path_contains_node_modules_folder, without_leading_path_separator, - without_trailing_slash_windows_path, - }; - pub(super) use bun_paths::strings::*; - #[inline] - pub(super) fn index_of_any(slice: &[u8], chars: &'static [u8]) -> Option { - bun_core::strings::index_of_any(slice, chars).map(|v| v as usize) - } - } - // bun_sys shim — adds the `std.fs`-shaped dir-open surface the resolver names - // (`openDirAbsoluteZ` / `Dir.openDirZ`) on top of the real `::bun_sys` crate. - // `open` / `open_dir_for_iteration` / `get_fd_path` / `OpenDirOptions` / - // `iterate_dir` are now provided by the `pub use ::bun_sys::*` glob. - mod bun_sys { - pub(super) use ::bun_sys::*; - - /// Port of `std.fs.openDirAbsoluteZ` — `open(path, O_DIRECTORY|O_RDONLY|O_CLOEXEC[|O_NOFOLLOW])`. - /// `opts.iterate` is a no-op on POSIX (Zig only used it to pick `iterate=true` - /// on `IterableDir`, which is just an open mode hint). - pub(super) fn open_dir_absolute_z( - path: &::bun_core::ZStr, - opts: OpenDirOptions, - ) -> core::result::Result { - #[cfg(unix)] - let nofollow = if opts.no_follow { libc::O_NOFOLLOW } else { 0 }; - #[cfg(not(unix))] - let nofollow = { - let _ = opts; - 0 - }; - ::bun_sys::open(path, O::DIRECTORY | O::CLOEXEC | O::RDONLY | nofollow, 0) - .map_err(Into::into) - } - /// Port of `std.fs.Dir.openDirZ` — `openat(dir, path, O_DIRECTORY|O_RDONLY|O_CLOEXEC)`. - pub(super) fn open_dir_z( - dir: Fd, - path: &[u8], - _opts: OpenDirOptions, - ) -> core::result::Result { - // PORT NOTE: callers pass either a `&'static [u8]` literal or a NUL-terminated - // slice; `open_dir_at` builds its own ZStr internally so we strip the sentinel. - let path = if path.last() == Some(&0) { - &path[..path.len() - 1] - } else { - path - }; - ::bun_sys::open_dir_at(dir, path).map_err(Into::into) - } - // `iterate_dir` / `dir_iterator::WrappedIterator` are real ports in - // `::bun_sys::dir_iterator` (POSIX getdents / Windows NtQueryDirectoryFile) - // and reach this module via the `pub use ::bun_sys::*` glob above. - pub(super) use ::bun_sys::RawFd; - } - - /// `bun_sys::Fd` extension surface — thin method-syntax wrappers over the - /// free functions `::bun_sys::{close, get_fd_path}` and `Fd::native`, so the - /// resolver body can spell `fd.close()` / `fd.get_fd_path(buf)` per the Zig. - trait FdExt: Sized { - fn close(self); - fn cast(self) -> bun_sys::RawFd; - fn native(self) -> bun_sys::RawFd; - fn get_fd_path<'b>( - self, - buf: &'b mut ::bun_paths::PathBuffer, - ) -> core::result::Result<&'b [u8], ::bun_core::Error>; - } - impl FdExt for ::bun_sys::Fd { - #[inline] - fn close(self) { - let _ = ::bun_sys::close(self); - } - #[inline] - fn cast(self) -> bun_sys::RawFd { - ::bun_sys::Fd::native(self) - } - #[inline] - fn native(self) -> bun_sys::RawFd { - ::bun_sys::Fd::native(self) - } - #[inline] - fn get_fd_path<'b>( - self, - buf: &'b mut ::bun_paths::PathBuffer, - ) -> core::result::Result<&'b [u8], ::bun_core::Error> { - ::bun_sys::get_fd_path(self, buf) - .map(|s| &*s) - .map_err(Into::into) - } - } - trait FdZero { - const ZERO: ::bun_sys::Fd; - } - impl FdZero for ::bun_sys::Fd { - const ZERO: ::bun_sys::Fd = ::bun_sys::Fd::INVALID; - } - - // ── bun_alloc::allocators re-export ────────────────────────────────────── - // `Result`/`ItemStatus` live at the `bun_alloc` crate root (re-exported via - // `bun_alloc::allocators`); add the `Status` alias the resolver body spells. - pub mod allocators { - pub use bun_alloc::ItemStatus as Status; - pub use bun_alloc::allocators::*; - } - - // Resolver-tier `options` — the canonical resolver-input types. - // - // MOVE_DOWN COMPLETE for the resolver↔bundler cycle: these are the types the - // resolver reads, defined at the lowest tier that can name all their parts - // (`jsx::Pragma`/`ConditionsMap` live in this crate; `Target`/`Loader` in - // `bun_options_types`). `bun_bundler::options::BundleOptions` is the ~200-field - // CLI/config aggregate; `bun_bundler::transpiler::resolver_bundle_options_subset` - // projects it into this struct for `Resolver::init1`. These are NOT a re-decl - // of the bundler type — the bundler depends on this crate and re-exports them. - pub mod options { - pub use crate::tsconfig_json::options::jsx; - pub(crate) use bun_ast::{Loader, LoaderHashTable, Target}; - pub use bun_options_types::bundle_enums::ModuleType; - - /// Port of `bundler/options.zig` `Packages`. - #[derive(Clone, Copy, PartialEq, Eq, Default)] - pub enum Packages { - #[default] - Bundle, - External, - } - - /// Port of `bundler/options.zig` `ExternalModules`. - #[derive(Default)] - pub struct ExternalModules { - pub patterns: Vec, - pub abs_paths: StringSet, - pub node_modules: StringSet, - } - impl Clone for ExternalModules { - fn clone(&self) -> Self { - // `StringSet::clone` is an inherent fallible method (returns - // `Result<_, AllocError>`), so this can't be `#[derive(Clone)]`. - Self { - patterns: self.patterns.clone(), - abs_paths: self.abs_paths.clone().expect("oom"), - node_modules: self.node_modules.clone().expect("oom"), - } - } - } - #[derive(Debug, Clone)] - pub struct WildcardPattern { - pub prefix: Box<[u8]>, - pub suffix: Box<[u8]>, - } - /// Re-export the real set type so `bun_bundler` can project user-supplied - /// `--external` `abs_paths`/`node_modules` through. The previous local ZST - /// stub returned `count() == 0` / `contains(..) == false`, so the resolver - /// silently ignored every `--external` absolute path / package name. - pub use bun_collections::StringSet; - - /// Port of `bundler/options.zig` `Conditions`. - #[derive(Default)] - pub struct Conditions { - pub import: crate::package_json::ConditionsMap, - pub require: crate::package_json::ConditionsMap, - pub style: crate::package_json::ConditionsMap, - } - - /// `Copy` tag selecting one of the extension-order lists owned by - /// [`BundleOptions`]. Replaces the previous `*const [Box<[u8]>]` - /// self-reference (`Resolver.extension_order` pointing into - /// `Resolver.opts`) with a value type — the Zig save/restore pattern - /// (`resolver.zig:691-696` etc.) survives unchanged because the tag is - /// `Copy`, and the actual slice is resolved on demand via - /// [`BundleOptions::ext_order_slice`] / [`Resolver::extension_order`]. - #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] - pub enum ExtOrder { - /// `opts.extension_order.default.default` - #[default] - DefaultDefault, - /// `opts.extension_order.default.esm` - DefaultEsm, - /// `opts.extension_order.node_modules.default` - NodeModulesDefault, - /// `opts.extension_order.node_modules.esm` - NodeModulesEsm, - /// `opts.extension_order.css` (Zig reads `Defaults.CssExtensionOrder` directly) - Css, - /// `opts.main_field_extension_order` — used when resolving the `"main"` - /// package.json field (`resolver.zig:3703,3715,3721`). - MainField, - } - - /// Convert a `&[&[u8]]` default constant into the owned form the resolver - /// stores. Mirrors `bun_bundler::options::owned_string_list`. - pub fn owned_string_list(s: &[&[u8]]) -> Box<[Box<[u8]>]> { - s.iter().map(|s| Box::<[u8]>::from(*s)).collect() - } - - /// Port of `bundler/options.zig` `ResolveFileExtensions`. - pub struct ExtensionOrder { - pub default: ExtensionOrderGroup, - pub node_modules: ExtensionOrderGroup, - /// Not on the bundler-side struct — the spec resolver reads - /// `Defaults.CssExtensionOrder` directly. Stored here so every - /// [`ExtOrder`] tag resolves into storage with the same owner/lifetime. - pub css: Box<[Box<[u8]>]>, - } - pub struct ExtensionOrderGroup { - pub default: Box<[Box<[u8]>]>, - pub esm: Box<[Box<[u8]>]>, - } - impl Default for ExtensionOrderGroup { - fn default() -> Self { - ExtensionOrderGroup { - default: owned_string_list(bundle_options::defaults::EXTENSION_ORDER), - esm: owned_string_list(bundle_options::defaults::MODULE_EXTENSION_ORDER), - } - } - } - impl Default for ExtensionOrder { - fn default() -> Self { - ExtensionOrder { - default: ExtensionOrderGroup::default(), - node_modules: ExtensionOrderGroup { - default: owned_string_list( - bundle_options::defaults::node_modules::EXTENSION_ORDER, - ), - esm: owned_string_list( - bundle_options::defaults::node_modules::MODULE_EXTENSION_ORDER, - ), - }, - css: owned_string_list(bundle_options::defaults::CSS_EXTENSION_ORDER), - } - } - } - impl ExtensionOrder { - /// Port of `options.zig` `ResolveFileExtensions.kind`. Returns the - /// [`ExtOrder`] tag; resolve to a slice via - /// [`BundleOptions::ext_order_slice`]. - pub fn kind(&self, kind: bun_ast::ImportKind, is_node_modules: bool) -> ExtOrder { - use bun_ast::ImportKind as K; - match kind { - K::Url | K::AtConditional | K::At => ExtOrder::Css, - K::Stmt | K::EntryPointBuild | K::EntryPointRun | K::Dynamic => { - if is_node_modules { - ExtOrder::NodeModulesEsm - } else { - ExtOrder::DefaultEsm - } - } - _ => { - if is_node_modules { - ExtOrder::NodeModulesDefault - } else { - ExtOrder::DefaultDefault - } - } - } - } - } - - impl BundleOptions { - /// Resolve an [`ExtOrder`] tag to the slice it names inside `self`. - /// All targets are `Box<[Box<[u8]>]>` owned by `self` and never - /// reallocated after `Resolver::init1`, so the returned borrow is - /// stable for the resolver's lifetime. - #[inline] - pub fn ext_order_slice(&self, tag: ExtOrder) -> &[Box<[u8]>] { - match tag { - ExtOrder::DefaultDefault => &self.extension_order.default.default, - ExtOrder::DefaultEsm => &self.extension_order.default.esm, - ExtOrder::NodeModulesDefault => &self.extension_order.node_modules.default, - ExtOrder::NodeModulesEsm => &self.extension_order.node_modules.esm, - ExtOrder::Css => &self.extension_order.css, - ExtOrder::MainField => &self.main_field_extension_order, - } - } - } - - pub mod bundle_options { - pub use super::ForceNodeEnv; - pub mod defaults { - pub const CSS_EXTENSION_ORDER: &[&[u8]] = &[b".css"]; - // Mirrors `bun_bundler::options::bundle_options_defaults::EXTENSION_ORDER` - // / `MODULE_EXTENSION_ORDER` — duplicated so `Default for BundleOptions` - // below is self-contained (resolver sits below bundler in the dep graph). - pub const EXTENSION_ORDER: &[&[u8]] = &[ - b".tsx", b".ts", b".jsx", b".cts", b".cjs", b".js", b".mjs", b".mts", b".json", - ]; - pub const MODULE_EXTENSION_ORDER: &[&[u8]] = &[ - b".tsx", b".jsx", b".mts", b".ts", b".mjs", b".js", b".cts", b".cjs", b".json", - ]; - /// Mirrors `bun_bundler::options::bundle_options_defaults::node_modules`. - pub mod node_modules { - pub const EXTENSION_ORDER: &[&[u8]] = &[ - b".jsx", b".cjs", b".js", b".mjs", b".mts", b".tsx", b".ts", b".cts", - b".json", - ]; - pub const MODULE_EXTENSION_ORDER: &[&[u8]] = &[ - b".mjs", b".jsx", b".js", b".mts", b".tsx", b".ts", b".cjs", b".cts", - b".json", - ]; - } - } - } - - // B-3 UNIFIED: FORWARD_DECL dropped — canonical type moved down to - // `bun_options_types::bundle_enums::ForceNodeEnv`. Re-exported so the - // `options::ForceNodeEnv` / `bundle_options::ForceNodeEnv` paths and the - // field on the local `BundleOptions` subset stay source-compatible. - pub use ::bun_options_types::ForceNodeEnv; - - /// Port of `bundler/options.zig` `Framework` (Bake) — only the - /// `built_in_modules` field, which is the sole resolver-read member. - pub struct Framework { - pub built_in_modules: - bun_collections::StringArrayHashMap, - } - - /// Resolver-tier `BundleOptions` — the canonical resolver-input struct. - /// `bun_bundler::options::BundleOptions` (the ~200-field CLI/config - /// aggregate) projects into this via - /// `bun_bundler::transpiler::resolver_bundle_options_subset`; the bundler - /// depends on this crate, so this type is the lower-tier source of truth - /// for everything resolution reads. - pub struct BundleOptions { - pub target: Target, - pub packages: Packages, - pub jsx: jsx::Pragma, - pub extension_order: ExtensionOrder, - pub conditions: Conditions, - pub external: ExternalModules, - pub extra_cjs_extensions: Box<[Box<[u8]>]>, - pub framework: Option, - pub global_cache: bun_options_types::global_cache::GlobalCache, - // Zig: `?*api.BunInstall` (options.zig:1753). Spec consumer - // `PackageManagerOptions.zig:load` only reads through it, so `*const` - // — the bundler projects this from `Option<&api::BunInstall>` and a - // `*mut` here would launder read-only provenance into a writable ptr. - pub install: *const (), - pub load_package_json: bool, - pub load_tsconfig_json: bool, - pub main_field_extension_order: Box<[Box<[u8]>]>, - pub main_fields: Box<[Box<[u8]>]>, - /// Spec resolver.zig `auto_main` compares the *pointer* of - /// `opts.main_fields` against `Target.DefaultMainFields.get(target)` to - /// detect "user did not pass --main-fields". The bundler stores an owned - /// `Box<[Box<[u8]>]>` whose pointer can never match a static, so the - /// bundler projects this flag explicitly instead. - pub main_fields_is_default: bool, - pub mark_builtins_as_external: bool, - pub polyfill_node_globals: bool, - pub prefer_offline_install: bool, - pub preserve_symlinks: bool, - pub rewrite_jest_for_tests: bool, - pub tsconfig_override: Option>, - pub production: bool, - pub force_node_env: ForceNodeEnv, - // Bundler-only fields read via `c.resolver.opts` in - // `linker_context/*` (Zig stores the full `BundleOptions` on the - // resolver). Projected by `bun_bundler` at link time. - pub output_dir: Box<[u8]>, - pub root_dir: Box<[u8]>, - pub public_path: Box<[u8]>, - pub compile: bool, - pub supports_multiple_outputs: bool, - pub tree_shaking: bool, - pub allow_runtime: bool, - } - - impl Default for BundleOptions { - /// Spec: `options.zig` field-init defaults. Only the fields the resolver - /// reads — `bun_bundler::Transpiler::init` overlays the per-field - /// projections it can map (target/packages/jsx/bools/global_cache/…) - /// before handing this to `Resolver::init1`. - fn default() -> Self { - BundleOptions { - target: Target::default(), - packages: Packages::default(), - jsx: jsx::Pragma::default(), - extension_order: ExtensionOrder::default(), - conditions: Conditions::default(), - external: ExternalModules::default(), - extra_cjs_extensions: Box::default(), - framework: None, - global_cache: Default::default(), - install: core::ptr::null(), - load_package_json: true, - load_tsconfig_json: true, - main_field_extension_order: owned_string_list( - bundle_options::defaults::EXTENSION_ORDER, - ), - main_fields: owned_string_list(DEFAULT_MAIN_FIELDS.get(Target::default())), - main_fields_is_default: true, - mark_builtins_as_external: false, - polyfill_node_globals: false, - prefer_offline_install: false, - preserve_symlinks: false, - rewrite_jest_for_tests: false, - tsconfig_override: None, - output_dir: Box::default(), - root_dir: Box::default(), - public_path: Box::default(), - compile: false, - supports_multiple_outputs: true, - tree_shaking: false, - allow_runtime: true, - production: false, - force_node_env: ForceNodeEnv::default(), - } - } - } - - impl BundleOptions { - /// Port of `options.zig:1825 BundleOptions.setProduction`. - pub fn set_production(&mut self, value: bool) { - if self.force_node_env == ForceNodeEnv::Unspecified { - self.production = value; - self.jsx.development = !value; - } - } - } - - // Port of `bundler/options.zig` `Target.DefaultMainFields`. - // - // These are the per-target default `--main-fields` orderings. `BundleOptions.main_fields` - // is initialised to alias one of these slices (see options.zig:1712 / 2022), and the - // resolver's `auto_main` heuristic at `load_as_main_field` compares the *pointer* of - // `opts.main_fields` against `DEFAULT_MAIN_FIELDS.get(opts.target)` to detect whether the - // user explicitly set a main-fields list. The previous `&[]` stub made that check always - // false, silently disabling the module-vs-main dual-resolution path. - pub struct TargetMainFields; - - // Note that this means if a package specifies "module" and "main", the ES6 - // module will not be selected. This means tree shaking will not work when - // targeting node environments. - // - // Some packages incorrectly treat the "module" field as "code for the browser". It - // actually means "code for ES6 environments" which includes both node and the browser. - // - // For example, the package "@firebase/app" prints a warning on startup about - // the bundler incorrectly using code meant for the browser if the bundler - // selects the "module" field instead of the "main" field. - // - // This is unfortunate but it's a problem on the side of those packages. - // They won't work correctly with other popular bundlers (with node as a target) anyway. - static DEFAULT_MAIN_FIELDS_NODE: &[&[u8]] = &[b"main", b"module"]; - - // Note that this means if a package specifies "main", "module", and - // "browser" then "browser" will win out over "module". This is the - // same behavior as webpack: https://github.com/webpack/webpack/issues/4674. - // - // This is deliberate because the presence of the "browser" field is a - // good signal that this should be preferred. Some older packages might only use CJS in their "browser" - // but in such a case they probably don't have any ESM files anyway. - static DEFAULT_MAIN_FIELDS_BROWSER: &[&[u8]] = - &[b"browser", b"module", b"jsnext:main", b"main"]; - static DEFAULT_MAIN_FIELDS_BUN: &[&[u8]] = &[b"module", b"main", b"jsnext:main"]; - - impl TargetMainFields { - pub fn get(&self, t: Target) -> &'static [&'static [u8]] { - match t { - Target::Node => DEFAULT_MAIN_FIELDS_NODE, - Target::Browser => DEFAULT_MAIN_FIELDS_BROWSER, - Target::Bun | Target::BunMacro | Target::BakeServerComponentsSsr => { - DEFAULT_MAIN_FIELDS_BUN - } - } - } - } - pub const DEFAULT_MAIN_FIELDS: TargetMainFields = TargetMainFields; - } - use self::bun_paths as ResolvePath; - use ::bun_ast::import_record as ast; - use ::bun_core::Output; - use ::bun_core::{Environment, FeatureFlags, Generation}; - use bun_ast::Msg; - use bun_collections::{BoundedArray, MultiArrayList}; - use bun_core::{MutableString, PathString}; - use bun_dotenv::env_loader as DotEnv; - use bun_paths::{MAX_PATH_BYTES, PathBuffer, SEP, SEP_STR}; - use bun_perf::system_timer::Timer; - use bun_sys::Fd as FD; - use bun_threading::Mutex; - - use crate::fs as Fs; - use crate::fs::FilenameStoreAppender; - use crate::node_fallbacks as NodeFallbackModules; - use crate::package_json::{BrowserMap, ESModule, PackageJSON}; - use crate::tsconfig_json::TSConfigJSON; - - pub use crate::data_url::DataURL; - pub use crate::dir_info as DirInfo; - pub use crate::dir_info::DirInfoRef; - pub use ::bun_options_types::global_cache::GlobalCache; - - // ── Process-lifetime arenas for DirInfo-cached parses ───────────────────── - // The DirInfo cache (`DirInfo::hash_map_instance()`) is a true process-lifetime - // singleton; entries hold `&'static PackageJSON` / `&'static TSConfigJSON` and - // borrow `&'static [u8]` source bytes. Zig models this with `bun.TrivialNew` - // (heap-allocate, never free). PORTING.md §Forbidden bars `Box::leak`/ - // `mem::forget` for this — process-lifetime storage must go through - // `LazyLock`. These append-only arenas are that storage; the `Box` heap - // address is stable across `Vec` growth, so handing out `&'static T` is sound. - - /// Intern a parsed `PackageJSON` into the process-lifetime DirInfo arena. - /// Returns `NonNull` (not `&'static`) so the mut-provenance survives into - /// `DirInfo::reset()`'s `drop_in_place` -- handing out `&T` here and casting - /// back to `*mut T` at the drop site would be UB under Stacked Borrows. - fn intern_package_json(pkg: PackageJSON) -> core::ptr::NonNull { - static ARENA: std::sync::LazyLock>>> = - std::sync::LazyLock::new(Default::default); - let mut guard = ARENA.lock(); - guard.push(Box::new(pkg)); - // SAFETY: ARENA is `'static` (LazyLock); entries are never removed; the - // `Box` heap address is stable across `Vec` reallocation. - // Derive from `&mut **last` so the returned pointer carries mut-provenance. - core::ptr::NonNull::from(&mut **guard.last_mut().unwrap()) - } - - /// Intern tsconfig.json source bytes into the process-lifetime DirInfo arena. - /// `use_shared_buffer = false` at the read site guarantees `Owned`/`Empty`. - fn intern_tsconfig_contents(contents: crate::cache::Contents) -> &'static [u8] { - use crate::cache::Contents; - let owned: Box<[u8]> = match contents { - Contents::Empty => return b"", - Contents::Owned(v) => v.into_boxed_slice(), - // Unreachable for the `parse_tsconfig` caller (use_shared_buffer=false); - // fall back to a copy so we never hand out a dangling slice. - other => Box::from(other.as_slice()), - }; - // `Interned::leak` is the centralized process-lifetime byte-slice store - // (PORTING.md §Forbidden bars open-coded `Box::leak` + `from_raw_parts`; - // `bun_ptr::Interned` is the sanctioned wrapper that consumes the `Box` - // and hands back a proven `&'static [u8]`). - bun_ptr::Interned::leak(owned).as_bytes() - } - - // Port of `const debuglog = Output.scoped(.Resolver, .hidden)` (resolver.zig:4). - // `bun_core::declare_scope!` emits the per-scope `static ScopedLogger`; the - // `debuglog!` macro forwards to the real `bun_core::scoped_log!` so debug builds - // emit and release builds dead-strip (PORTING.md §Logging). - // - // PORT NOTE: resolver.zig:1692 also binds `const dev = Output.scoped(.Resolver, - // .visible)` for `bustDirCache` — same scope name, different visibility. Rust's - // `declare_scope!` is one static per ident; route both through the `.hidden` - // declaration (matches the file-top binding) and let `BUN_DEBUG_Resolver=1` - // surface the bust log. - bun_core::define_scoped_log!(debuglog, Resolver, hidden); - - // PORT NOTE: `Path` in the body is the `'static`-interned variant (paths borrow - // DirnameStore/FilenameStore). Alias here so the ~80 bare-`Path` use sites - // resolve without a per-site lifetime annotation. - type Path = crate::fs::Path<'static>; - type DifferentCase = crate::fs::DifferentCase<'static>; - - use crate::dir_info::HashMapExt as _; - - pub struct SideEffectsData { - pub source: Option>, // TODO(port): lifetime — never instantiated - pub range: bun_ast::Range, - - // If true, "sideEffects" was an array. If false, "sideEffects" was false. - pub is_side_effects_array_in_json: bool, - } - - /// A temporary threadlocal buffer with a lifetime more than the current - /// function call. - /// - /// These used to be individual `threadlocal var x: bun.PathBuffer = undefined` - /// declarations. On Windows each `PathBuffer` is 96 KB (vs 4 KB on POSIX) and - /// PE/COFF has no TLS-BSS, so 25 of them here cost ~2.5 MB of raw zeros in - /// bun.exe and in every thread's TLS block. Grouping them behind a lazily - /// allocated pointer brings that down to 8 bytes. See `bun.ThreadlocalBuffers`. - /// - /// Experimenting with making this one struct instead of a bunch of different - /// threadlocal vars yielded no performance improvement on macOS when bundling - /// 10 copies of Three.js. Potentially revisit after https://github.com/oven-sh/bun/issues/2716 - pub struct Bufs { - pub extension_path: PathBuffer, - pub tsconfig_match_full_buf: PathBuffer, - pub tsconfig_match_full_buf2: PathBuffer, - pub tsconfig_match_full_buf3: PathBuffer, - - pub esm_subpath: [u8; 512], - pub esm_absolute_package_path: PathBuffer, - pub esm_absolute_package_path_joined: PathBuffer, - - // PORT NOTE: Zig left this `= undefined`; `DirEntryResolveQueueItem` holds - // `&'static [u8]` fields, so a zeroed bit-pattern is UB in Rust. Use - // `MaybeUninit` and `assume_init_{ref,mut}` at the (linear write-then-read) - // use sites in `dir_info_cached_maybe_log`. - pub dir_entry_paths_to_resolve: [core::mem::MaybeUninit; 256], - pub open_dirs: [FD; 256], - pub resolve_without_remapping: PathBuffer, - pub index: PathBuffer, - pub dir_info_uncached_filename: PathBuffer, - pub node_bin_path: PathBuffer, - pub dir_info_uncached_path: PathBuffer, - pub tsconfig_base_url: PathBuffer, - pub relative_abs_path: PathBuffer, - pub load_as_file_or_directory_via_tsconfig_base_path: PathBuffer, - pub node_modules_check: PathBuffer, - pub field_abs_path: PathBuffer, - pub tsconfig_path_abs: PathBuffer, - pub check_browser_map: PathBuffer, - pub remap_path: PathBuffer, - pub load_as_file: PathBuffer, - pub remap_path_trailing_slash: PathBuffer, - pub path_in_global_disk_cache: PathBuffer, - pub abs_to_rel: PathBuffer, - pub node_modules_paths_buf: PathBuffer, - pub import_path_for_standalone_module_graph: PathBuffer, - - #[cfg(windows)] - pub win32_normalized_dir_info_cache: [u8; MAX_PATH_BYTES * 2], - #[cfg(not(windows))] - pub win32_normalized_dir_info_cache: (), - } - // TODO(port): bun.ThreadlocalBuffers(Bufs) — lazily-allocated threadlocal Box. - // In Rust we model it as a `thread_local! { static BUFS_PTR: Cell<*mut Bufs> }` - // caching a leaked `Box` pointer (the Box is never freed in Zig either — - // process-lifetime scratch storage). The `bufs!()` macro hands out `&mut` to a - // single field. This relies on the caller never holding two `bufs!()` borrows - // simultaneously across the same field; the Zig code already obeys that invariant. - thread_local! { - static BUFS_PTR: core::cell::Cell<*mut Bufs> = const { core::cell::Cell::new(core::ptr::null_mut()) }; - } - - #[inline(always)] - fn bufs_storage_get() -> *mut Bufs { - // Fast path: single TLS pointer load + null check. `LocalKey>::get` - // (T: Copy) compiles to a plain `__tls_get_addr` + load with no - // RefCell/Option/closure machinery on the hot path (benches: misc/require-fs). - let p = BUFS_PTR.get(); - if !p.is_null() { - return p; - } - bufs_storage_init() - } - - #[cold] - fn bufs_storage_init() -> *mut Bufs { - // SAFETY: every field of `Bufs` is a byte/integer array - // (`PathBuffer` = `[u8; N]`, `[FD; 256]` where `Fd` is a - // `#[repr(C)]` integer newtype, `[MaybeUninit<_>; 256]` which has - // no validity requirement, `()`), so EVERY bit-pattern — not just - // all-zero — is a valid `Bufs`. Zig left these `= undefined`; each - // field is scratch (write-then-read within a single resolve call, - // including `open_dirs` which is bounded by `open_dir_count`), so - // there is no need to pay for zero-filling ~100 KiB on first use. - let p: *mut Bufs = Box::leak(unsafe { Box::::new_uninit().assume_init() }); - BUFS_PTR.set(p); - p - } - - /// `bufs(.field)` → `bufs!(field)` returns `&mut `. - /// // SAFETY: callers must not alias the same field; threadlocal so no cross-thread races. - macro_rules! bufs { - ($field:ident) => { - // SAFETY: threadlocal storage; callers must not alias the same field within one call frame. - unsafe { &mut (*bufs_storage_get()).$field } - }; -} - - pub struct PathPair { - pub primary: Path, - pub secondary: Option, - } - - impl Default for PathPair { - fn default() -> Self { - Self { - primary: Path::empty(), - secondary: None, - } - } - } - - pub struct PathPairIter<'a> { - index: u8, // u2 in Zig - ctx: &'a mut PathPair, - } - - impl<'a> PathPairIter<'a> { - pub fn next(&mut self) -> Option<&mut Path> { - if let Some(path_) = self.next_() { - // SAFETY: reshaped for borrowck — recurse via raw ptr to avoid double &mut. - let p: *mut Path = path_; - unsafe { - if (*p).is_disabled { - return self.next(); - } - return Some(&mut *p); - } - } - None - } - - fn next_(&mut self) -> Option<&mut Path> { - let ind = self.index; - self.index = self.index.saturating_add(1); - - match ind { - 0 => Some(&mut self.ctx.primary), - 1 => self.ctx.secondary.as_mut(), - _ => None, - } - } - } - - impl PathPair { - pub fn iter(&mut self) -> PathPairIter<'_> { - PathPairIter { - ctx: self, - index: 0, - } - } - } - - // Re-export of `bun_ast::SideEffects`. - // Spec: options.zig:884 `Loader.sideEffects()` returns `bun.resolver.SideEffects` - // — the SAME type stored in `Result.primary_side_effects_data`. Re-export so - // `result.primary_side_effects_data = loader.side_effects()` type-checks. - use bun_ast::SideEffects; - - pub struct Result { - pub path_pair: PathPair, - - pub jsx: options::jsx::Pragma, - - pub package_json: Option<*const PackageJSON>, - - pub diff_case: Option>, - - // If present, any ES6 imports to this file can be considered to have no side - // effects. This means they should be removed if unused. - pub primary_side_effects_data: SideEffects, - - // This is the "type" field from "package.json" - pub module_type: options::ModuleType, - - pub debug_meta: Option, - - pub dirname_fd: FD, - pub file_fd: FD, - pub import_kind: ast::ImportKind, - - /// Pack boolean flags to reduce padding overhead. - /// Previously 6 separate bool fields caused ~42+ bytes of padding waste. - pub flags: ResultFlags, - } - - impl Default for Result { - fn default() -> Self { - Self { - path_pair: PathPair::default(), - jsx: options::jsx::Pragma::default(), - package_json: None, - diff_case: None, - primary_side_effects_data: SideEffects::HasSideEffects, - module_type: options::ModuleType::Unknown, - debug_meta: None, - dirname_fd: FD::INVALID, - file_fd: FD::INVALID, - import_kind: ast::ImportKind::Stmt, // Zig: undefined - flags: ResultFlags::default(), - } - } - } - - bitflags::bitflags! { - #[derive(Default, Clone, Copy)] - pub struct ResultFlags: u8 { - const IS_EXTERNAL = 1 << 0; - const IS_EXTERNAL_AND_REWRITE_IMPORT_PATH = 1 << 1; - const IS_STANDALONE_MODULE = 1 << 2; - // This is true when the package was loaded from within the node_modules directory. - const IS_FROM_NODE_MODULES = 1 << 3; - // If true, unused imports are retained in TypeScript code. This matches the - // behavior of the "importsNotUsedAsValues" field in "tsconfig.json" when the - // value is not "remove". - const PRESERVE_UNUSED_IMPORTS_TS = 1 << 4; - const EMIT_DECORATOR_METADATA = 1 << 5; - const EXPERIMENTAL_DECORATORS = 1 << 6; - // _padding: u1 - } - } - - // Convenience accessors mirroring the Zig packed-struct field syntax. - impl ResultFlags { - #[inline] - pub fn is_external(&self) -> bool { - self.contains(Self::IS_EXTERNAL) - } - #[inline] - pub fn set_is_external(&mut self, v: bool) { - self.set(Self::IS_EXTERNAL, v) - } - #[inline] - pub fn is_external_and_rewrite_import_path(&self) -> bool { - self.contains(Self::IS_EXTERNAL_AND_REWRITE_IMPORT_PATH) - } - #[inline] - pub fn set_is_external_and_rewrite_import_path(&mut self, v: bool) { - self.set(Self::IS_EXTERNAL_AND_REWRITE_IMPORT_PATH, v) - } - #[inline] - pub fn is_standalone_module(&self) -> bool { - self.contains(Self::IS_STANDALONE_MODULE) - } - #[inline] - pub fn is_from_node_modules(&self) -> bool { - self.contains(Self::IS_FROM_NODE_MODULES) - } - #[inline] - pub fn set_is_from_node_modules(&mut self, v: bool) { - self.set(Self::IS_FROM_NODE_MODULES, v) - } - #[inline] - pub fn emit_decorator_metadata(&self) -> bool { - self.contains(Self::EMIT_DECORATOR_METADATA) - } - #[inline] - pub fn set_emit_decorator_metadata(&mut self, v: bool) { - self.set(Self::EMIT_DECORATOR_METADATA, v) - } - #[inline] - pub fn experimental_decorators(&self) -> bool { - self.contains(Self::EXPERIMENTAL_DECORATORS) - } - #[inline] - pub fn set_experimental_decorators(&mut self, v: bool) { - self.set(Self::EXPERIMENTAL_DECORATORS, v) - } - } - - pub enum ResultUnion { - Success(Result), - Failure(bun_core::Error), - Pending(PendingResolution), - NotFound, - } - - impl Result { - /// Read-only view of `package_json`. The field stores `Option<*const _>` - /// (rather than `Option<&'static _>`) so [`Default`] / zeroed-init stays - /// bit-valid; callers that only read go through here. Single deref site - /// for the ARENA-backed pointer — same invariant as - /// [`dir_info::DirInfo::package_json`]. - #[inline] - pub fn package_json_ref(&self) -> Option<&'static PackageJSON> { - Self::deref_package_json(self.package_json) - } - - /// Field-value form of [`package_json_ref`] for sites where `self` is - /// already mutably borrowed (e.g. while iterating `path_pair`). Takes the - /// `Copy` field directly so the borrow checker only sees a field read. - #[inline] - pub fn deref_package_json(ptr: Option<*const PackageJSON>) -> Option<&'static PackageJSON> { - // SAFETY: ARENA — every `*const PackageJSON` stored in - // `Result::package_json` is interned in the resolver's process-lifetime - // PackageJSON cache (or a `'static` fallback-module literal); never - // freed while a `Result` is live (see LIFETIMES.tsv). No - // `&mut PackageJSON` is ever materialized concurrently with a read. - ptr.map(|p| unsafe { &*p }) - } - - pub fn path(&mut self) -> Option<&mut Path> { - if !self.path_pair.primary.is_disabled { - return Some(&mut self.path_pair.primary); - } - - if let Some(second) = self.path_pair.secondary.as_mut() { - if !second.is_disabled { - return Some(second); - } - } - - None - } - - pub fn path_const(&self) -> Option<&Path> { - if !self.path_pair.primary.is_disabled { - return Some(&self.path_pair.primary); - } - - if let Some(second) = self.path_pair.secondary.as_ref() { - if !second.is_disabled { - return Some(second); - } - } - - None - } - - // remember: non-node_modules can have package.json - // checking package.json may not be relevant - pub fn is_likely_node_module(&self) -> bool { - let Some(path_) = self.path_const() else { - return false; - }; - self.flags.is_from_node_modules() - || strings::index_of(path_.text(), b"/node_modules/").is_some() - } - - // Most NPM modules are CommonJS - // If unspecified, assume CommonJS. - // If internal app code, assume ESM. - pub fn should_assume_common_js(&self, kind: ast::ImportKind) -> bool { - match self.module_type { - options::ModuleType::Esm => false, - options::ModuleType::Cjs => true, - _ => { - if kind == ast::ImportKind::Require || kind == ast::ImportKind::RequireResolve { - return true; - } - - // If we rely just on isPackagePath, we mess up tsconfig.json baseUrl paths. - self.is_likely_node_module() - } - } - } - - pub fn hash(&self, _: &[u8], _: options::Loader) -> u32 { - let module = self.path_pair.primary.text(); - // SEP_STR ++ "node_modules" ++ SEP_STR - let node_module_root = - const_format::concatcp!(SEP_STR, "node_modules", SEP_STR).as_bytes(); - if let Some(end_) = strings::last_index_of(module, node_module_root) { - let end: usize = end_ + node_module_root.len(); - return bun_wyhash::hash(&module[end..]) as u32; - } - - bun_wyhash::hash(self.path_pair.primary.text()) as u32 - } - } - - pub struct DebugMeta { - pub notes: Vec, - pub suggestion_text: &'static [u8], - pub suggestion_message: &'static [u8], - pub suggestion_range: SuggestionRange, - } - - #[derive(Clone, Copy, PartialEq, Eq)] - pub enum SuggestionRange { - Full, - End, - } - - impl DebugMeta { - pub fn init() -> DebugMeta { - DebugMeta { - notes: Vec::new(), - suggestion_text: b"", - suggestion_message: b"", - suggestion_range: SuggestionRange::Full, - } - } - - pub fn log_error_msg( - &mut self, - log: &mut bun_ast::Log, - source: Option<&bun_ast::Source>, - r: bun_ast::Range, - args: core::fmt::Arguments<'_>, - ) -> core::result::Result<(), bun_core::Error> { - // TODO(port): narrow error set - if source.is_some() && !self.suggestion_message.is_empty() { - let suggestion_range = if self.suggestion_range == SuggestionRange::End { - bun_ast::Range { - loc: bun_ast::Loc { - start: r.end_i() as i32 - 1, - }, - ..Default::default() - } - } else { - r - }; - let data = bun_ast::range_data(source, suggestion_range, self.suggestion_message); - // PORT NOTE: Zig spec writes `data.location.?.suggestion = m.suggestion_text` - // here, but `logger.Location` (logger.zig:73) has no `suggestion` field — - // `logErrorMsg` is uncalled in the Zig source so the field access is never - // type-checked under lazy compilation. Mirror the effective behavior (no-op). - let _ = &self.suggestion_text; - self.notes.push(data); - } - - let mut msg_text = Vec::new(); - write!(&mut msg_text, "{}", args).ok(); - log.add_msg(Msg { - kind: bun_ast::Kind::Err, - data: bun_ast::range_data(source, r, msg_text), - notes: core::mem::take(&mut self.notes).into_boxed_slice(), - ..Default::default() - }); - Ok(()) - } - } - - pub struct DirEntryResolveQueueItem { - pub result: allocators::Result, - // PORT NOTE: `RawSlice` (not `&'static [u8]`) — these point into the - // threadlocal `dir_info_uncached_path` buffer and are consumed before - // `dir_info_cached_maybe_log` returns. `RawSlice` is `repr(transparent)` - // over `*const [u8]` so the bit-level zero-init invariant for `Bufs` is - // unchanged (the array slot is `MaybeUninit`-wrapped), and read sites use - // safe `.slice()` instead of an open-coded raw-ptr deref. - pub unsafe_path: bun_ptr::RawSlice, - pub safe_path: bun_ptr::RawSlice, - pub fd: FD, - } - - impl Default for DirEntryResolveQueueItem { - fn default() -> Self { - Self { - result: allocators::Result { - hash: 0, - index: allocators::NOT_FOUND, - status: allocators::Status::Unknown, - }, - unsafe_path: bun_ptr::RawSlice::EMPTY, - safe_path: bun_ptr::RawSlice::EMPTY, - fd: FD::INVALID, - } - } - } - - // `bun_alloc::Result` doesn't derive Clone (yet); all its fields are Copy, so - // hand-roll Clone here for the queue-item move at `dir_info_cached`. - impl Clone for DirEntryResolveQueueItem { - fn clone(&self) -> Self { - Self { - result: allocators::Result { - hash: self.result.hash, - index: self.result.index, - status: self.result.status, - }, - unsafe_path: self.unsafe_path, - safe_path: self.safe_path, - fd: self.fd, - } - } - } - - pub struct DebugLogs { - pub what: Vec, - pub indent: MutableString, - pub notes: Vec, - } - - #[derive(Clone, Copy, PartialEq, Eq)] - pub enum FlushMode { - Fail, - Success, - } - - impl DebugLogs { - pub fn init() -> core::result::Result { - let mutable = MutableString::init(0)?; - Ok(DebugLogs { - what: Vec::new(), - indent: mutable, - notes: Vec::new(), - }) - } - - // deinit → Drop (only frees `notes`; `indent` deinit was commented out in Zig) - - #[cold] - pub fn increase_indent(&mut self) { - self.indent.append(b" ").expect("unreachable"); - } - - #[cold] - pub fn decrease_indent(&mut self) { - let new_len = self.indent.list.len() - 1; - self.indent.list.truncate(new_len); - } - - #[cold] - pub fn add_note(&mut self, text: Vec) { - let len = self.indent.len(); - let final_text = if len > 0 { - let mut __text = Vec::with_capacity(text.len() + len); - __text.extend_from_slice(self.indent.list.as_slice()); - __text.extend_from_slice(&text); - // d.notes.allocator.free(_text) — drop(text) is implicit - __text - } else { - text - }; - - self.notes - .push(bun_ast::range_data(None, bun_ast::Range::NONE, final_text)); - } - - #[cold] - pub fn add_note_fmt(&mut self, args: core::fmt::Arguments<'_>) { - let mut buf = Vec::new(); - write!(&mut buf, "{}", args).expect("unreachable"); - self.add_note(buf); - } - } - - pub struct MatchResult { - pub path_pair: PathPair, - pub dirname_fd: FD, - pub file_fd: FD, - pub is_node_module: bool, - pub package_json: Option<*const PackageJSON>, - pub diff_case: Option>, - pub dir_info: Option, - pub module_type: options::ModuleType, - pub is_external: bool, - } - - impl Default for MatchResult { - fn default() -> Self { - Self { - path_pair: PathPair::default(), - dirname_fd: FD::INVALID, - file_fd: FD::INVALID, - is_node_module: false, - package_json: None, - diff_case: None, - dir_info: None, - module_type: options::ModuleType::Unknown, - is_external: false, - } - } - } - - pub enum MatchResultUnion { - NotFound, - Success(MatchResult), - Pending(PendingResolution), - Failure(bun_core::Error), - } - - pub struct PendingResolution { - pub esm: crate::package_json::PackageExternal, - pub dependency: Dependency::Version, - pub resolution_id: Install::PackageID, - pub root_dependency_id: Install::DependencyID, - pub import_record_id: u32, - pub string_buf: Vec, - pub tag: PendingResolutionTag, - } - - impl Default for PendingResolution { - fn default() -> Self { - Self { - esm: Default::default(), - dependency: Default::default(), - resolution_id: Install::INVALID_PACKAGE_ID, - root_dependency_id: Install::INVALID_PACKAGE_ID, - import_record_id: u32::MAX, - string_buf: Vec::new(), - tag: PendingResolutionTag::Download, - } - } - } - - pub type PendingResolutionList = MultiArrayList; - - impl PendingResolution { - // PORT NOTE: deinitListItems → Drop on MultiArrayList - // (Zig body only freed `dependency` + `string_buf` per item; both are owned fields with Drop.) - - // deinit → Drop (frees dependency + string_buf; both have Drop) - - pub fn init( - esm: crate::package_json::Package<'_>, - dependency: Dependency::Version, - resolution_id: Install::PackageID, - ) -> core::result::Result { - // PORT NOTE: Zig body called `try esm.copy(allocator)` and left `string_buf` - // / `tag` defaulted; that fn was never compiled (Zig lazy-analyzes unreferenced - // fns). `Package::copy` is the count→allocate→clone Builder dance the live - // call sites open-code, so thread the freshly-allocated buffer into - // `string_buf` here so `Drop` frees what backs the cloned `esm` strings. - let (esm, string_buf) = esm.copy()?; - Ok(PendingResolution { - esm, - dependency, - resolution_id, - string_buf, - ..PendingResolution::default() - }) - } - } - - #[derive(Clone, Copy, PartialEq, Eq)] - pub enum PendingResolutionTag { - Download, - Resolve, - Done, - } - - pub struct LoadResult { - pub path: &'static [u8], // TODO(port): lifetime — interned in dirname_store - pub diff_case: Option>, - pub dirname_fd: FD, - pub file_fd: FD, - pub dir_info: Option, - } - - // This is a global so even if multiple resolvers are created, the mutex will still work - // TODO(port): `bun_threading::Mutex` has no `const fn new()`; use LazyLock until it does. - // `pub(crate)` so the `fs::EntriesMap::inner` debug-assert can verify it is held - // (the resolver mutex is one of the two documented guards for the entries singleton). - pub(crate) static RESOLVER_MUTEX: std::sync::LazyLock = - std::sync::LazyLock::new(Mutex::default); - // Zig had `resolver_Mutex_loaded` to lazily zero-init; Rust const init handles that. - - type BinFolderArray = BoundedArray<&'static [u8], 128>; - // TODO(port): `BoundedArray` has no const constructor; init lazily under - // `BIN_FOLDERS_LOADED` (matches Zig's `bin_folders_loaded` lazy zero-init). - static BIN_FOLDERS: bun_core::RacyCell> = - bun_core::RacyCell::new(core::mem::MaybeUninit::uninit()); - static BIN_FOLDERS_LOCK: std::sync::LazyLock = std::sync::LazyLock::new(Mutex::default); - static BIN_FOLDERS_LOADED: core::sync::atomic::AtomicBool = - core::sync::atomic::AtomicBool::new(false); - - // LAYERING: `AnyResolveWatcher` is the erased vtable the resolver calls to - // register directory watches. The concrete callback lives in `bun_watcher` - // (lower tier); defining the vtable shape there and re-exporting here keeps a - // single type so `Watcher::get_resolve_watcher()` flows directly into - // `Resolver.watcher` without a seam converter. - pub use bun_watcher::AnyResolveWatcher; - - // Zig: `pub fn ResolveWatcher(comptime Context: type, comptime onWatch: anytype) type` — - // type-generator returning a struct with `.init(ctx) -> AnyResolveWatcher` and a - // monomorphized `watch` shim. Per PORTING.md (`fn Foo(comptime T) type` → `struct Foo`). - // - // PORT NOTE: const fn-pointer generics (`adt_const_params` for fn ptrs) and - // const params depending on type params are both forbidden. Reshape to a - // runtime fn-pointer carried alongside the context — `init` produces the same - // `AnyResolveWatcher` erased shim as Zig's monomorphized `wrap`. - - pub struct ResolveWatcher { - on_watch: fn(*mut C, &[u8], FD), - _marker: core::marker::PhantomData<*mut C>, - } - impl ResolveWatcher { - pub const fn new(on_watch: fn(*mut C, &[u8], FD)) -> Self { - Self { - on_watch, - _marker: core::marker::PhantomData, - } - } - pub fn init(self, ctx: *mut C) -> AnyResolveWatcher { - AnyResolveWatcher { - context: ctx.cast(), - // SAFETY: `fn(*mut C, ..)` and `fn(*mut (), ..)` are ABI-identical - // (Rust-ABI, thin-ptr first arg); the `wrap` shim in Zig did the - // same erase. The callback body discharges its own type-recovery. - callback: unsafe { - bun_ptr::cast_fn_ptr::( - self.on_watch, - ) - }, - } - } - } - - pub struct Resolver<'a> { - pub opts: options::BundleOptions, - // PORT NOTE: Zig `fs: *Fs.FileSystem` / `log: *logger.Log` are raw aliasing - // pointers — the bundler builds a `Resolver` per worker thread sharing the - // process-wide `FileSystem` singleton, so `&'a mut` here would manufacture - // aliased unique refs across threads (instant UB). Model as `*mut` and - // deref through the `fs()` / `log()` accessors below. - pub fs: *mut Fs::FileSystem, - pub log: *mut bun_ast::Log, - // allocator dropped — global mimalloc - /// PORT NOTE: Zig stores `[]const []const u8` aliasing into - /// `r.opts.extension_order` and saves/restores it across nested resolves. - /// Stored as a `Copy` enum tag (no self-reference) and resolved on demand - /// via [`Self::extension_order`] / [`options::BundleOptions::ext_order_slice`]. - pub extension_order: options::ExtOrder, - pub timer: Timer, - - pub care_about_bin_folder: bool, - pub care_about_scripts: bool, - - /// Read the "browser" field in package.json files? - /// For Bun's runtime, we don't. - pub care_about_browser_field: bool, - - pub debug_logs: Option, - pub elapsed: u64, // tracing - - pub watcher: Option, - - pub caches: CacheSet, - pub generation: Generation, - - /// Auto-install backend. `bun_install::PackageManager` implements - /// [`AutoInstaller`]; the resolver only sees the trait object so it stays - /// below `bun_install` in the dep graph. The runtime/bundler that enables - /// auto-install (`opts.global_cache != .disable`) is responsible for - /// constructing the `PackageManager` (Zig: `PackageManager.initWithRuntime`) - /// and assigning it here BEFORE resolution; the resolver no longer - /// constructs it lazily — that would require depending on `bun_install`, - /// which depends on us. When `None`, [`get_package_manager`] panics if the - /// auto-install path is reached. - pub package_manager: Option>, - pub on_wake_package_manager: Install::WakeHandler, - // Spec resolver.zig:477 `env_loader: ?*DotEnv.Loader` — raw nullable pointer. - // Stored as `NonNull` (not `&'a Loader`) because the same allocation is - // mutably reborrowed via `Transpiler.env: *mut Loader` after this field is - // set (e.g. bake/production.rs assigns this then calls `configure_defines()` - // → `run_env_loader()` which takes `&mut *self.env`). Holding a live - // `&Loader` across that `&mut Loader` would be aliased-&mut UB; a raw - // pointer carries no aliasing guarantee and matches the Zig shape. - pub env_loader: Option>>, - pub store_fd: bool, - - pub standalone_module_graph: Option<&'a dyn StandaloneModuleGraph>, - - // These are sets that represent various conditions for the "exports" field - // in package.json. - // esm_conditions_default: bun.StringHashMap(bool), - // esm_conditions_import: bun.StringHashMap(bool), - // esm_conditions_require: bun.StringHashMap(bool), - - // A special filtered import order for CSS "@import" imports. - // - // The "resolve extensions" setting determines the order of implicit - // extensions to try when resolving imports with the extension omitted. - // Sometimes people create a JavaScript/TypeScript file and a CSS file with - // the same name when they create a component. At a high level, users expect - // implicit extensions to resolve to the JS file when being imported from JS - // and to resolve to the CSS file when being imported from CSS. - // - // Different bundlers handle this in different ways. Parcel handles this by - // having the resolver prefer the same extension as the importing file in - // front of the configured "resolve extensions" order. Webpack's "css-loader" - // plugin just explicitly configures a special "resolve extensions" order - // consisting of only ".css" for CSS files. - // - // It's unclear what behavior is best here. What we currently do is to create - // a special filtered version of the configured "resolve extensions" order - // for CSS files that filters out any extension that has been explicitly - // configured with a non-CSS loader. This still gives users control over the - // order but avoids the scenario where we match an import in a CSS file to a - // JavaScript-related file. It's probably not perfect with plugins in the - // picture but it's better than some alternatives and probably pretty good. - // atImportExtensionOrder []string - - // This mutex serves two purposes. First of all, it guards access to "dirCache" - // which is potentially mutated during path resolution. But this mutex is also - // necessary for performance. The "React admin" benchmark mysteriously runs - // twice as fast when this mutex is locked around the whole resolve operation - // instead of around individual accesses to "dirCache". For some reason, - // reducing parallelism in the resolver helps the rest of the bundler go - // faster. I'm not sure why this is but please don't change this unless you - // do a lot of testing with various benchmarks and there aren't any regressions. - pub mutex: &'static Mutex, - - /// This cache maps a directory path to information about that directory and - /// all parent directories. When interacting with this structure, make sure - /// to validate your keys with `Resolver.assertValidCacheKey` - // PORT NOTE: Zig `dir_cache: *DirInfo.HashMap` is a raw aliasing pointer to the - // `DirInfo::hash_map_instance()` singleton. Modeled as `*mut` (not `&'static mut`) - // for the same reason as `fs`/`log` above — every per-worker `Resolver` shares the - // singleton, so a `&'static mut` here would manufacture aliased unique refs (UB). - // Deref through the `dir_cache()` accessor below. - pub dir_cache: *mut DirInfo::HashMap, - - /// This is set to false for the runtime. The runtime should choose "main" - /// over "module" in package.json - pub prefer_module_field: bool, - - /// This is an array of paths to resolve against. Used for passing an - /// object '{ paths: string[] }' to `require` and `resolve`; This field - /// is overwritten while the resolution happens. - /// - /// When this is null, it is as if it is set to `&.{ path.dirname(referrer) }`. - pub custom_dir_paths: Option<&'a [bun_core::String]>, - } - - /// RAII guard returned by [`Resolver::scoped_log`]. Restores the previous - /// `Resolver::log` pointer on drop — port of the Zig - /// `defer resolver.log = orig_log` save/restore pattern. - pub struct ResolverLogScope { - slot: *mut *mut bun_ast::Log, - prev: *mut bun_ast::Log, - } - - impl Drop for ResolverLogScope { - #[inline] - fn drop(&mut self) { - // SAFETY: `slot` was derived via `addr_of_mut!` from the raw resolver - // pointer in `scoped_log` (SharedReadWrite provenance); caller contract - // guarantees the resolver outlives this guard. - unsafe { *self.slot = self.prev }; - } - } - - impl<'a> Resolver<'a> { - /// Per-worker constructor — replaces the bundler's prior bitwise - /// `transpiler.* = from.*` (Zig ThreadPool.zig:308) for the resolver - /// portion. Every `Copy` / raw-pointer field is copied from `from`; the - /// per-worker `caches` (the only `Drop`-carrying field, via the - /// `Json` cache's `MimallocArena`) and `debug_logs`/`timer` are freshly - /// constructed so nothing the parent owns is aliased into the worker. - /// - /// `opts` and `log` are supplied by the caller (the worker projects a - /// fresh `BundleOptions` subset and arena-allocates its own `Log`). - /// - /// # Safety - /// `from`'s `standalone_module_graph` / `env_loader` borrow data that - /// outlives the returned resolver (process-lifetime singletons in every - /// caller). The lifetime is widened from `'from` to `'a` here; callers - /// must uphold that the borrowed data outlives `'a`. - pub unsafe fn for_worker( - from: &Resolver<'_>, - log: *mut bun_ast::Log, - opts: options::BundleOptions, - ) -> Resolver<'a> { - Resolver { - opts, - fs: from.fs, - log, - extension_order: from.extension_order, - timer: Timer::start().unwrap_or_else(|_| panic!("Timer fail")), - care_about_bin_folder: from.care_about_bin_folder, - care_about_scripts: from.care_about_scripts, - care_about_browser_field: from.care_about_browser_field, - // `DebugLogs` owns Vecs — per-worker fresh. - debug_logs: None, - elapsed: 0, - watcher: from.watcher, - // Spec ThreadPool.zig:313 `transpiler.resolver.caches = CacheSet.Set.init(allocator)`. - caches: CacheSet::init(), - generation: from.generation, - package_manager: from.package_manager, - on_wake_package_manager: from.on_wake_package_manager.clone(), - // SAFETY: see fn doc — pointee outlives `'a`. - env_loader: from.env_loader.map(|p| p.cast::>()), - store_fd: from.store_fd, - // SAFETY: see fn doc — lifetime-widen the trait-object borrow. The - // vtable layout is identical (only the borrow-checker tag differs); - // a raw-pointer `as`-cast cannot change the `+ 'b` bound, so widen - // via a layout-preserving transmute on the `Option<&dyn>`. - standalone_module_graph: unsafe { - core::mem::transmute::< - Option<&'_ dyn StandaloneModuleGraph>, - Option<&'a dyn StandaloneModuleGraph>, - >(from.standalone_module_graph) - }, - mutex: from.mutex, - dir_cache: from.dir_cache, - prefer_module_field: from.prefer_module_field, - // Transient per-resolve scratch (only set for `require(..., {paths})`); - // never carried across worker init. - custom_dir_paths: None, - } - } - - /// Port of Zig `r.fs` deref. - /// - /// PORT NOTE (Stacked Borrows): returns the RAW `*mut` (NOT `&'a mut`). A - /// `&'a mut` accessor would let two `fs()` calls manufacture coexisting - /// aliased unique refs to the same singleton (PORTING.md §Forbidden: - /// aliased-&mut), and any later `&mut *self.fs` retag would pop a previously - /// returned `&'a mut`'s SB tag while it's still nominally live for `'a`. - /// Callers must `unsafe { &mut *r.fs() }` at the narrowest use site and let - /// the projection die at end-of-expression. Spec resolver.zig:455 stores raw - /// `*Fs.FileSystem` and dereferences per-use. - #[inline(always)] - pub fn fs(&self) -> *mut Fs::FileSystem { - self.fs - } - - /// Shared-borrow of the FileSystem singleton for read-only methods - /// (`abs_buf*`, `normalize_buf`, `dirname_store`, `filename_store`, - /// `top_level_dir`). Preferred over `unsafe { &mut *self.fs() }` whenever - /// the callee takes `&self` — avoids materializing a `&mut FileSystem` - /// that could (under Stacked Borrows) pop a coexisting `rfs_ptr()` / - /// `&mut *query.entry` tag derived from the same allocation. - #[inline(always)] - pub fn fs_ref(&self) -> &Fs::FileSystem { - // SAFETY: BACKREF — `self.fs` is the process-global FileSystem singleton - // (LIFETIMES.tsv: STATIC); resolver mutex serializes all mutation. A - // shared `&` cannot alias-UB with the raw `*mut RealFS` projections - // used elsewhere because no Unique tag is pushed. - unsafe { &*self.fs } - } - - /// Unique-borrow of the `FileSystem` singleton. Centralizes the - /// `unsafe { &mut *self.fs() }` retag for call sites that hold no other - /// borrow of `self` across the call. Sites that need `&mut FileSystem` - /// while also borrowing a disjoint `self.` (e.g. - /// `self.caches.fs.read_file_with_allocator`) cannot route through - /// `&mut self` and continue to narrow-retag via the raw [`fs()`](Self::fs) - /// accessor — same caveat as [`log_mut`](Self::log_mut). - #[inline(always)] - pub fn fs_mut(&mut self) -> &mut Fs::FileSystem { - // SAFETY: BACKREF — `self.fs` is the never-null process-global - // `FileSystem` singleton (set in `init1`); resolver mutex serializes - // all mutation across worker clones; `&mut self` rules out - // intra-instance aliasing. - unsafe { &mut *self.fs } - } - - /// Resolve the current [`options::ExtOrder`] tag to the slice it names - /// inside `self.opts`. Port of Zig `r.extension_order` field read. - #[inline(always)] - pub fn extension_order(&self) -> &[Box<[u8]>] { - self.opts.ext_order_slice(self.extension_order) - } - - /// Raw-pointer projection to the inner `RealFS` (`self.fs.fs`). - /// - /// PORT NOTE (Stacked Borrows): derived directly from the raw `*mut - /// FileSystem` field via `addr_of_mut!` so the resulting `*mut RealFS` - /// carries SharedReadWrite provenance — later `fs_ref()` (Shared) or - /// short-lived `&mut *self.fs()` retags do NOT invalidate it. Callers - /// re-borrow `&mut *self.rfs_ptr()` per use; do not bind a `&mut RealFS` - /// across another `fs()` deref. - #[inline(always)] - pub fn rfs_ptr(&self) -> *mut Fs::file_system::RealFS { - // SAFETY: `self.fs` is the process-global FileSystem singleton; valid - // for the resolver's lifetime. `addr_of_mut!` creates a raw place - // projection without an intermediate `&mut FileSystem`. - unsafe { core::ptr::addr_of_mut!((*self.fs).fs) } - } - - /// Port of Zig `r.log` deref. - /// - /// PORT NOTE (Stacked Borrows): returns RAW `*mut` (see `fs()` note). BACKREF - /// — owner (Transpiler/BundleV2) outlives the Resolver; worker clones share - /// the same Log under the resolver mutex. Caller `unsafe { &mut *r.log() }` - /// at each use site; do not bind the projected `&mut Log` across another - /// `log()` deref. - #[inline(always)] - pub fn log(&self) -> *mut bun_ast::Log { - self.log - } - - /// Temporarily redirect `self.log` to `log`, returning a guard that - /// restores the previous pointer on drop. Port of the Zig - /// `const orig = r.log; r.log = &tmp; defer r.log = orig;` pattern. - /// - /// Takes a raw `*mut Self` (not `&mut self`) so the stored slot pointer - /// carries SharedReadWrite provenance and stays valid under Stacked - /// Borrows when the caller subsequently reborrows the resolver - /// (`read_dir_info` etc.) before the guard drops. - /// - /// # Safety - /// `self_` must point at a `Resolver` that outlives the returned guard, - /// and `log` must remain valid for that same duration (declare the guard - /// *after* the temporary `Log` so it drops first). - #[inline] - pub unsafe fn scoped_log(self_: *mut Self, log: *mut bun_ast::Log) -> ResolverLogScope { - // SAFETY: caller contract — `self_` is live; `addr_of_mut!` projects - // the field place without an intermediate `&mut Resolver`. - let slot = unsafe { core::ptr::addr_of_mut!((*self_).log) }; - // SAFETY: `slot` just derived from a live resolver. - let prev = unsafe { *slot }; - unsafe { *slot = log }; - ResolverLogScope { slot, prev } - } - - /// Shared-borrow of the resolver's `Log` for read-only inspection - /// (e.g. `log.level`). Preferred over `unsafe { &*self.log() }`. - #[inline(always)] - pub fn log_ref(&self) -> &bun_ast::Log { - // SAFETY: BACKREF — `self.log` is never null (set in `init1` / - // `scoped_log`, owner-allocated, outlives the Resolver). Resolver - // mutex serializes mutation; a Shared `&` here pushes no Unique tag - // and so cannot alias-UB with the narrow `log_mut()` retags elsewhere - // (none are live across a `log_ref()` call). - unsafe { &*self.log } - } - - /// Unique-borrow of the resolver's `Log` for `add_*_fmt` / `add_msg`. - /// - /// Centralizes the per-site `unsafe { &mut *self.log() }` retag. `&mut - /// self` rules out two coexisting `&mut Log` from the SAME `Resolver`; - /// cross-clone aliasing (worker copies share the owner's `Log`) is - /// guarded by the resolver `mutex` — same invariant the open-coded sites - /// already relied on. - /// - /// Sites that need `&mut Log` while holding a disjoint `&mut self.` - /// (`flush_debug_logs` ↔ `self.debug_logs`, `parse_tsconfig` ↔ - /// `self.caches.json`) cannot route through `&mut self` and continue to - /// narrow-retag via the raw [`log()`](Self::log) accessor. - #[inline(always)] - pub fn log_mut(&mut self) -> &mut bun_ast::Log { - // SAFETY: BACKREF — `self.log` is never null (set in `init1` / - // `scoped_log`); the pointee (owner-allocated `Log`, or a stack `Log` - // pinned by a live `ResolverLogScope`) outlives every borrow returned - // here. Resolver mutex serializes mutation across worker clones. - unsafe { &mut *self.log } - } - - /// Port of Zig `r.dir_cache` deref. - /// - /// PORT NOTE (Stacked Borrows): returns RAW `*mut` (see `fs()` note). ARENA — - /// `DirInfo::hash_map_instance()` singleton; never freed. Caller - /// `unsafe { &mut *r.dir_cache() }` at each use site. - #[inline(always)] - pub fn dir_cache(&self) -> *mut DirInfo::HashMap { - self.dir_cache - } - - /// Unique-borrow of the `DirInfo` BSSMap singleton. - /// - /// Centralizes the `unsafe { &mut *self.dir_cache() }` retag that every - /// call site previously open-coded. `&mut self` ensures no two coexisting - /// `&mut HashMap` are produced from the SAME `Resolver`; cross-clone - /// aliasing (per-worker `Resolver`s share the singleton) is - /// guarded by the resolver `mutex` — identical invariant to the prior - /// per-site `unsafe`. - /// - /// Stacked Borrows: each call pushes a fresh Unique tag on the BSSMap - /// allocation, so any `*mut DirInfo` previously projected from an earlier - /// `dir_cache_mut()` borrow is popped. Callers that need a slot pointer to - /// survive a subsequent map access must route both through ONE bound - /// `&mut HashMap` (see `dir_info_for_resolution` / `dir_info_cached_maybe_log`). - #[inline(always)] - pub fn dir_cache_mut(&mut self) -> &mut DirInfo::HashMap { - // SAFETY: ARENA — `self.dir_cache` is the never-null - // `DirInfo::hash_map_instance()` static (set in `init1`, never - // reassigned, never freed). Resolver mutex serializes all mutation - // across worker clones; `&mut self` rules out intra-instance aliasing. - unsafe { &mut *self.dir_cache } - } - - /// Port of resolver.zig `getPackageManager`. The Zig spec lazily calls - /// `PackageManager.initWithRuntime` here; in the Rust crate graph that - /// would be a `bun_resolver → bun_install` cycle, so the lazy init is - /// dispatched through the link-time `extern "Rust"` factory - /// [`__bun_resolver_init_package_manager`] (defined `#[no_mangle]` in - /// `bun_install::auto_installer`). The factory performs - /// `HTTPThread.init` + `PackageManager.initWithRuntime` and returns the - /// process-static singleton as a `dyn AutoInstaller`. We then wire - /// `on_wake` and cache the pointer — exactly the Zig body. Reached from - /// the auto-install path (`load_node_modules` global-cache block) when - /// [`use_package_manager`] is `true`. - pub fn get_package_manager(&mut self) -> *mut dyn AutoInstaller { - if let Some(pm) = self.package_manager { - return pm.as_ptr(); - } - // Zig: `bun.HTTPThread.init(&.{}); const pm = PackageManager.initWithRuntime( - // this.log, this.opts.install, bun.default_allocator, .{}, this.env_loader.?);` - let env = self - .env_loader - .expect("Resolver.env_loader must be set before auto-install") - .as_ptr() - // SAFETY: `DotEnv::Loader<'a>` is layout-identical across `'a`; - // `init_with_runtime` only borrows it for the synchronous init - // (the static `PackageManager` retains a raw `NonNull`, - // matching Zig's `*DotEnv.Loader` aliasing). - .cast::(); - // SAFETY: `__bun_resolver_init_package_manager` is defined - // `#[no_mangle]` in `bun_install::auto_installer` and linked into the - // final binary; `self.log` / `self.opts.install` / `env` point at - // process-lifetime storage (Transpiler-owned). The returned pointer - // names the `PackageManager` singleton (`'static`). - let pm: NonNull = - unsafe { __bun_resolver_init_package_manager(self.log, self.opts.install, env) }; - // Zig: `pm.onWake = this.onWakePackageManager;` - // SAFETY: `pm` is the just-initialized singleton; sole `&mut` here. - unsafe { (*pm.as_ptr()).set_on_wake(self.on_wake_package_manager.clone()) }; - self.package_manager = Some(pm); - pm.as_ptr() - } - - /// Safe accessor for the optional [`AutoInstaller`] back-reference. - /// - /// Single `unsafe` deref site for the `package_manager: - /// Option>` field. The pointee is the - /// process-static `PackageManager` singleton (set via - /// [`get_package_manager`](Self::get_package_manager) / - /// `__bun_resolver_init_package_manager`), so it strictly outlives the - /// resolver. `&mut self` ensures the returned `&mut dyn AutoInstaller` is - /// the only live reference for its lifetime. - #[inline] - pub fn auto_installer(&mut self) -> Option<&mut dyn AutoInstaller> { - // SAFETY: BACKREF — `package_manager` names the bun_install-owned - // singleton, live for the resolver's lifetime once installed; `&mut - // self` ⇒ exclusive access to the only Rust handle. - self.package_manager.map(|mut pm| unsafe { pm.as_mut() }) - } - - /// Safe read-only accessor for the optional `DotEnv::Loader` back-reference. - /// - /// Single `unsafe` deref site for the `env_loader: Option>` - /// field. The pointee is the Transpiler-owned loader (set from - /// `transpiler.env`) and strictly outlives the resolver. Only called once - /// resolution has begun (after `run_env_loader()`), so no `&mut Loader` is - /// live concurrently — see the field comment for why this is *not* stored - /// as `Option<&'a Loader>`. - #[inline] - pub fn env_loader(&self) -> Option<&'a DotEnv::Loader<'a>> { - // SAFETY: BACKREF — `env_loader` names the Transpiler-owned - // `DotEnv::Loader`, live for the resolver's lifetime `'a`; resolution - // never mutates the env, so no `&mut Loader` overlaps this shared - // borrow. Returned as `&'a` (not tied to `&self`) so callers may keep - // the env borrow across `&mut self` resolver calls. - self.env_loader.map(|p| unsafe { p.as_ref() }) - } - - #[inline] - pub fn use_package_manager(&self) -> bool { - // TODO(@paperclover): make this configurable. the rationale for disabling - // auto-install in standalone mode is that such executable must either: - // - // - bundle the dependency itself. dynamic `require`/`import` could be - // changed to bundle potential dependencies specified in package.json - // - // - want to load the user's node_modules, which is what currently happens. - // - // auto install, as of writing, is also quite buggy and untested, it always - // installs the latest version regardless of a user's package.json or specifier. - // in addition to being not fully stable, it is completely unexpected to invoke - // a package manager after bundling an executable. if enough people run into - // this, we could implement point 1 - if self.standalone_module_graph.is_some() { - return false; - } - - self.opts.global_cache.is_enabled() - } - - pub fn init1( - log: *mut bun_ast::Log, - _fs: *mut Fs::FileSystem, - opts: options::BundleOptions, - ) -> Self { - // resolver_Mutex_loaded check elided; static is const-inited in Rust. - - let care_about_browser_field = opts.target == options::Target::Browser; - Resolver { - // allocator dropped - // Route through the per-monomorphization singleton so this field and - // `DirInfo::get_parent()` / `get_enclosing_browser_scope()` share storage - // (Zig `BSSMap.init()` is a per-type singleton, not a fresh alloc). - dir_cache: DirInfo::hash_map_instance(), - mutex: &*RESOLVER_MUTEX, - caches: CacheSet::init(), - opts, - timer: Timer::start().unwrap_or_else(|_| panic!("Timer fail")), - fs: _fs, - log, - extension_order: options::ExtOrder::DefaultDefault, - care_about_browser_field, - care_about_bin_folder: false, - care_about_scripts: false, - debug_logs: None, - elapsed: 0, - watcher: None, - generation: 0, - package_manager: None, - on_wake_package_manager: Default::default(), - env_loader: None, - store_fd: false, - standalone_module_graph: None, - prefer_module_field: true, - custom_dir_paths: None, - } - } - - pub fn is_external_pattern(&self, import_path: &[u8]) -> bool { - if self.opts.packages == options::Packages::External && is_package_path(import_path) { - return true; - } - self.matches_user_external_pattern(import_path) - } - - /// True iff `import_path` matches a user-supplied `--external` wildcard - /// pattern. Does NOT consider `packages = external`; use - /// `isExternalPattern` for the combined check. - pub fn matches_user_external_pattern(&self, import_path: &[u8]) -> bool { - for pattern in self.opts.external.patterns.iter() { - if import_path.len() >= pattern.prefix.len() + pattern.suffix.len() - && (import_path.starts_with(pattern.prefix.as_ref()) - && import_path.ends_with(pattern.suffix.as_ref())) - { - return true; - } - } - false - } - - /// Resolves `import_path` via the enclosing tsconfig's `paths`. Returns - /// the `MatchResult` iff a key matches AND the mapped target exists on - /// disk. Used to let path-aliased local files win over `packages=external` - /// without breaking catch-all `"*"` paths entries that only cover ambient - /// type stubs. - pub fn resolve_via_tsconfig_paths( - &mut self, - source_dir: &[u8], - import_path: &[u8], - kind: ast::ImportKind, - ) -> Option { - // SAFETY: PORT — `import_path` is caller-interned (DirnameStore/source text) - // and outlives the returned MatchResult. Zig used raw `[]const u8` here. - // TODO(port): thread an explicit `'a` through MatchResult instead. - let import_path: &'static [u8] = unsafe { &*std::ptr::from_ref::<[u8]>(import_path) }; - if source_dir.is_empty() { - return None; - } - if !bun_paths::is_absolute(source_dir) { - return None; - } - let dir_info = self.dir_info_cached(source_dir).ok().flatten()?; - let tsconfig = dir_info.enclosing_tsconfig_json?; - if tsconfig.paths.count() == 0 { - return None; - } - self.match_tsconfig_paths(tsconfig, import_path, kind) - } - - pub fn flush_debug_logs( - &mut self, - flush_mode: FlushMode, - ) -> core::result::Result<(), bun_core::Error> { - // TODO(port): narrow error set - // PORT NOTE: capture `log` before partially borrowing `self.debug_logs` - // so the method call doesn't conflict with the field borrow (`log()` - // derefs the raw `*mut Log` and is lifetime-decoupled from `&self`). - // SAFETY: BACKREF — `self.log` points at owner-allocated `Log`; disjoint from - // `self.debug_logs` (separate allocation), so the `&mut Log` does not alias the - // `self.debug_logs.as_mut()` borrow below. - let log = unsafe { &mut *self.log() }; - if let Some(debug) = self.debug_logs.as_mut() { - // PORT NOTE: spec resolver.zig:650-658 — only consume `what`/`notes` inside - // the arm that actually emits, so the success-at-non-verbose path touches - // nothing. `add_range_debug_with_notes`/`add_verbose_with_notes` take - // `&'static [u8]`; bypass them and build the `Msg` directly so the Log owns - // the `what` buffer via `Data.text: Cow::Owned` (no `Box::leak`, PORTING.md - // §Forbidden). The `should_print` gate mirrors the bypassed wrappers. - if flush_mode == FlushMode::Fail { - if bun_ast::Kind::Debug.should_print(log.level) { - let what = core::mem::take(&mut debug.what); - let notes = core::mem::take(&mut debug.notes).into_boxed_slice(); - log.add_msg(Msg { - kind: bun_ast::Kind::Debug, - data: bun_ast::range_data( - None, - bun_ast::Range { - loc: bun_ast::Loc::default(), - ..Default::default() - }, - what, - ), - notes, - ..Default::default() - }); - } - } else if (log.level as u32) <= (bun_ast::Level::Verbose as u32) { - if bun_ast::Kind::Verbose.should_print(log.level) { - let what = core::mem::take(&mut debug.what); - let notes = core::mem::take(&mut debug.notes).into_boxed_slice(); - log.add_msg(Msg { - kind: bun_ast::Kind::Verbose, - data: bun_ast::range_data( - None, - bun_ast::Range { - loc: bun_ast::Loc::EMPTY, - ..Default::default() - }, - what, - ), - notes, - ..Default::default() - }); - } - } - } - Ok(()) - } - - // var tracing_start: i128 — unused; dropped. - - pub fn resolve_and_auto_install( - &mut self, - source_dir: &[u8], - import_path: &[u8], - kind: ast::ImportKind, - global_cache: GlobalCache, - ) -> ResultUnion { - // SAFETY: PORT — `import_path` is caller-interned (source text / DirnameStore) - // and outlives the returned Result. Zig used raw `[]const u8` here. - // TODO(port): thread an explicit lifetime through Result instead. - let import_path: &'static [u8] = unsafe { &*std::ptr::from_ref::<[u8]>(import_path) }; - let _tracer = ::bun_perf::trace(::bun_perf::PerfEvent::ModuleResolverResolve); - - // Only setting 'current_action' in debug mode because module resolution - // is done very often, and has a very low crash rate. - // TODO(port): bun.crash_handler.current_action save/restore (Environment.show_crash_trace gated) - #[cfg(debug_assertions)] - let _crash_guard = - ::bun_crash_handler::set_current_action_resolver(source_dir, import_path, kind); - - #[cfg(debug_assertions)] - if bun_core::debug_flags::has_resolve_breakpoint(import_path) { - bun_core::Output::debug(&format_args!( - "Resolving {} from {}", - bstr::BStr::new(import_path), - bstr::BStr::new(source_dir), - )); - // @breakpoint() — no Rust equiv; left as TODO(port) - } - - let original_order = self.extension_order; - // PORT NOTE: Zig `defer r.extension_order = original_order` — reshaped for - // borrowck so the restore happens explicitly at every return point below. - self.extension_order = match kind { - ast::ImportKind::Url | ast::ImportKind::AtConditional | ast::ImportKind::At => { - options::ExtOrder::Css - } - ast::ImportKind::EntryPointBuild - | ast::ImportKind::EntryPointRun - | ast::ImportKind::Stmt - | ast::ImportKind::Dynamic => options::ExtOrder::DefaultEsm, - _ => options::ExtOrder::DefaultDefault, - }; - - if FeatureFlags::TRACING { - self.timer.reset(); - } - - // Spec resolver.zig:703-707: `defer { if (tracing) r.elapsed += r.timer.read() }` - // — fires on EVERY return path. Capture raw field ptrs (Copy) so the closure - // does not hold a `&mut self` borrow across the function body. - let elapsed_ptr: *mut u64 = core::ptr::addr_of_mut!(self.elapsed); - let timer_ptr: *const Timer = core::ptr::addr_of!(self.timer); - scopeguard::defer! { - if FeatureFlags::TRACING { - // SAFETY: `self` outlives this guard (drops at end of fn body); - // `elapsed`/`timer` are not borrowed when the guard fires. - unsafe { *elapsed_ptr += (*timer_ptr).read(); } - } - } - - if self.log_ref().level == bun_ast::Level::Verbose { - if self.debug_logs.is_some() { - // deinit → drop - self.debug_logs = None; - } - self.debug_logs = Some(DebugLogs::init().expect("unreachable")); - } - - if import_path.is_empty() { - self.extension_order = original_order; - return ResultUnion::NotFound; - } - - if self.opts.mark_builtins_as_external { - if import_path.starts_with(b"node:") - || import_path.starts_with(b"bun:") - || HardcodedAlias::has( - import_path, - self.opts.target, - HardcodedAliasCfg { - rewrite_jest_for_tests: self.opts.rewrite_jest_for_tests, - }, - ) - { - self.extension_order = original_order; - return ResultUnion::Success(Result { - import_kind: kind, - path_pair: PathPair { - primary: Path::init(import_path), - secondary: None, - }, - module_type: options::ModuleType::Cjs, - primary_side_effects_data: SideEffects::NoSideEffectsPureData, - flags: ResultFlags::IS_EXTERNAL, - ..Default::default() - }); - } - } - - // #29590: a tsconfig `paths` key can look bare (e.g. "@/*") and - // otherwise collide with `packages=external + isPackagePath`. Try - // the alias first, but only follow it when it actually resolves to - // a file on disk — a catch-all `"*": ["./types/*"]` for ambient - // .d.ts stubs must still let real bare imports stay external. - if kind != ast::ImportKind::EntryPointBuild - && kind != ast::ImportKind::EntryPointRun - && self.opts.packages == options::Packages::External - && is_package_path(import_path) - && !self.matches_user_external_pattern(import_path) - { - if let Some(res) = self.resolve_via_tsconfig_paths(source_dir, import_path, kind) { - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note(b"Resolved via tsconfig.json \"paths\" before applying packages=external".to_vec()); - } - let _ = self.flush_debug_logs(FlushMode::Success); - self.extension_order = original_order; - return ResultUnion::Success(Result { - import_kind: kind, - path_pair: res.path_pair, - diff_case: res.diff_case, - package_json: res.package_json, - dirname_fd: res.dirname_fd, - file_fd: res.file_fd, - jsx: self.opts.jsx.clone(), - ..Default::default() - }); - } - } - - // Certain types of URLs default to being external for convenience, - // while these rules should not be applied to the entrypoint as it is never external (#12734) - if kind != ast::ImportKind::EntryPointBuild - && kind != ast::ImportKind::EntryPointRun - && (self.is_external_pattern(import_path) - // "fill: url(#filter);" - || (kind.is_from_css() && import_path.starts_with(b"#")) - // "background: url(http://example.com/images/image.png);" - || import_path.starts_with(b"http://") - // "background: url(https://example.com/images/image.png);" - || import_path.starts_with(b"https://") - // "background: url(//example.com/images/image.png);" - || import_path.starts_with(b"//")) - { - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note(b"Marking this path as implicitly external".to_vec()); - } - let _ = self.flush_debug_logs(FlushMode::Success); - - self.extension_order = original_order; - return ResultUnion::Success(Result { - import_kind: kind, - path_pair: PathPair { - primary: Path::init(import_path), - secondary: None, - }, - module_type: if !kind.is_from_css() { - options::ModuleType::Esm - } else { - options::ModuleType::Unknown - }, - flags: ResultFlags::IS_EXTERNAL, - ..Default::default() - }); - } - - match DataURL::parse(import_path) { - Err(_) => { - self.extension_order = original_order; - return ResultUnion::Failure(bun_core::err!("InvalidDataURL")); - } - Ok(Some(data_url)) => { - // "import 'data:text/javascript,console.log(123)';" - // "@import 'data:text/css,body{background:white}';" - let mime = data_url.decode_mime_type(); - use ::bun_http_types::MimeType::Category; - if matches!( - mime.category, - Category::Javascript | Category::Css | Category::Json | Category::Text - ) { - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note( - b"Putting this path in the \"dataurl\" namespace".to_vec(), - ); - } - let _ = self.flush_debug_logs(FlushMode::Success); - - self.extension_order = original_order; - return ResultUnion::Success(Result { - path_pair: PathPair { - primary: Path::init_with_namespace(import_path, b"dataurl"), - secondary: None, - }, - ..Default::default() - }); - } - - // "background: url(data:image/png;base64,iVBORw0KGgo=);" - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note(b"Marking this \"dataurl\" as external".to_vec()); - } - let _ = self.flush_debug_logs(FlushMode::Success); - - self.extension_order = original_order; - return ResultUnion::Success(Result { - path_pair: PathPair { - primary: Path::init_with_namespace(import_path, b"dataurl"), - secondary: None, - }, - flags: ResultFlags::IS_EXTERNAL, - ..Default::default() - }); - } - Ok(None) => {} - } - - // When using `bun build --compile`, module resolution is never - // relative to our special /$bunfs/ directory. - // - // It's always relative to the current working directory of the project root. - // - // ...unless you pass a relative path that exists in the standalone module graph executable. - let mut source_dir_resolver = bun_paths::PosixToWinNormalizer::default(); - let source_dir_normalized: &[u8] = 'brk: { - if let Some(graph) = self.standalone_module_graph { - if ::bun_options_types::standalone_path::is_bun_standalone_file_path( - import_path, - ) { - if graph.find_assume_standalone_path(import_path).is_some() { - self.extension_order = original_order; - return ResultUnion::Success(Result { - import_kind: kind, - path_pair: PathPair { - primary: Path::init(import_path), - secondary: None, - }, - module_type: options::ModuleType::Esm, - flags: ResultFlags::IS_STANDALONE_MODULE, - ..Default::default() - }); - } - - self.extension_order = original_order; - return ResultUnion::NotFound; - } else if ::bun_options_types::standalone_path::is_bun_standalone_file_path( - source_dir, - ) { - if import_path.len() > 2 && is_dot_slash(&import_path[0..2]) { - let buf = bufs!(import_path_for_standalone_module_graph); - let joined = bun_paths::join_abs_string_buf( - source_dir, - buf, - &[import_path], - bun_paths::Platform::Loose, - ); - - // Support relative paths in the graph - if let Some(file_name) = graph.find_assume_standalone_path(joined) { - // Intern: trait borrows into the graph; `Path::init` - // needs `'static` (DirnameStore-backed). - let file_name = Fs::file_system::DirnameStore::instance() - .append_slice(file_name) - .expect("unreachable"); - self.extension_order = original_order; - return ResultUnion::Success(Result { - import_kind: kind, - path_pair: PathPair { - primary: Path::init(file_name), - secondary: None, - }, - module_type: options::ModuleType::Esm, - flags: ResultFlags::IS_STANDALONE_MODULE, - ..Default::default() - }); - } - } - break 'brk Fs::FileSystem::instance().top_level_dir; - } - } - - // Fail now if there is no directory to resolve in. This can happen for - // virtual modules (e.g. stdin) if a resolve directory is not specified. - // - // TODO: This is skipped for now because it is impossible to set a - // resolveDir so we default to the top level directory instead (this - // is backwards compat with Bun 1.0 behavior) - // See https://github.com/oven-sh/bun/issues/8994 for more details. - if source_dir.is_empty() { - // if let Some(debug) = self.debug_logs.as_mut() { - // debug.add_note(b"Cannot resolve this path without a directory".to_vec()); - // let _ = self.flush_debug_logs(FlushMode::Fail); - // } - // return ResultUnion::Failure(bun_core::err!("MissingResolveDir")); - break 'brk Fs::FileSystem::instance().top_level_dir; - } - - // This can also be hit if you use plugins with non-file namespaces, - // or call the module resolver from javascript (Bun.resolveSync) - // with a faulty parent specifier. - if !bun_paths::is_absolute(source_dir) { - // if let Some(debug) = self.debug_logs.as_mut() { - // debug.add_note(b"Cannot resolve this path without an absolute directory".to_vec()); - // let _ = self.flush_debug_logs(FlushMode::Fail); - // } - // return ResultUnion::Failure(bun_core::err!("InvalidResolveDir")); - break 'brk Fs::FileSystem::instance().top_level_dir; - } - - break 'brk source_dir_resolver - .resolve_cwd(source_dir) - .unwrap_or_else(|_| panic!("Failed to query CWD")); - }; - - // r.mutex.lock(); - // defer r.mutex.unlock(); - // errdefer (r.flushDebugLogs(.fail) catch {}) — handled at each error return below - - // A path with a null byte cannot exist on the filesystem. Continuing - // anyways would cause assertion failures. - if strings::index_of_char(import_path, 0).is_some() { - let _ = self.flush_debug_logs(FlushMode::Fail); - self.extension_order = original_order; - return ResultUnion::NotFound; - } - - let mut tmp = self.resolve_without_symlinks( - source_dir_normalized, - import_path, - kind, - global_cache, - ); - - // Fragments in URLs in CSS imports are technically expected to work - if matches!(tmp, ResultUnion::NotFound) && kind.is_from_css() { - 'try_without_suffix: { - // If resolution failed, try again with the URL query and/or hash removed - let maybe_suffix = strings::index_of_any(import_path, b"?#"); - let Some(suffix) = maybe_suffix else { - break 'try_without_suffix; - }; - if suffix < 1 { - break 'try_without_suffix; - } - - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Retrying resolution after removing the suffix {}", - bstr::BStr::new(&import_path[suffix..]) - )); - } - let result2 = self.resolve_without_symlinks( - source_dir_normalized, - &import_path[0..suffix], - kind, - global_cache, - ); - if matches!(result2, ResultUnion::NotFound) { - break 'try_without_suffix; - } - tmp = result2; - } - } - - let ret = match tmp { - ResultUnion::Success(mut result) => { - if result.path_pair.primary.namespace() != b"node" - && !result.flags.is_standalone_module() - { - if let Err(err) = self.finalize_result(&mut result, kind) { - self.extension_order = original_order; - return ResultUnion::Failure(err); - } - } - - let _ = self.flush_debug_logs(FlushMode::Success); - result.import_kind = kind; - if cfg!(feature = "debug_logs") { - // TODO(port): debuglog! with bun.fmt.fmtPath formatting - } - ResultUnion::Success(result) - } - ResultUnion::Failure(e) => { - let _ = self.flush_debug_logs(FlushMode::Fail); - ResultUnion::Failure(e) - } - ResultUnion::Pending(pending) => { - let _ = self.flush_debug_logs(FlushMode::Fail); - ResultUnion::Pending(pending) - } - ResultUnion::NotFound => { - let _ = self.flush_debug_logs(FlushMode::Fail); - ResultUnion::NotFound - } - }; - - // (tracing `elapsed` accumulation handled by `_elapsed_guard` above on all paths) - self.extension_order = original_order; - ret - } - - pub fn resolve( - &mut self, - source_dir: &[u8], - import_path: &[u8], - kind: ast::ImportKind, - ) -> core::result::Result { - // TODO(port): narrow error set - match self.resolve_and_auto_install(source_dir, import_path, kind, GlobalCache::disable) - { - ResultUnion::Success(result) => Ok(result), - ResultUnion::Pending(_) | ResultUnion::NotFound => { - Err(bun_core::err!("ModuleNotFound")) - } - ResultUnion::Failure(e) => Err(e), - } - } - - /// Runs a resolution but also checking if a Bun Bake framework has an - /// override. This is used in one place in the bundler. - pub fn resolve_with_framework( - &mut self, - source_dir: &[u8], - import_path: &[u8], - kind: ast::ImportKind, - ) -> core::result::Result { - // SAFETY: PORT — `import_path` is caller-interned (source text / DirnameStore) - // and outlives the returned Result. TODO(port): thread explicit lifetime. - let import_path: &'static [u8] = unsafe { &*std::ptr::from_ref::<[u8]>(import_path) }; - // TODO(port): narrow error set - if let Some(f) = self.opts.framework.as_ref() { - if let Some(mod_) = f.built_in_modules.get(import_path) { - match mod_ { - // TYPE_ONLY(b0): BuiltInModule relocated bun_runtime::bake::framework → bun_options_types (T3) - bun_options_types::BuiltInModule::Code(_) => { - return Ok(Result { - import_kind: kind, - path_pair: PathPair { - primary: Fs::Path::init_with_namespace(import_path, b"node"), - secondary: None, - }, - module_type: options::ModuleType::Esm, - primary_side_effects_data: SideEffects::NoSideEffectsPureData, - flags: ResultFlags::default(), - ..Default::default() - }); - } - bun_options_types::BuiltInModule::Import(path) => { - // PORT NOTE: copy out `path` so the `&self.opts.framework` borrow - // ends before `self.resolve(&mut self, ...)`. - let path: &'static [u8] = - unsafe { &*std::ptr::from_ref::<[u8]>(path.as_ref()) }; - let top = self.fs_ref().top_level_dir; - return self.resolve(top, path, ast::ImportKind::EntryPointBuild); - } - } - // unreachable in Zig (return after switch) - } - } - self.resolve(source_dir, import_path, kind) - } - - pub fn finalize_result( - &mut self, - result: &mut Result, - kind: ast::ImportKind, - ) -> core::result::Result<(), bun_core::Error> { - // TODO(port): narrow error set - if result.flags.is_external() { - return Ok(()); - } - - let mut iter = result.path_pair.iter(); - let mut module_type = result.module_type; - while let Some(path) = iter.next() { - let Ok(Some(dir)) = self.read_dir_info(path.name.dir) else { - continue; - }; - let mut needs_side_effects = true; - if let Some(existing) = Result::deref_package_json(result.package_json) { - // if we don't have it here, they might put it in a sideEfffects - // map of the parent package.json - // TODO: check if webpack also does this parent lookup - use crate::package_json::SideEffects as PJSideEffects; - needs_side_effects = matches!( - existing.side_effects, - PJSideEffects::Unspecified - | PJSideEffects::Glob(_) - | PJSideEffects::Mixed(_) - ); - - result.primary_side_effects_data = match &existing.side_effects { - PJSideEffects::Unspecified => SideEffects::HasSideEffects, - PJSideEffects::False => SideEffects::NoSideEffectsPackageJson, - PJSideEffects::Map(map) => { - if map.contains_key( - &crate::package_json::StringHashMapUnownedKey::init(path.text()), - ) { - SideEffects::HasSideEffects - } else { - SideEffects::NoSideEffectsPackageJson - } - } - PJSideEffects::Glob(_) => { - if existing.side_effects.has_side_effects(path.text()) { - SideEffects::HasSideEffects - } else { - SideEffects::NoSideEffectsPackageJson - } - } - PJSideEffects::Mixed(_) => { - if existing.side_effects.has_side_effects(path.text()) { - SideEffects::HasSideEffects - } else { - SideEffects::NoSideEffectsPackageJson - } - } - }; - - if existing.name.is_empty() || self.care_about_bin_folder { - result.package_json = None; - } - } - - result.package_json = result - .package_json - .or(dir.enclosing_package_json.map(|p| std::ptr::from_ref(p))); - - if needs_side_effects { - if let Some(package_json) = Result::deref_package_json(result.package_json) { - use crate::package_json::SideEffects as PJSideEffects; - result.primary_side_effects_data = match &package_json.side_effects { - PJSideEffects::Unspecified => SideEffects::HasSideEffects, - PJSideEffects::False => SideEffects::NoSideEffectsPackageJson, - PJSideEffects::Map(map) => { - if map.contains_key( - &crate::package_json::StringHashMapUnownedKey::init( - path.text(), - ), - ) { - SideEffects::HasSideEffects - } else { - SideEffects::NoSideEffectsPackageJson - } - } - PJSideEffects::Glob(_) => { - if package_json.side_effects.has_side_effects(path.text()) { - SideEffects::HasSideEffects - } else { - SideEffects::NoSideEffectsPackageJson - } - } - PJSideEffects::Mixed(_) => { - if package_json.side_effects.has_side_effects(path.text()) { - SideEffects::HasSideEffects - } else { - SideEffects::NoSideEffectsPackageJson - } - } - }; - } - } - - if let Some(tsconfig) = dir.enclosing_tsconfig_json { - result.jsx = tsconfig.merge_jsx(result.jsx.clone()); - result.flags.set_emit_decorator_metadata( - result.flags.emit_decorator_metadata() || tsconfig.emit_decorator_metadata, - ); - result.flags.set_experimental_decorators( - result.flags.experimental_decorators() || tsconfig.experimental_decorators, - ); - } - - // If you use mjs or mts, then you're using esm - // If you use cjs or cts, then you're using cjs - // This should win out over the module type from package.json - if !kind.is_from_css() - && module_type == options::ModuleType::Unknown - && path.name.ext.len() == 4 - { - module_type = - module_type_from_ext(path.name.ext).unwrap_or(options::ModuleType::Unknown); - } - - if let Some(entries) = dir.get_entries_ref(self.generation) { - if let Some(query) = entries.get(path.name.filename) { - let symlink_path = query.entry().symlink(self.rfs_ptr(), self.store_fd); - if !symlink_path.is_empty() { - path.set_realpath(symlink_path); - if !result.file_fd.is_valid() { - result.file_fd = query.entry().cache().fd; - } - - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Resolved symlink \"{}\" to \"{}\"", - bstr::BStr::new(path.text()), - bstr::BStr::new(symlink_path) - )); - } - } else if !dir.abs_real_path.is_empty() { - // When the directory is a symlink, we don't need to call getFdPath. - let parts = [dir.abs_real_path.as_ref(), query.entry().base()]; - let mut buf = bun_paths::PathBuffer::uninit(); - - // PORT NOTE: `abs_buf` returns a borrow of `buf`; capture only the - // length so `buf` can be re-borrowed for null-termination below. - let out_len = self.fs_ref().abs_buf(&parts, &mut buf).len(); - - let store_fd = self.store_fd; - - if !query.entry().cache().fd.is_valid() && store_fd { - buf[out_len] = 0; - // SAFETY: buf[out_len] == 0 written above - let span = bun_core::ZStr::from_buf(&buf[..], out_len); - // Spec resolver.zig:1099 uses `try std.fs.openFileAbsoluteZ`, - // which propagates I/O errors so `resolveAndAutoInstall` can - // return them as `Result.Union.failure`. Mirror that — never - // panic on EACCES/EMFILE/ELOOP here. - let file = bun_sys::open(span, bun_sys::O::RDONLY, 0) - .map_err(Into::::into)?; - query.entry().set_cache_fd(file); - Fs::FileSystem::set_max_fd(file.native()); - } - - // PORT NOTE: snapshot `need_to_close_files` and raw-ptr the entry so - // the closure captures only Copy values — keeps `self` and - // `query.entry` reborrowable across the guard's lifetime. - let need_close = self.fs_ref().fs.need_to_close_files(); - // ARENA — Entry lives in the BSSMap singleton; guard runs before - // the slot is reused (resolver mutex held). Capture as `BackRef` - // (Copy, Deref) so the closure stays Copy-only while the read is - // a safe `BackRef::get()` instead of a raw-ptr deref. - let entry_ref = bun_ptr::BackRef::::from( - core::ptr::NonNull::new(query.entry).expect("EntryStore slot"), - ); - scopeguard::defer! { - if need_close { - let e = entry_ref.get(); - let fd = e.cache().fd; - if fd.is_valid() { - fd.close(); - e.set_cache_fd(FD::INVALID); - } - } - } - - let symlink = - Fs::FilenameStore::instance().append_slice(&buf[..out_len])?; - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Resolved symlink \"{}\" to \"{}\"", - bstr::BStr::new(symlink), - bstr::BStr::new(path.text()) - )); - } - query.entry().set_cache_symlink(PathString::init(symlink)); - if !result.file_fd.is_valid() && store_fd { - result.file_fd = query.entry().cache().fd; - } - - path.set_realpath(symlink); - } - } - } - } - - if !kind.is_from_css() && module_type == options::ModuleType::Unknown { - if let Some(pkg) = result.package_json_ref() { - module_type = pkg.module_type; - } - } - - result.module_type = module_type; - Ok(()) - } - - pub fn resolve_without_symlinks( - &mut self, - source_dir: &[u8], - input_import_path: &'static [u8], - kind: ast::ImportKind, - global_cache: GlobalCache, - ) -> ResultUnion { - debug_assert!(bun_paths::is_absolute(source_dir)); - - let mut import_path = input_import_path; - - // This implements the module resolution algorithm from node.js, which is - // described here: https://nodejs.org/api/modules.html#modules_all_together - let mut result = Result { - path_pair: PathPair { - primary: Path::empty(), - secondary: None, - }, - jsx: self.opts.jsx.clone(), - ..Default::default() - }; - - // Return early if this is already an absolute path. In addition to asking - // the file system whether this is an absolute path, we also explicitly check - // whether it starts with a "/" and consider that an absolute path too. This - // is because relative paths can technically start with a "/" on Windows - // because it's not an absolute path on Windows. Then people might write code - // with imports that start with a "/" that works fine on Windows only to - // experience unexpected build failures later on other operating systems. - // Treating these paths as absolute paths on all platforms means Windows - // users will not be able to accidentally make use of these paths. - if bun_paths::is_absolute(import_path) { - // Collapse relative directory specifiers if they exist. Extremely - // loose check to avoid always doing this copy, but avoid spending - // too much time on the check. - if strings::index_of(import_path, b"..").is_some() { - let platform = bun_paths::Platform::AUTO; - let ends_with_dir = platform.is_separator(import_path[import_path.len() - 1]) - || (import_path.len() > 3 - && platform.is_separator(import_path[import_path.len() - 3]) - && import_path[import_path.len() - 2] == b'.' - && import_path[import_path.len() - 1] == b'.'); - let buf = bufs!(relative_abs_path); - let Some(abs) = self.fs_ref().abs_buf_checked(&[import_path], buf) else { - return ResultUnion::NotFound; - }; - let mut len = abs.len(); - if ends_with_dir { - buf[len] = platform.separator(); - len += 1; - } - // `bufs!` hands out an unconstrained-lifetime `&mut PathBuffer` - // (threadlocal storage); a safe reborrow satisfies `&'static [u8]`. - import_path = &buf[..len]; - } - - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "The import \"{}\" is being treated as an absolute path", - bstr::BStr::new(import_path) - )); - } - - // First, check path overrides from the nearest enclosing TypeScript "tsconfig.json" file - if let Ok(Some(dir_info)) = self.dir_info_cached(source_dir) { - if let Some(tsconfig) = dir_info.enclosing_tsconfig_json { - if tsconfig.paths.count() > 0 { - if let Some(res) = - self.match_tsconfig_paths(tsconfig, import_path, kind) - { - // We don't set the directory fd here because it might remap an entirely different directory - return ResultUnion::Success(Result { - path_pair: res.path_pair, - diff_case: res.diff_case, - package_json: res.package_json, - dirname_fd: res.dirname_fd, - file_fd: res.file_fd, - jsx: tsconfig.merge_jsx(result.jsx), - ..Default::default() - }); - } - } - } - } - - if self.opts.external.abs_paths.count() > 0 - && self.opts.external.abs_paths.contains(import_path) - { - // If the string literal in the source text is an absolute path and has - // been marked as an external module, mark it as *not* an absolute path. - // That way we preserve the literal text in the output and don't generate - // a relative path from the output directory to that path. - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "The path \"{}\" is marked as external by the user", - bstr::BStr::new(import_path) - )); - } - - return ResultUnion::Success(Result { - path_pair: PathPair { - primary: Path::init(import_path), - secondary: None, - }, - flags: ResultFlags::IS_EXTERNAL, - ..Default::default() - }); - } - - // Run node's resolution rules (e.g. adding ".js") - let mut normalizer = ResolvePath::PosixToWinNormalizer::default(); - if let Some(entry) = self - .load_as_file_or_directory(normalizer.resolve(source_dir, import_path), kind) - { - return ResultUnion::Success(Result { - dirname_fd: entry.dirname_fd, - path_pair: entry.path_pair, - diff_case: entry.diff_case, - package_json: entry.package_json, - file_fd: entry.file_fd, - jsx: self.opts.jsx.clone(), - ..Default::default() - }); - } - - return ResultUnion::NotFound; - } - - // Check both relative and package paths for CSS URL tokens, with relative - // paths taking precedence over package paths to match Webpack behavior. - let is_package_path_ = - kind != ast::ImportKind::EntryPointRun && is_package_path_not_absolute(import_path); - let check_relative = !is_package_path_ || kind.is_from_css(); - let check_package = is_package_path_; - - if check_relative { - if let Some(custom_paths) = self.custom_dir_paths { - // @branchHint(.unlikely) - bun_core::hint::cold(); - for custom_path in custom_paths { - let custom_utf8 = custom_path.to_utf8_without_ref(); - match self.check_relative_path( - custom_utf8.slice(), - import_path, - kind, - global_cache, - ) { - ResultUnion::Success(res) => return ResultUnion::Success(res), - ResultUnion::Pending(p) => return ResultUnion::Pending(p), - ResultUnion::Failure(p) => return ResultUnion::Failure(p), - ResultUnion::NotFound => {} - } - } - debug_assert!(!check_package); // always from JavaScript - return ResultUnion::NotFound; // bail out now since there isn't anywhere else to check - } else { - match self.check_relative_path(source_dir, import_path, kind, global_cache) { - ResultUnion::Success(res) => return ResultUnion::Success(res), - ResultUnion::Pending(p) => return ResultUnion::Pending(p), - ResultUnion::Failure(p) => return ResultUnion::Failure(p), - ResultUnion::NotFound => {} - } - } - } - - if check_package { - if self.opts.polyfill_node_globals { - let had_node_prefix = import_path.starts_with(b"node:"); - let import_path_without_node_prefix = if had_node_prefix { - &import_path[b"node:".len()..] - } else { - import_path - }; - - if let Some(fallback_module) = - NodeFallbackModules::map().get(import_path_without_node_prefix) - { - result.path_pair.primary = fallback_module.path.clone(); - result.module_type = options::ModuleType::Cjs; - // @ptrFromInt(@intFromPtr(...)) — cast away constness - result.package_json = Some(std::ptr::from_ref::( - fallback_module.package_json, - )); - result.flags.set_is_from_node_modules(true); - return ResultUnion::Success(result); - } - - if had_node_prefix { - // Module resolution fails automatically for unknown node builtins - if !HardcodedAlias::has( - import_path_without_node_prefix, - options::Target::Node, - HardcodedAliasCfg::default(), - ) { - return ResultUnion::NotFound; - } - - // Valid node:* modules becomes {} in the output - result.path_pair.primary.namespace = b"node"; - result.path_pair.primary.text = import_path_without_node_prefix; - result.path_pair.primary.name = - Fs::PathName::init(import_path_without_node_prefix); - result.module_type = options::ModuleType::Cjs; - result.path_pair.primary.is_disabled = true; - result.flags.set_is_from_node_modules(true); - result.primary_side_effects_data = SideEffects::NoSideEffectsPureData; - return ResultUnion::Success(result); - } - - // Always mark "fs" as disabled, matching Webpack v4 behavior - if import_path_without_node_prefix.starts_with(b"fs") - && (import_path_without_node_prefix.len() == 2 - || import_path_without_node_prefix[2] == b'/') - { - result.path_pair.primary.namespace = b"node"; - result.path_pair.primary.text = import_path_without_node_prefix; - result.path_pair.primary.name = - Fs::PathName::init(import_path_without_node_prefix); - result.module_type = options::ModuleType::Cjs; - result.path_pair.primary.is_disabled = true; - result.flags.set_is_from_node_modules(true); - result.primary_side_effects_data = SideEffects::NoSideEffectsPureData; - return ResultUnion::Success(result); - } - } - - // Check for external packages first - if self.opts.external.node_modules.count() > 0 - // Imports like "process/" need to resolve to the filesystem, not a builtin - && !import_path.ends_with(b"/") - { - let mut query = import_path; - loop { - if self.opts.external.node_modules.contains(query) { - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "The path \"{}\" was marked as external by the user", - bstr::BStr::new(query) - )); - } - return ResultUnion::Success(Result { - path_pair: PathPair { - primary: Path::init(query), - secondary: None, - }, - flags: ResultFlags::IS_EXTERNAL, - ..Default::default() - }); - } - - // If the module "foo" has been marked as external, we also want to treat - // paths into that module such as "foo/bar" as external too. - let Some(slash) = strings::last_index_of_char(query, b'/') else { - break; - }; - query = &query[0..slash]; - } - } - - if let Some(custom_paths) = self.custom_dir_paths { - bun_core::hint::cold(); - for custom_path in custom_paths { - let custom_utf8 = custom_path.to_utf8_without_ref(); - match self.check_package_path( - custom_utf8.slice(), - import_path, - kind, - global_cache, - ) { - ResultUnion::Success(res) => return ResultUnion::Success(res), - ResultUnion::Pending(p) => return ResultUnion::Pending(p), - ResultUnion::Failure(p) => return ResultUnion::Failure(p), - ResultUnion::NotFound => {} - } - } - } else { - match self.check_package_path(source_dir, import_path, kind, global_cache) { - ResultUnion::Success(res) => return ResultUnion::Success(res), - ResultUnion::Pending(p) => return ResultUnion::Pending(p), - ResultUnion::Failure(p) => return ResultUnion::Failure(p), - ResultUnion::NotFound => {} - } - } - } - - ResultUnion::NotFound - } - - pub fn check_relative_path( - &mut self, - source_dir: &[u8], - import_path: &[u8], - kind: ast::ImportKind, - global_cache: GlobalCache, - ) -> ResultUnion { - let Some(abs_path) = self - .fs_ref() - .abs_buf_checked(&[source_dir, import_path], bufs!(relative_abs_path)) - else { - return ResultUnion::NotFound; - }; - - if self.opts.external.abs_paths.count() > 0 - && self.opts.external.abs_paths.contains(abs_path) - { - // If the string literal in the source text is an absolute path and has - // been marked as an external module, mark it as *not* an absolute path. - // That way we preserve the literal text in the output and don't generate - // a relative path from the output directory to that path. - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "The path \"{}\" is marked as external by the user", - bstr::BStr::new(abs_path) - )); - } - - return ResultUnion::Success(Result { - path_pair: PathPair { - primary: Path::init( - self.fs_ref() - .dirname_store - .append_slice(abs_path) - .expect("oom"), - ), - secondary: None, - }, - flags: ResultFlags::IS_EXTERNAL, - ..Default::default() - }); - } - - // Check the "browser" map - if self.care_about_browser_field { - let dirname = bun_paths::dirname(abs_path).expect("unreachable"); - if let Ok(Some(import_dir_info_outer)) = self.dir_info_cached(dirname) { - if let Some(import_dir_info) = - import_dir_info_outer.get_enclosing_browser_scope() - { - let pkg = import_dir_info.package_json().unwrap(); - if let Some(remap) = self - .check_browser_map::<{ BrowserMapPathKind::AbsolutePath }>( - &import_dir_info, - abs_path, - ) - { - // Is the path disabled? - if remap.is_empty() { - let mut _path = Path::init( - self.fs_ref() - .dirname_store - .append_slice(abs_path) - .expect("unreachable"), - ); - _path.is_disabled = true; - return ResultUnion::Success(Result { - path_pair: PathPair { - primary: _path, - secondary: None, - }, - ..Default::default() - }); - } - - match self.resolve_without_remapping( - import_dir_info, - remap, - kind, - global_cache, - ) { - MatchResultUnion::Success(match_result) => { - let mut flags = ResultFlags::default(); - flags.set_is_external(match_result.is_external); - flags.set_is_external_and_rewrite_import_path( - match_result.is_external, - ); - return ResultUnion::Success(Result { - path_pair: match_result.path_pair, - diff_case: match_result.diff_case, - dirname_fd: match_result.dirname_fd, - package_json: Some(std::ptr::from_ref(pkg)), - jsx: self.opts.jsx.clone(), - module_type: match_result.module_type, - flags, - ..Default::default() - }); - } - _ => {} - } - } - } - } - } - - let prev_extension_order = self.extension_order; - // PORT NOTE: defer restore reshaped — restored before each return - if strings::path_contains_node_modules_folder(abs_path) { - self.extension_order = self.opts.extension_order.kind(kind, true); - } - let ret = if let Some(res) = self.load_as_file_or_directory(abs_path, kind) { - ResultUnion::Success(Result { - path_pair: res.path_pair, - diff_case: res.diff_case, - dirname_fd: res.dirname_fd, - package_json: res.package_json, - jsx: self.opts.jsx.clone(), - ..Default::default() - }) - } else { - ResultUnion::NotFound - }; - self.extension_order = prev_extension_order; - ret - } - - pub fn check_package_path( - &mut self, - source_dir: &[u8], - unremapped_import_path: &'static [u8], - kind: ast::ImportKind, - global_cache: GlobalCache, - ) -> ResultUnion { - let mut import_path = unremapped_import_path; - let mut source_dir_info: DirInfoRef = match self.dir_info_cached(source_dir) { - Err(_) => return ResultUnion::NotFound, - Ok(Some(d)) => d, - Ok(None) => 'dir: { - // It is possible to resolve with a source file that does not exist: - // A. Bundler plugin refers to a non-existing `resolveDir`. - // B. `createRequire()` is called with a path that does not exist. This was - // hit in Nuxt, specifically the `vite-node` dependency [1]. - // - // Normally it would make sense to always bail here, but in the case of - // resolving "hello" from "/project/nonexistent_dir/index.ts", resolution - // should still query "/project/node_modules" and "/node_modules" - // - // For case B in Node.js, they use `_resolveLookupPaths` in - // combination with `_nodeModulePaths` to collect a listing of - // all possible parent `node_modules` [2]. Bun has a much smarter - // approach that caches directory entries, but it (correctly) does - // not cache non-existing directories. To successfully resolve this, - // Bun finds the nearest existing directory, and uses that as the base - // for `node_modules` resolution. Since that directory entry knows how - // to resolve concrete node_modules, this iteration stops at the first - // existing directory, regardless of what it is. - // - // The resulting `source_dir_info` cannot resolve relative files. - // - // [1]: https://github.com/oven-sh/bun/issues/16705 - // [2]: https://github.com/nodejs/node/blob/e346323109b49fa6b9a4705f4e3816fc3a30c151/lib/internal/modules/cjs/loader.js#L1934 - if cfg!(debug_assertions) { - debug_assert!(is_package_path(import_path)); - } - let mut closest_dir = source_dir; - // Use std.fs.path.dirname to get `null` once the entire - // directory tree has been visited. `null` is theoretically - // impossible since the drive root should always exist. - while let Some(current) = bun_paths::dirname(closest_dir) { - match self.dir_info_cached(current) { - Err(_) => return ResultUnion::NotFound, - Ok(Some(dir)) => break 'dir dir, - Ok(None) => {} - } - closest_dir = current; - } - return ResultUnion::NotFound; - } - }; - - if self.care_about_browser_field { - // Support remapping one package path to another via the "browser" field - if let Some(browser_scope) = source_dir_info.get_enclosing_browser_scope() { - if let Some(package_json) = browser_scope.package_json() { - if let Some(remapped) = self - .check_browser_map::<{ BrowserMapPathKind::PackagePath }>( - &browser_scope, - import_path, - ) - { - if remapped.is_empty() { - // "browser": {"module": false} - // does the module exist in the filesystem? - match self.load_node_modules( - import_path, - kind, - source_dir_info, - global_cache, - false, - ) { - MatchResultUnion::Success(node_module) => { - let mut pair = node_module.path_pair; - pair.primary.is_disabled = true; - if let Some(sec) = pair.secondary.as_mut() { - sec.is_disabled = true; - } - return ResultUnion::Success(Result { - path_pair: pair, - dirname_fd: node_module.dirname_fd, - diff_case: node_module.diff_case, - package_json: Some(std::ptr::from_ref(package_json)), - jsx: self.opts.jsx.clone(), - ..Default::default() - }); - } - _ => { - // "browser": {"module": false} - // the module doesn't exist and it's disabled - // so we should just not try to load it - let mut primary = Path::init(import_path); - primary.is_disabled = true; - return ResultUnion::Success(Result { - path_pair: PathPair { - primary, - secondary: None, - }, - diff_case: None, - jsx: self.opts.jsx.clone(), - ..Default::default() - }); - } - } - } - - import_path = remapped; - source_dir_info = browser_scope; - } - } - } - } - - match self.resolve_without_remapping(source_dir_info, import_path, kind, global_cache) { - MatchResultUnion::Success(res) => { - let mut result = Result { - path_pair: PathPair { - primary: Path::empty(), - secondary: None, - }, - jsx: self.opts.jsx.clone(), - ..Default::default() - }; - result.path_pair = res.path_pair; - result.dirname_fd = res.dirname_fd; - result.file_fd = res.file_fd; - result.package_json = res.package_json; - result.diff_case = res.diff_case; - result.flags.set_is_from_node_modules( - result.flags.is_from_node_modules() || res.is_node_module, - ); - result.jsx = self.opts.jsx.clone(); - result.module_type = res.module_type; - result.flags.set_is_external(res.is_external); - // Potentially rewrite the import path if it's external that - // was remapped to a different path - result - .flags - .set_is_external_and_rewrite_import_path(result.flags.is_external()); - - if result.path_pair.primary.is_disabled && result.path_pair.secondary.is_none() - { - return ResultUnion::Success(result); - } - - if res.package_json.is_some() && self.care_about_browser_field { - let base_dir_info = match res.dir_info { - Some(d) => d, - None => match self.read_dir_info(result.path_pair.primary.name.dir) { - Ok(Some(d)) => d, - _ => return ResultUnion::Success(result), - }, - }; - if let Some(browser_scope) = base_dir_info.get_enclosing_browser_scope() { - if let Some(remap) = self - .check_browser_map::<{ BrowserMapPathKind::AbsolutePath }>( - &browser_scope, - result.path_pair.primary.text(), - ) - { - if remap.is_empty() { - result.path_pair.primary.is_disabled = true; - result.path_pair.primary = - Fs::Path::init_with_namespace(remap, b"file"); - } else { - match self.resolve_without_remapping( - browser_scope, - remap, - kind, - global_cache, - ) { - MatchResultUnion::Success(remapped) => { - result.path_pair = remapped.path_pair; - result.dirname_fd = remapped.dirname_fd; - result.file_fd = remapped.file_fd; - result.package_json = remapped.package_json; - result.diff_case = remapped.diff_case; - result.module_type = remapped.module_type; - result.flags.set_is_external(remapped.is_external); - - // Potentially rewrite the import path if it's external that - // was remapped to a different path - result.flags.set_is_external_and_rewrite_import_path( - result.flags.is_external(), - ); - - result.flags.set_is_from_node_modules( - result.flags.is_from_node_modules() - || remapped.is_node_module, - ); - return ResultUnion::Success(result); - } - _ => {} - } - } - } - } - } - - ResultUnion::Success(result) - } - MatchResultUnion::Pending(p) => ResultUnion::Pending(p), - MatchResultUnion::Failure(p) => ResultUnion::Failure(p), - _ => ResultUnion::NotFound, - } - } - - // This is a fallback, hopefully not called often. It should be relatively quick because everything should be in the cache. - pub fn package_json_for_resolved_node_module( - &mut self, - result: &Result, - ) -> Option<*const PackageJSON> { - let mut dir_info = self - .dir_info_cached(result.path_pair.primary.name.dir) - .ok() - .flatten()?; - loop { - if let Some(pkg) = dir_info.package_json() { - // if it doesn't have a name, assume it's something just for adjusting the main fields (react-bootstrap does this) - // In that case, we really would like the top-level package that you download from NPM - // so we ignore any unnamed packages - return Some(std::ptr::from_ref(pkg)); - } - - dir_info = dir_info.get_parent()?; - } - } - - pub fn root_node_module_package_json( - &mut self, - result: &Result, - ) -> Option> { - let path = result.path_const()?; - let mut absolute = path.text(); - // /foo/node_modules/@babel/standalone/index.js - // ^------------^ - let mut end = - strings::last_index_of(absolute, NODE_MODULE_ROOT_STRING).or_else(|| { - // try non-symlinked version - if path.pretty().len() != absolute.len() { - absolute = path.pretty(); - return strings::last_index_of(absolute, NODE_MODULE_ROOT_STRING); - } - None - })?; - end += NODE_MODULE_ROOT_STRING.len(); - - let is_scoped_package = absolute[end] == b'@'; - end += strings::index_of_char(&absolute[end..], SEP)? as usize; - - // /foo/node_modules/@babel/standalone/index.js - // ^ - if is_scoped_package { - end += 1; - end += strings::index_of_char(&absolute[end..], SEP)? as usize; - } - - end += 1; - - // /foo/node_modules/@babel/standalone/index.js - // ^ - let slice = &absolute[0..end]; - - // Try to avoid the hash table lookup whenever possible - // That can cause filesystem lookups in parent directories and it requires a lock - if let Some(pkg) = result.package_json_ref() { - if slice == pkg.source.path.name.dir_with_trailing_slash() { - return Some(RootPathPair { - package_json: std::ptr::from_ref(pkg), - base_path: slice, - }); - } - } - - { - let dir_info = self.dir_info_cached(slice).ok().flatten()?; - Some(RootPathPair { - base_path: slice, - package_json: std::ptr::from_ref(dir_info.package_json()?), - }) - } - } - - /// Directory cache keys must follow the following rules. If the rules are broken, - /// then there will be conflicting cache entries, and trying to bust the cache may not work. - /// - /// When an incorrect cache key is used, this assertion will trip; ignoring it allows - /// very very subtle cache invalidation issues to happen, which will cause modules to - /// mysteriously fail to resolve. - /// - /// The rules for this changed in https://github.com/oven-sh/bun/pull/9144 after multiple - /// cache issues were found on Windows. These issues extended to other platforms because - /// we never checked if the cache key was following the rules. - /// - /// CACHE KEY RULES: - /// A cache key must use native slashes, and must NOT end with a trailing slash. - /// But drive roots MUST have a trailing slash ('/' and 'C:\') - /// UNC paths, even if the root, must not have the trailing slash. - /// - /// The helper function bun.strings.withoutTrailingSlashWindowsPath can be used - /// to remove the trailing slash from a path - pub fn assert_valid_cache_key(path: &[u8]) { - if cfg!(debug_assertions) { - if path.len() > 1 - && strings::char_is_any_slash(path[path.len() - 1]) - && !if cfg!(windows) { - path.len() == 3 && path[1] == b':' - } else { - path.len() == 1 - } - { - panic!( - "Internal Assertion Failure: Invalid cache key \"{}\"\nSee Resolver.assertValidCacheKey for details.", - bstr::BStr::new(path) - ); - } - } - } - - /// Bust the directory cache for the given path. - /// See `assertValidCacheKey` for requirements on the input - pub fn bust_dir_cache(&mut self, path: &[u8]) -> bool { - Self::assert_valid_cache_key(path); - let first_bust = self.fs_mut().fs.bust_entries_cache(path); - let second_bust = self.dir_cache_mut().remove(path); - bun_core::scoped_log!( - Resolver, - "Bust {} = {}, {}", - bstr::BStr::new(path), - first_bust, - second_bust - ); - first_bust || second_bust - } - - /// bust both the named file and a parent directory, because `./hello` can resolve - /// to `./hello.js` or `./hello/index.js` - pub fn bust_dir_cache_from_specifier( - &mut self, - import_source_file: &[u8], - specifier: &[u8], - ) -> bool { - if bun_paths::is_absolute(specifier) { - let dir = bun_paths::dirname_platform(specifier, bun_paths::Platform::AUTO); - let a = self.bust_dir_cache(dir); - let b = self.bust_dir_cache(specifier); - return a || b; - } - - if !(specifier.starts_with(b"./") || specifier.starts_with(b"../")) { - return false; - } - if !bun_paths::is_absolute(import_source_file) { - return false; - } - - let joined = bun_paths::join_abs( - bun_paths::dirname_platform(import_source_file, bun_paths::Platform::AUTO), - bun_paths::Platform::AUTO, - specifier, - ); - let dir = bun_paths::dirname_platform(joined, bun_paths::Platform::AUTO); - - let a = self.bust_dir_cache(dir); - let b = self.bust_dir_cache(joined); - a || b - } - - pub fn load_node_modules( - &mut self, - import_path: &[u8], - kind: ast::ImportKind, - // PORT NOTE: `DirInfoRef` (not `&mut`) — body re-enters `dir_cache` via - // `dir_info_cached()` which, in the self-reference branch, returns the - // SAME BSSMap slot. A `&mut` param carries an FnEntry protector under - // Stacked Borrows; the inner retag would pop it (aliased-&mut UB). - // Spec resolver.zig:1761 takes raw `*DirInfo`; the arena handle Derefs - // to `&DirInfo` per use so overlapping shared reads are sound. - _dir_info: DirInfoRef, - global_cache: GlobalCache, - forbid_imports: bool, - ) -> MatchResultUnion { - let mut dir_info: DirInfoRef = _dir_info; - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Searching for {} in \"node_modules\" directories starting from \"{}\"", - bstr::BStr::new(import_path), - bstr::BStr::new(dir_info.abs_path) - )); - debug.increase_indent(); - } - // PORT NOTE: Zig `defer { debug.decreaseIndent() }` — reshaped for borrowck; - // `decrease_indent()` is called explicitly at every return point below. - - // First, check path overrides from the nearest enclosing TypeScript "tsconfig.json" file - - if let Some(tsconfig) = dir_info.enclosing_tsconfig_json { - // Try path substitutions first - if tsconfig.paths.count() > 0 { - if let Some(res) = self.match_tsconfig_paths(tsconfig, import_path, kind) { - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchResultUnion::Success(res); - } - } - - // Try looking up the path relative to the base URL - if tsconfig.has_base_url() { - let base: &[u8] = &tsconfig.base_url; - if let Some(abs) = self.fs_ref().abs_buf_checked( - &[base, import_path], - bufs!(load_as_file_or_directory_via_tsconfig_base_path), - ) { - if let Some(res) = self.load_as_file_or_directory(abs, kind) { - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchResultUnion::Success(res); - } - } - } - } - - let mut is_self_reference = false; - - // Find the parent directory with the "package.json" file - let mut dir_info_package_json: Option = Some(dir_info); - while let Some(d) = dir_info_package_json { - if d.package_json.is_some() { - break; - } - dir_info_package_json = d.get_parent(); - } - - // Check for subpath imports: https://nodejs.org/api/packages.html#subpath-imports - if let Some(_dir_info_package_json) = dir_info_package_json { - let package_json = _dir_info_package_json.package_json().unwrap(); - - if import_path.starts_with(b"#") - && !forbid_imports - && package_json.imports.is_some() - { - let r = self.load_package_imports( - import_path, - _dir_info_package_json, - kind, - global_cache, - ); - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return r; - } - - // https://nodejs.org/api/packages.html#packages_self_referencing_a_package_using_its_name - let package_name = crate::package_json::Package::parse_name(import_path); - if let Some(_package_name) = package_name { - if _package_name == package_json.name.as_ref() && package_json.exports.is_some() - { - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "\"{}\" is a self-reference", - bstr::BStr::new(import_path) - )); - } - dir_info = _dir_info_package_json; - is_self_reference = true; - } - } - } - - let esm_ = crate::package_json::Package::parse(import_path, bufs!(esm_subpath)); - - let source_dir_info = dir_info; - let mut any_node_modules_folder = false; - let use_node_module_resolver = global_cache != GlobalCache::force; - - // Then check for the package in any enclosing "node_modules" directories - // or in the package root directory if it's a self-reference - while use_node_module_resolver { - // Skip directories that are themselves called "node_modules", since we - // don't ever want to search for "node_modules/node_modules" - 'node_modules: { - if !(dir_info.has_node_modules() || is_self_reference) { - break 'node_modules; - } - any_node_modules_folder = true; - let abs_path: &[u8] = if is_self_reference { - dir_info.abs_path - } else { - match self.fs_ref().abs_buf_checked( - &[dir_info.abs_path, b"node_modules", import_path], - bufs!(node_modules_check), - ) { - Some(p) => p, - None => break 'node_modules, - } - }; - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Checking for a package in the directory \"{}\"", - bstr::BStr::new(abs_path) - )); - } - - let prev_extension_order = self.extension_order; - // PORT NOTE: defer restore reshaped — restored at end of block - - if let Some(ref esm) = esm_ { - let abs_package_path: &[u8] = if is_self_reference { - dir_info.abs_path - } else { - let parts = [dir_info.abs_path, b"node_modules".as_slice(), esm.name]; - self.fs_ref() - .abs_buf(&parts, bufs!(esm_absolute_package_path)) - }; - - if let Ok(Some(pkg_dir_info)) = self.dir_info_cached(abs_package_path) { - self.extension_order = match kind { - ast::ImportKind::Url - | ast::ImportKind::AtConditional - | ast::ImportKind::At => options::ExtOrder::Css, - _ => self.opts.extension_order.kind(kind, true), - }; - - if let Some(package_json) = pkg_dir_info.package_json() { - if let Some(exports_map) = package_json.exports.as_ref() { - // The condition set is determined by the kind of import - let mut module_type = package_json.module_type; - // PORT NOTE: reshaped for borrowck — Zig held a single `ESModule` - // with a raw `*DebugLogs` across both `resolve` calls and the - // intervening `handle_esm_resolution`. In Rust, keeping the - // `ESModule` (which holds `&mut self.debug_logs`) alive across a - // `&mut self` call is aliased-&mut UB. Build a fresh short-lived - // `ESModule` per `resolve` call so its borrow ends before - // `self.handle_esm_resolution` re-borrows `self`. - let conditions = match kind { - ast::ImportKind::Require - | ast::ImportKind::RequireResolve => { - self.opts.conditions.require.clone().expect("oom") - } - ast::ImportKind::At | ast::ImportKind::AtConditional => { - self.opts.conditions.style.clone().expect("oom") - } - _ => self.opts.conditions.import.clone().expect("oom"), - }; - - // Resolve against the path "/", then join it with the absolute - // directory path. This is done because ESM package resolution uses - // URLs while our path resolution uses file system paths. We don't - // want problems due to Windows paths, which are very unlike URL - // paths. We also want to avoid any "%" characters in the absolute - // directory path accidentally being interpreted as URL escapes. - { - // PERF(port): extra conditions clone vs Zig — profile in Phase B. - let esm_resolution = ESModule { - conditions: conditions.clone().expect("oom"), - debug_logs: self.debug_logs.as_mut(), - module_type: &mut module_type, - } - .resolve(b"/", esm.subpath, &exports_map.root); - // ESModule temporary dropped here; `self` is unborrowed. - - if let Some(result) = self.handle_esm_resolution( - esm_resolution, - abs_package_path, - kind, - package_json, - esm.subpath, - ) { - let mut result_copy = result; - result_copy.is_node_module = true; - result_copy.module_type = module_type; - self.extension_order = prev_extension_order; - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchResultUnion::Success(result_copy); - } - } - - // Some popular packages forget to include the extension in their - // exports map, so we try again without the extension. - // - // This is useful for browser-like environments - // where you want a file extension in the URL - // pathname by convention. Vite does this. - // - // React is an example of a package that doesn't include file extensions. - // { - // "exports": { - // ".": "./index.js", - // "./jsx-runtime": "./jsx-runtime.js", - // } - // } - // - // We limit this behavior just to ".js" files. - let extname = bun_paths::extension(esm.subpath); - if extname == b".js" && esm.subpath.len() > 3 { - let esm_resolution = ESModule { - conditions, - debug_logs: self.debug_logs.as_mut(), - module_type: &mut module_type, - } - .resolve( - b"/", - &esm.subpath[0..esm.subpath.len() - 3], - &exports_map.root, - ); - if let Some(result) = self.handle_esm_resolution( - esm_resolution, - abs_package_path, - kind, - package_json, - esm.subpath, - ) { - let mut result_copy = result; - result_copy.is_node_module = true; - result_copy.module_type = module_type; - self.extension_order = prev_extension_order; - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchResultUnion::Success(result_copy); - } - } - - // if they hid "package.json" from "exports", still allow importing it. - if esm.subpath == b"./package.json" { - self.extension_order = prev_extension_order; - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchResultUnion::Success(MatchResult { - // PORT NOTE: PackageJSON.source.path is bun_paths::fs::Path<'static>; convert - // to the resolver's interned crate::fs::Path<'static> via its text. - path_pair: PathPair { - primary: Path::init(package_json.source.path.text), - secondary: None, - }, - dirname_fd: pkg_dir_info.get_file_descriptor(), - file_fd: FD::INVALID, - // Spec resolver.zig:1930 — `Path.isNodeModule()` checks - // `lastIndexOf(name.dir, SEP++"node_modules"++SEP)`, i.e. a - // separator-bounded directory component on `name.dir` (not a - // bare substring of the full text). `bun_paths::fs::Path<'static>` - // doesn't carry that method, so re-derive via the resolver's - // `Path` (already done one line up for `path_pair.primary`). - is_node_module: Path::init( - package_json.source.path.text, - ) - .is_node_module(), - package_json: Some(std::ptr::from_ref(package_json)), - dir_info: Some(dir_info), - ..Default::default() - }); - } - - self.extension_order = prev_extension_order; - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchResultUnion::NotFound; - } - } - } - } - - if let Some(res) = self.load_as_file_or_directory(abs_path, kind) { - self.extension_order = prev_extension_order; - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchResultUnion::Success(res); - } - self.extension_order = prev_extension_order; - } - - match dir_info.get_parent() { - Some(p) => dir_info = p, - None => break, - } - } - - // try resolve from `NODE_PATH` - // https://nodejs.org/api/modules.html#loading-from-the-global-folders - let node_path: &[u8] = self - .env_loader() - .and_then(|env| env.get(b"NODE_PATH")) - .unwrap_or(b""); - if !node_path.is_empty() { - let delim = if cfg!(windows) { b';' } else { b':' }; - for path in node_path.split(|&b| b == delim).filter(|s| !s.is_empty()) { - let Some(abs_path) = self - .fs_ref() - .abs_buf_checked(&[path, import_path], bufs!(node_modules_check)) - else { - continue; - }; - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Checking for a package in the NODE_PATH directory \"{}\"", - bstr::BStr::new(abs_path) - )); - } - if let Some(res) = self.load_as_file_or_directory(abs_path, kind) { - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchResultUnion::Success(res); - } - } - } - - dir_info = source_dir_info; - - // this is the magic! - if global_cache.can_use(any_node_modules_folder) - && self.use_package_manager() - && esm_.is_some() - && strings::is_npm_package_name(esm_.as_ref().unwrap().name) - { - let esm = esm_.as_ref().unwrap().with_auto_version(); - 'load_module_from_cache: { - // If the source directory doesn't have a node_modules directory, we can - // check the global cache directory for a package.json file. - // - // PORT NOTE (Stacked Borrows): `get_package_manager` returns the - // `*mut dyn AutoInstaller` raw pointer; the body below re-borrows - // `self` for `enqueue_dependency_to_resolve` / `debug_logs` / - // `log()`. The PackageManager lives in a separate allocation, so - // derive a raw pointer once and re-borrow per use — disjoint - // from `self`'s storage. - let manager_ptr: *mut dyn AutoInstaller = self.get_package_manager(); - // SAFETY: re-borrowed narrowly per use; PackageManager outlives resolver. - macro_rules! manager { - () => { - unsafe { &mut *manager_ptr } - }; - } - let mut dependency_version = Dependency::Version::default(); - let mut dependency_behavior = Dependency::Behavior::PROD; - let mut string_buf: &[u8] = esm.version; - - // const initial_pending_tasks = manager.pending_tasks; - let mut resolved_package_id: Install::PackageID = 'brk: { - // check if the package.json in the source directory was already added to the lockfile - // and try to look up the dependency from there - if let Some(package_json) = dir_info.package_json_for_dependencies() { - let mut dependencies_list: &[Dependency::Dependency] = &[]; - let resolve_from_lockfile = package_json.package_manager_package_id - != Install::INVALID_PACKAGE_ID; - - if resolve_from_lockfile { - let dependencies = manager!().lockfile_package_dependencies( - package_json.package_manager_package_id, - ); - - // try to find this package name in the dependencies of the enclosing package - dependencies_list = - dependencies.get(manager!().lockfile_dependencies_buf()); - string_buf = manager!().lockfile_string_bytes(); - } else if esm_.as_ref().unwrap().version.is_empty() { - // If you don't specify a version, default to the one chosen in your package.json - dependencies_list = package_json.dependencies.map.values(); - string_buf = package_json.dependencies.source_buf; - } - - for (dependency_id, dependency) in dependencies_list.iter().enumerate() - { - if !strings::eql_long( - dependency.name.slice(string_buf), - esm.name, - true, - ) { - continue; - } - - dependency_version = dependency.version.clone(); - dependency_behavior = dependency.behavior; - - if resolve_from_lockfile { - let resolutions = manager!().lockfile_package_resolutions( - package_json.package_manager_package_id, - ); - - // found it! - break 'brk resolutions - .get(manager!().lockfile_resolutions_buf())[dependency_id]; - } - - break; - } - } - - // If we get here, it means that the lockfile doesn't have this package at all. - // we know nothing - break 'brk Install::INVALID_PACKAGE_ID; - }; - - // Now, there are two possible states: - // 1) We have resolved the package ID, either from the - // lockfile globally OR from the particular package.json - // dependencies list - // - // 2) We parsed the Dependency.Version but there is no - // existing resolved package ID - - // If its an exact version, we can just immediately look it up in the global cache and resolve from there - // If the resolved package ID is _not_ invalid, we can just check - - // If this returns null, then it means we need to *resolve* the package - // Even after resolution, we might still need to download the package - // There are two steps here! Two steps! - let resolution: Resolution = 'brk: { - if resolved_package_id == Install::INVALID_PACKAGE_ID { - if dependency_version.tag == Dependency::version::Tag::Uninitialized { - let sliced_string = - Semver::SlicedString::init(esm.version, esm.version); - if !esm_.as_ref().unwrap().version.is_empty() - && dir_info.enclosing_package_json.is_some() - && global_cache.allow_version_specifier() - { - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchResultUnion::Failure(bun_core::err!( - "VersionSpecifierNotAllowedHere" - )); - } - string_buf = esm.version; - dependency_version = match manager!().parse_dependency( - Semver::String::init(esm.name, esm.name), - None, - esm.version, - &sliced_string, - self.log(), - ) { - Some(v) => v, - None => break 'load_module_from_cache, - }; - } - - if let Some(id) = - manager!().lockfile_resolve(esm.name, &dependency_version) - { - resolved_package_id = id; - } - } - - if resolved_package_id != Install::INVALID_PACKAGE_ID { - break 'brk manager!().lockfile_package_resolution(resolved_package_id); - } - - // unsupported or not found dependency, we might need to install it to the cache - match self.enqueue_dependency_to_resolve( - // Read the raw `NonNull` fields directly (NOT the - // `&'static`-yielding accessors) so mut-provenance from - // `intern_package_json` survives to the write inside - // (Zig: resolver.zig:2074). - dir_info - .package_json_for_dependencies - .or(dir_info.package_json), - &esm, - dependency_behavior, - &mut resolved_package_id, - dependency_version.clone(), - string_buf, - ) { - DependencyToResolve::Resolution(res) => break 'brk res, - DependencyToResolve::Pending(pending) => { - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchResultUnion::Pending(pending); - } - DependencyToResolve::Failure(err) => { - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchResultUnion::Failure(err); - } - // this means we looked it up in the registry and the package doesn't exist or the version doesn't exist - DependencyToResolve::NotFound => { - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchResultUnion::NotFound; - } - } - }; - - let dir_path_for_resolution = match manager!().path_for_resolution( - resolved_package_id, - &resolution, - bufs!(path_in_global_disk_cache), - ) { - Ok(p) => p, - Err(err) => { - // if it's missing, we need to install it - if err == bun_core::err!("FileNotFound") { - match manager!().get_preinstall_state(resolved_package_id) { - Install::PreinstallState::Done => { - // PORT NOTE: `MatchResult.path_pair` is `Path<'static>`; - // intern `import_path` so the disabled-module record - // outlives this frame (Zig had no lifetime here). - let interned = Fs::file_system::DirnameStore::instance() - .append_slice(import_path) - .expect("unreachable"); - let mut path = Fs::Path::init(interned); - path.is_disabled = true; - // this might mean the package is disabled - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchResultUnion::Success(MatchResult { - path_pair: PathPair { - primary: path, - secondary: None, - }, - ..Default::default() - }); - } - st @ (Install::PreinstallState::Extract - | Install::PreinstallState::Extracting) => { - if !global_cache.can_install() { - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchResultUnion::NotFound; - } - let (cloned, string_buf) = esm.copy().expect("unreachable"); - - if st == Install::PreinstallState::Extract { - let dependency_id = manager!() - .lockfile_legacy_package_to_dependency_id( - resolved_package_id, - ) - .expect("unreachable"); - // The npm version + URL live inside `resolution.value`; - // the `AutoInstaller` impl decodes them itself. - if let Err(enqueue_download_err) = manager!() - .enqueue_package_for_download( - esm.name, - dependency_id, - resolved_package_id, - &resolution, - Install::TaskCallbackContext { - root_request_id: 0, - }, - None, - ) - { - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchResultUnion::Failure( - enqueue_download_err, - ); - } - } - - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchResultUnion::Pending(PendingResolution { - esm: cloned, - dependency: dependency_version, - resolution_id: resolved_package_id, - string_buf, - tag: PendingResolutionTag::Download, - ..Default::default() - }); - } - _ => {} - } - } - - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchResultUnion::Failure(err); - } - }; - - match self.dir_info_for_resolution(dir_path_for_resolution, resolved_package_id) - { - Ok(dir_info_to_use_) => { - if let Some(pkg_dir_info) = dir_info_to_use_ { - let abs_package_path = pkg_dir_info.abs_path; - let mut module_type = options::ModuleType::Unknown; - if let Some(package_json) = pkg_dir_info.package_json() { - if let Some(exports_map) = package_json.exports.as_ref() { - // The condition set is determined by the kind of import - // PORT NOTE: reshaped for borrowck — see identical note above. - let conditions = match kind { - ast::ImportKind::Require - | ast::ImportKind::RequireResolve => { - self.opts.conditions.require.clone().expect("oom") - } - _ => self.opts.conditions.import.clone().expect("oom"), - }; - - // Resolve against the path "/", then join it with the absolute - // directory path. This is done because ESM package resolution uses - // URLs while our path resolution uses file system paths. We don't - // want problems due to Windows paths, which are very unlike URL - // paths. We also want to avoid any "%" characters in the absolute - // directory path accidentally being interpreted as URL escapes. - { - // PERF(port): extra conditions clone vs Zig — profile in Phase B. - let esm_resolution = ESModule { - conditions: conditions.clone().expect("oom"), - debug_logs: self.debug_logs.as_mut(), - module_type: &mut module_type, - } - .resolve(b"/", esm.subpath, &exports_map.root); - - if let Some(result) = self.handle_esm_resolution( - esm_resolution, - abs_package_path, - kind, - package_json, - esm.subpath, - ) { - let mut result_copy = result; - result_copy.is_node_module = true; - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchResultUnion::Success(result_copy); - } - } - - // Some popular packages forget to include the extension in their - // exports map, so we try again without the extension. - // (same comment as above) - // - // We limit this behavior just to ".js" files. - let extname = bun_paths::extension(esm.subpath); - if extname == b".js" && esm.subpath.len() > 3 { - let esm_resolution = ESModule { - conditions, - debug_logs: self.debug_logs.as_mut(), - module_type: &mut module_type, - } - .resolve( - b"/", - &esm.subpath[0..esm.subpath.len() - 3], - &exports_map.root, - ); - if let Some(result) = self.handle_esm_resolution( - esm_resolution, - abs_package_path, - kind, - package_json, - esm.subpath, - ) { - let mut result_copy = result; - result_copy.is_node_module = true; - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchResultUnion::Success(result_copy); - } - } - - // if they hid "package.json" from "exports", still allow importing it. - if esm.subpath == b"./package.json" { - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchResultUnion::Success(MatchResult { - path_pair: PathPair { - primary: Fs::Path::init( - package_json.source.path.text, - ), - secondary: None, - }, - dirname_fd: pkg_dir_info.get_file_descriptor(), - file_fd: FD::INVALID, - is_node_module: package_json - .source - .path - .is_node_module(), - package_json: Some(std::ptr::from_ref( - package_json, - )), - dir_info: Some(dir_info), - ..Default::default() - }); - } - - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchResultUnion::NotFound; - } - } - - let Some(abs_path) = self.fs_ref().abs_buf_checked( - &[pkg_dir_info.abs_path, esm.subpath], - bufs!(node_modules_check), - ) else { - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchResultUnion::NotFound; - }; - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Checking for a package in the directory \"{}\"", - bstr::BStr::new(abs_path) - )); - } - - if let Some(mut res) = - self.load_as_file_or_directory(abs_path, kind) - { - res.is_node_module = true; - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchResultUnion::Success(res); - } - } - } - Err(err) => { - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return MatchResultUnion::Failure(err); - } - } - } - } - - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - MatchResultUnion::NotFound - } - - fn dir_info_for_resolution( - &mut self, - dir_path_maybe_trail_slash: &[u8], - package_id: Install::PackageID, - ) -> core::result::Result, bun_core::Error> { - // TODO(port): narrow error set - debug_assert!(self.package_manager.is_some()); - - let dir_path = strings::without_trailing_slash_windows_path(dir_path_maybe_trail_slash); - - Self::assert_valid_cache_key(dir_path); - // Stacked Borrows: bind ONE `&mut HashMap` and route both the lookup and the slot - // projection through it so the returned `*mut DirInfo` shares a parent tag with the - // borrow it was derived from (a second `dir_cache_mut()` Unique retag of the - // whole `BSSMapInner` would otherwise pop it). - let dc = self.dir_cache_mut(); - let mut dir_cache_info_result = dc.get_or_put(dir_path)?; - if dir_cache_info_result.status == allocators::Status::Exists { - // we've already looked up this package before - return Ok(dc - .at_index(dir_cache_info_result.index) - .map(DirInfoRef::from_slot)); - } - // SAFETY: PORT (Stacked Borrows) — derive `rfs` from the raw `*mut FileSystem` - // field via `addr_of_mut!` so later `&mut *self.log()` / `&mut *self.dir_cache()` - // retags below don't pop its provenance. Re-borrow `&mut *rfs` per use. - let rfs: *mut Fs::file_system::RealFS = self.rfs_ptr(); - macro_rules! rfs { - () => { - unsafe { &mut *rfs } - }; - } - // resolver mutex held; `EntriesMap` methods are safe wrappers over the singleton. - let mut cached_dir_entry_result = rfs!().entries.get_or_put(dir_path)?; - - // PORT NOTE: always assigned by either the cached-hit arm or the - // `needs_iter` block below; null-init so rustc accepts the proof. - let mut dir_entries_option: *mut Fs::file_system::real_fs::EntriesOption = - core::ptr::null_mut(); - let mut needs_iter = true; - let mut in_place: Option<*mut Fs::file_system::DirEntry> = None; - let open_dir = match bun_sys::open_dir_for_iteration(FD::cwd(), dir_path) { - Ok(d) => d, - Err(err) => { - // TODO: handle this error better - let _ = self.log_mut().add_error_fmt( - None, - bun_ast::Loc::EMPTY, - format_args!("Unable to open directory: {}", bstr::BStr::new(err.name())), - ); - return Err(err.into()); - } - }; - - if let Some(cached_entry) = rfs!().entries.at_index(cached_dir_entry_result.index) { - if let Fs::file_system::real_fs::EntriesOption::Entries(entries) = cached_entry { - if entries.generation >= self.generation { - dir_entries_option = cached_entry; - needs_iter = false; - } else { - in_place = Some(std::ptr::from_mut(*entries)); - } - } - } - - if needs_iter { - // SAFETY: (block-wide) `in_place`/`dir_entries_ptr`/`dir_entries_option` point to slots - // in `rfs.entries` (BSSMap singleton) or a fresh leaked Box; both outlive this fn and - // are accessed under `rfs.entries_mutex` (see LIFETIMES.tsv). - let mut new_entry = Fs::file_system::DirEntry::init( - if let Some(existing) = in_place { - // SAFETY: see block-wide note above. - unsafe { &*existing }.dir - } else { - Fs::file_system::DirnameStore::instance() - .append_slice(dir_path) - .expect("unreachable") - }, - self.generation, - ); - - // Pre-size `data` so the per-entry inserts below skip the - // 1→2→4→…→N hashbrown rehash cascade from an empty table. 64 - // covers a typical node_modules package dir; larger dirs still - // rehash from there (cheap relative to starting at 0). - new_entry.data.reserve(64); - - let mut dir_iterator = bun_sys::iterate_dir(open_dir); - // Hoist the `FilenameStore` singleton resolve out of the per-entry loop - // (see `DirEntry::add_entry` doc-comment) and reuse the appender state. - let mut filename_store = FilenameStoreAppender::new(); - while let Ok(Some(_value)) = dir_iterator.next() { - new_entry - .add_entry_with_store( - // SAFETY: see block-wide note above. - in_place.map(|existing| unsafe { &mut (*existing).data }), - &_value, - &mut filename_store, - (), - ) - .expect("unreachable"); - } - if let Some(existing) = in_place { - // SAFETY: see block-wide note above. - // PORT NOTE: Zig `clearAndFree` — `StringHashMap` (std::HashMap newtype) - // has no separate `clear_and_free`; `clear()` drops all entries. - unsafe { &mut *existing }.data.clear(); - } - - if self.store_fd { - new_entry.fd = open_dir; - } - // PORT NOTE: see `dir_info_cached_maybe_log` — `DirEntry.data` holds a `NonNull`, - // so a zeroed slot is UB; box `new_entry` directly for the fresh case. - let dir_entries_ptr = match in_place { - Some(p) => { - // SAFETY: dir_entries_ptr is a live BSSMap slot (`in_place`). - unsafe { *p = new_entry }; - p - } - None => bun_core::heap::into_raw(Box::new(new_entry)), - }; - - // bun.fs.debug("readdir({f}, {s}) = {d}", ...) — TODO(port): scoped log - - dir_entries_option = rfs!() - .entries - // SAFETY: see block-wide note above. - .put( - &mut cached_dir_entry_result, - Fs::file_system::real_fs::EntriesOption::Entries(unsafe { - &mut *dir_entries_ptr - }), - ) - .expect("unreachable"); - } - - // We must initialize it as empty so that the result index is correct. - // This is important so that browser_scope has a valid index. - // PORT NOTE: erase the `&mut DirInfo` borrow to `*mut` immediately so - // `self.dir_cache` (and `*self`) are reborrowable for the call below. - let dir_info_ptr: *mut DirInfo::DirInfo = self - .dir_cache_mut() - .put(&mut dir_cache_info_result, DirInfo::DirInfo::default()) - .expect("unreachable"); - - // `dir_path` is a slice into the threadlocal `bufs(.path_in_global_disk_cache)` buffer, - // which gets overwritten on the next auto-install resolution. `dirInfoUncached` stores - // its `path` argument directly as `DirInfo.abs_path` in the permanent `dir_cache`, so - // pass the interned copy from `DirEntry.dir` (always backed by `DirnameStore`) instead. - // SAFETY: ARENA — `dir_entries_option` is a slot in `rfs.entries` (BSSMap) and - // outlives the resolver. Hoist the `&'static [u8] dir` read out so no `&EntriesOption` - // temporary is live when the raw `*mut` is passed below (avoids a needless Unique - // retag that would pop the shared tag mid-argument-list under Stacked Borrows). - let dir_entries_dir = unsafe { &*dir_entries_option }.entries().dir; - self.dir_info_uncached( - dir_info_ptr, - dir_entries_dir, - // already `*mut EntriesOption` — pass raw, no intermediate `&mut` retag - dir_entries_option, - dir_cache_info_result, - cached_dir_entry_result.index, - // Packages in the global disk cache are top-level, we shouldn't try - // to check for a parent package.json - None, - allocators::NOT_FOUND, - open_dir, - Some(package_id), - )?; - // SAFETY: `dir_info_ptr` is the BSSMap slot just filled by `dir_info_uncached`. - Ok(Some(unsafe { DirInfoRef::from_raw(dir_info_ptr) })) - } - - fn enqueue_dependency_to_resolve( - &mut self, - // PORT NOTE: Zig `package_json_: ?*PackageJSON` (mutable). Carried as - // `NonNull` end-to-end so the mut-provenance from `intern_package_json` - // survives to the `package_manager_package_id` write below — taking - // `*const` and casting back to `*mut` would be UB under Stacked Borrows. - package_json_: Option>, - esm: &crate::package_json::Package<'_>, - behavior: Dependency::Behavior, - input_package_id_: &mut Install::PackageID, - version: Dependency::Version, - version_buf: &[u8], - ) -> DependencyToResolve { - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Enqueueing pending dependency \"{}@{}\"", - bstr::BStr::new(esm.name), - bstr::BStr::new(esm.version) - )); - } - - let input_package_id = *input_package_id_; - // PORT NOTE: see `manager_ptr` note in `load_node_modules` — split the - // `&mut self` borrow by holding the PackageManager via raw pointer. - let pm_ptr: *mut dyn AutoInstaller = self.get_package_manager(); - // SAFETY: PackageManager lives in a separate allocation; disjoint from `self`. - macro_rules! pm { - () => { - unsafe { &mut *pm_ptr } - }; - } - // we should never be trying to resolve a dependency that is already resolved - debug_assert!(pm!().lockfile_resolve(esm.name, &version).is_none()); - - // Add the containing package to the lockfile - - let is_main = pm!().lockfile_packages_len() == 0 - && input_package_id == Install::INVALID_PACKAGE_ID; - if is_main { - if let Some(mut package_json) = package_json_ { - // SAFETY: BACKREF — `package_json` is an interned arena slot - // (see `intern_package_json`); `NonNull` carries mut-provenance - // from `NonNull::from(&mut **last)` and no other live borrow - // exists here. - let package_json: &mut PackageJSON = unsafe { package_json.as_mut() }; - // PORT NOTE: Zig called `Package.fromPackageJSON(lockfile, pm, - // log, package_json, features)` then `setHasInstallScript` then - // `lockfile.appendPackage`. The `Package` type is bun_install- - // internal; the `AutoInstaller` impl performs all three steps. - let id = match pm!().lockfile_append_from_package_json( - package_json, - Install::Features { - dev_dependencies: true, - is_main: true, - dependencies: true, - optional_dependencies: true, - ..Default::default() - }, - ) { - Ok(id) => id, - Err(err) => return DependencyToResolve::Failure(err), - }; - package_json.package_manager_package_id = id; - } else { - // we're resolving an unknown package - // the unknown package is the root package - if let Err(err) = pm!().lockfile_append_root_stub() { - return DependencyToResolve::Failure(err); - } - } - } - - if self.opts.prefer_offline_install { - if let Some(package_id) = pm!().resolve_from_disk_cache(esm.name, &version) { - *input_package_id_ = package_id; - return DependencyToResolve::Resolution( - pm!().lockfile_package_resolution(package_id), - ); - } - } - - if input_package_id == Install::INVALID_PACKAGE_ID || input_package_id == 0 { - // All packages are enqueued to the root - // because we download all the npm package dependencies - match pm!().enqueue_dependency_to_root(esm.name, &version, version_buf, behavior) { - Install::EnqueueResult::Resolution { - package_id, - resolution, - } => { - *input_package_id_ = package_id; - return DependencyToResolve::Resolution(resolution); - } - Install::EnqueueResult::Pending(id) => { - let (cloned, string_buf) = esm.copy().expect("unreachable"); - - return DependencyToResolve::Pending(PendingResolution { - esm: cloned, - dependency: version, - root_dependency_id: id, - string_buf, - tag: PendingResolutionTag::Resolve, - ..Default::default() - }); - } - Install::EnqueueResult::NotFound => { - return DependencyToResolve::NotFound; - } - Install::EnqueueResult::Failure(err) => { - return DependencyToResolve::Failure(err); - } - } - } - - // PORT NOTE: 1:1 with Zig — `resolver.zig` ends this function with - // `bun.unreachablePanic("TODO: implement enqueueDependencyToResolve for - // non-root packages", .{})`. The non-root path is genuinely unimplemented - // in the Zig source; this is not a porting stub. - unreachable!("TODO: implement enqueueDependencyToResolve for non-root packages") - } - - fn handle_esm_resolution( - &mut self, - esm_resolution_: crate::package_json::Resolution, - abs_package_path: &[u8], - kind: ast::ImportKind, - package_json: &PackageJSON, - package_subpath: &[u8], - ) -> Option { - let mut esm_resolution = esm_resolution_; - use crate::package_json::Status; - if !((matches!( - esm_resolution.status, - Status::Inexact | Status::Exact | Status::ExactEndsWithStar - )) && !esm_resolution.path.is_empty() - && esm_resolution.path[0] == SEP) - { - return None; - } - - let abs_esm_path: &[u8] = match self.fs_ref().abs_buf_checked( - &[ - abs_package_path, - strings::without_leading_path_separator(&esm_resolution.path), - ], - bufs!(esm_absolute_package_path_joined), - ) { - Some(p) => p, - None => { - esm_resolution.status = Status::ModuleNotFound; - return None; - } - }; - - match esm_resolution.status { - Status::Exact | Status::ExactEndsWithStar => { - let resolved_dir_info = match self - .dir_info_cached(bun_paths::dirname(abs_esm_path).unwrap()) - .ok() - .flatten() - { - Some(d) => d, - None => { - esm_resolution.status = Status::ModuleNotFound; - return None; - } - }; - let entries = match resolved_dir_info.get_entries_ref(self.generation) { - Some(e) => e, - None => { - esm_resolution.status = Status::ModuleNotFound; - return None; - } - }; - let extension_order: options::ExtOrder = - if kind == ast::ImportKind::At || kind == ast::ImportKind::AtConditional { - self.extension_order - } else { - self.opts - .extension_order - .kind(kind, resolved_dir_info.is_inside_node_modules()) - }; - - let base = bun_paths::basename(abs_esm_path); - let entry_query = match entries.get(base) { - Some(q) => q, - None => { - let ends_with_star = esm_resolution.status == Status::ExactEndsWithStar; - esm_resolution.status = Status::ModuleNotFound; - - // Try to have a friendly error message if people forget the extension - if ends_with_star { - let buf = bufs!(load_as_file); - buf[..base.len()].copy_from_slice(base); - for ext in self.opts.ext_order_slice(extension_order).iter() { - let ext: &[u8] = ext; - let file_name = &mut buf[0..base.len() + ext.len()]; - file_name[base.len()..].copy_from_slice(ext); - if entries.get(&file_name[..]).is_some() { - if let Some(debug) = self.debug_logs.as_mut() { - let parts = - [package_json.name.as_ref(), package_subpath]; - debug.add_note_fmt(format_args!( - "The import {} is missing the extension {}", - bstr::BStr::new(ResolvePath::join( - &parts, - bun_paths::Platform::AUTO - )), - bstr::BStr::new(ext) - )); - } - esm_resolution.status = - Status::ModuleNotFoundMissingExtension; - let _ = ext; // PORT NOTE: Zig stored `missing_suffix = ext` here; unused after `return null`. - break; - } - } - } - return None; - } - }; - - if entry_query.entry().kind(self.rfs_ptr(), self.store_fd) - == Fs::file_system::EntryKind::Dir - { - let ends_with_star = esm_resolution.status == Status::ExactEndsWithStar; - esm_resolution.status = Status::UnsupportedDirectoryImport; - - // Try to have a friendly error message if people forget the "/index.js" suffix - if ends_with_star { - if let Ok(Some(dir_info_ref)) = self.dir_info_cached(abs_esm_path) { - if let Some(dir_entries) = - dir_info_ref.get_entries_ref(self.generation) - { - let index = b"index"; - let buf = bufs!(load_as_file); - buf[..index.len()].copy_from_slice(index); - for ext in self.opts.ext_order_slice(extension_order).iter() { - let ext: &[u8] = ext; - let file_name = &mut buf[0..index.len() + ext.len()]; - file_name[index.len()..].copy_from_slice(ext); - let index_query = dir_entries.get(&file_name[..]); - if let Some(iq) = index_query { - if iq.entry().kind(self.rfs_ptr(), self.store_fd) - == Fs::file_system::EntryKind::File - { - if let Some(debug) = self.debug_logs.as_mut() { - let mut ms = - Vec::with_capacity(1 + file_name.len()); - ms.push(b'/'); - ms.extend_from_slice(&file_name[..]); - let parts = [ - package_json.name.as_ref(), - package_subpath, - ]; - debug.add_note_fmt(format_args!( - "The import {} is missing the suffix {}", - bstr::BStr::new(ResolvePath::join( - &parts, - bun_paths::Platform::AUTO - )), - bstr::BStr::new(&ms) - )); - } - break; - } - } - } - } - } - } - - return None; - } - - let absolute_out_path: &[u8] = { - if entry_query.entry().abs_path.is_empty() { - // SAFETY: EntryStore-owned slot; resolver mutex held. RHS fully - // evaluated before LHS `&mut Entry` is materialized. - unsafe { &mut *entry_query.entry }.abs_path = PathString::init( - self.fs_ref() - .dirname_store - .append_slice(abs_esm_path) - .expect("unreachable"), - ); - } - entry_query.entry().abs_path.slice() - }; - let module_type = if let Some(pkg) = resolved_dir_info.package_json() { - pkg.module_type - } else { - options::ModuleType::Unknown - }; - - Some(MatchResult { - path_pair: PathPair { - primary: Path::init_with_namespace(absolute_out_path, b"file"), - secondary: None, - }, - dirname_fd: entries.fd, - file_fd: entry_query.entry().cache().fd, - dir_info: Some(resolved_dir_info), - diff_case: entry_query.diff_case, - is_node_module: true, - package_json: Some( - resolved_dir_info - .package_json() - .map(|p| std::ptr::from_ref(p)) - .unwrap_or(std::ptr::from_ref(package_json)), - ), - module_type, - ..Default::default() - }) - } - Status::Inexact => { - // If this was resolved against an expansion key ending in a "/" - // instead of a "*", we need to try CommonJS-style implicit - // extension and/or directory detection. - if let Some(res) = self.load_as_file_or_directory(abs_esm_path, kind) { - let mut res_copy = res; - res_copy.is_node_module = true; - res_copy.package_json = res_copy - .package_json - .or(Some(std::ptr::from_ref(package_json))); - return Some(res_copy); - } - esm_resolution.status = Status::ModuleNotFound; - None - } - _ => unreachable!(), - } - } - - pub fn resolve_without_remapping( - &mut self, - // PORT NOTE: `DirInfoRef` (not `&mut`) — forwards into `load_node_modules` - // which re-enters `dir_cache` and may re-derive the same DirInfo slot. - // Spec resolver.zig:2584 takes raw `*DirInfo`. - source_dir_info: DirInfoRef, - import_path: &[u8], - kind: ast::ImportKind, - global_cache: GlobalCache, - ) -> MatchResultUnion { - if is_package_path(import_path) { - self.load_node_modules(import_path, kind, source_dir_info, global_cache, false) - } else { - let Some(resolved) = self.fs_ref().abs_buf_checked( - &[source_dir_info.abs_path, import_path], - bufs!(resolve_without_remapping), - ) else { - return MatchResultUnion::NotFound; - }; - if let Some(result) = self.load_as_file_or_directory(resolved, kind) { - return MatchResultUnion::Success(result); - } - MatchResultUnion::NotFound - } - } - - pub fn parse_tsconfig( - &mut self, - file: &[u8], - dirname_fd: FD, - ) -> core::result::Result>, bun_core::Error> { - // Since tsconfig.json is cached permanently, in our DirEntries cache - // we must use the global allocator - let mut entry = self.caches.fs.read_file_with_allocator( - // SAFETY: process-global `FileSystem` singleton (see `fs()` PORT NOTE); narrow `&mut` - // for this call only — `self.caches` is a field of `self` (disjoint allocation). - unsafe { &mut *self.fs() }, - file, - dirname_fd, - false, - None, - None, - )?; - // PORT NOTE: reshaped for borrowck — `mem::take` the contents (leaving - // `Contents::Empty` behind) so `entry` stays whole for the close-guard. - let entry_contents = core::mem::take(&mut entry.contents); - let _close_guard = scopeguard::guard(entry, |mut e| { - let _ = e.close_fd(); - }); - - // The file name needs to be persistent because it can have errors - // and if those errors need to print the filename - // then it will be undefined memory if we parse another tsconfig.json later - let key_path = self - .fs_ref() - .dirname_store - .append_slice(file) - .expect("unreachable"); - - // `use_shared_buffer = false` above, so `entry_contents` is - // `Contents::Owned`/`Empty`. Zig reads with `bun.default_allocator` and - // never frees (tsconfig is interned into the permanent DirInfo cache). - // PORTING.md §Forbidden bars `mem::forget`/`from_raw_parts` to mint - // `&'static`; route through the process-lifetime arena instead. - // TODO(port): once `bun_ast::Source.contents` becomes `Cow<'static,[u8]>` - // / `Box<[u8]>`, the arena indirection here can be dropped. - let contents_static: &'static [u8] = intern_tsconfig_contents(entry_contents); - - let source = bun_ast::Source::init_path_string(key_path, contents_static); - let file_dir = source.path.source_dir(); - - // SAFETY: BACKREF — `self.log` (see `log()` PORT NOTE); disjoint from `self.caches`, - // narrow `&mut` for this call only. - let mut result = match TSConfigJSON::parse( - unsafe { &mut *self.log() }, - &source, - &mut self.caches.json, - )? { - Some(r) => r, - None => return Ok(None), - }; - - if result.has_base_url() { - // this might leak - if !bun_paths::is_absolute(&result.base_url) { - // PORT NOTE: Zig interns into `dirname_store` and stores the - // arena slice; Rust `base_url: Box<[u8]>` owns its bytes, so - // copy `abs_buf`'s thread-local result directly instead of - // double-copying through the arena. - let abs = self - .fs_ref() - .abs_buf(&[file_dir, &result.base_url[..]], bufs!(tsconfig_base_url)); - result.base_url = Box::from(abs); - } - } - - if result.paths.count() > 0 - && (result.base_url_for_paths.is_empty() - || !bun_paths::is_absolute(&result.base_url_for_paths)) - { - // this might leak - let abs = self - .fs_ref() - .abs_buf(&[file_dir, &result.base_url[..]], bufs!(tsconfig_base_url)); - result.base_url_for_paths = Box::from(abs); - } - - // PORT NOTE: Zig `TSConfigJSON.parse` returns `*TSConfigJSON` (already - // heap). Return the `Box` so the caller (`dir_info_uncached`) takes - // ownership — intermediate configs in an extends-chain are dropped via - // `heap::take`, the final one is interned into the DirInfo cache. - Ok(Some(result)) - } - - pub fn bin_dirs(&self) -> &[&'static [u8]] { - if !BIN_FOLDERS_LOADED.load(core::sync::atomic::Ordering::Acquire) { - return &[]; - } - // SAFETY: BIN_FOLDERS protected by BIN_FOLDERS_LOCK at write sites; - // `BIN_FOLDERS_LOADED` (acquire) guarantees init. - unsafe { (*BIN_FOLDERS.get()).assume_init_ref().const_slice() } - } - - pub fn parse_package_json( - &mut self, - file: &[u8], - dirname_fd: FD, - package_id: Option, - ) -> core::result::Result>, bun_core::Error> - { - use crate::package_json::{IncludeDependencies, IncludeScripts}; - // PORT NOTE: Zig threaded both as comptime params; `IncludeDependencies` is a - // const generic on `PackageJSON::parse`, `IncludeScripts` is runtime (it only - // gates one branch). - let include_scripts = if self.care_about_scripts { - IncludeScripts::IncludeScripts - } else { - IncludeScripts::IgnoreScripts - }; - let pkg = if ALLOW_DEPENDENCIES { - PackageJSON::parse::<{ IncludeDependencies::Local }>( - self, - file, - dirname_fd, - package_id, - include_scripts, - ) - } else { - PackageJSON::parse::<{ IncludeDependencies::None }>( - self, - file, - dirname_fd, - package_id, - include_scripts, - ) - }; - let Some(pkg) = pkg else { return Ok(None) }; - - // PORT NOTE: Zig `PackageJSON.new` = `bun.TrivialNew` (heap-allocate, - // never freed — DirInfo cache holds `&'static` refs). PORTING.md - // §Forbidden bars `Box::leak`; intern into the process-lifetime arena - // owned alongside the DirInfo singleton instead. - Ok(Some(intern_package_json(pkg))) - } - - fn dir_info_cached( - &mut self, - path: &[u8], - ) -> core::result::Result, bun_core::Error> { - self.dir_info_cached_maybe_log(true, path) - } - - pub fn read_dir_info( - &mut self, - path: &[u8], - ) -> core::result::Result, bun_core::Error> { - self.dir_info_cached_maybe_log(false, path) - } - - /// Like `readDirInfo`, but returns `null` instead of throwing an error. - pub fn read_dir_info_ignore_error(&mut self, path: &[u8]) -> Option { - self.dir_info_cached_maybe_log(false, path).ok().flatten() - } - - // PORT NOTE: Zig's `dirInfoCachedMaybeLog` takes `comptime enable_logging` - // and `comptime follow_symlinks`. `follow_symlinks` is `true` at every call - // site, so it's dropped here; `enable_logging` is a plain runtime parameter - // (it gates one cold error-formatting branch) so this large dir-walk function - // monomorphizes to a single copy instead of two faulted in at startup. - fn dir_info_cached_maybe_log( - &mut self, - enable_logging: bool, - raw_input_path: &[u8], - ) -> core::result::Result, bun_core::Error> { - // TODO(port): narrow error set - // `self.mutex` is `&'static Mutex` (Copy) — bind it first so the guard - // doesn't keep `self` borrowed across the body. - let _unlock = self.mutex.lock_guard(); - let mut input_path = raw_input_path; - - if is_dot_slash(input_path) || input_path == b"." { - input_path = self.fs_ref().top_level_dir; - } - - // A path longer than MAX_PATH_BYTES cannot name a real directory. - // Bailing here also prevents overflowing `dir_info_uncached_path` - // below when called with user-controlled absolute import paths. - if input_path.len() > MAX_PATH_BYTES { - return Ok(None); - } - - #[cfg(windows)] - { - let win32_normalized_dir_info_cache_buf = bufs!(win32_normalized_dir_info_cache); - input_path = self - .fs_ref() - .normalize_buf(win32_normalized_dir_info_cache_buf, input_path); - // kind of a patch on the fact normalizeBuf isn't 100% perfect what we want - if (input_path.len() == 2 && input_path[1] == b':') - || (input_path.len() == 3 && input_path[1] == b':' && input_path[2] == b'.') - { - debug_assert!( - input_path.as_ptr() == win32_normalized_dir_info_cache_buf.as_ptr() - ); - win32_normalized_dir_info_cache_buf[2] = b'\\'; - input_path = &win32_normalized_dir_info_cache_buf[..3]; - } - - // Filter out \\hello\, a UNC server path but without a share. - // When there isn't a share name, such path is not considered to exist. - if input_path.starts_with(b"\\\\") { - let first_slash = strings::index_of_char(&input_path[2..], b'\\') - .ok_or(()) - .ok(); - if first_slash.is_none() { - return Ok(None); - } - let first_slash = first_slash.unwrap(); - if strings::index_of_char(&input_path[2 + first_slash as usize..], b'\\') - .is_none() - { - return Ok(None); - } - } - } - - ::bun_core::assertf!( - bun_paths::is_absolute(input_path), - "cannot resolve DirInfo for non-absolute path: {}", - bstr::BStr::new(input_path) - ); - - let path_without_trailing_slash = - strings::without_trailing_slash_windows_path(input_path); - Self::assert_valid_cache_key(path_without_trailing_slash); - let top_result = self - .dir_cache_mut() - .get_or_put(path_without_trailing_slash)?; - if top_result.status != allocators::Status::Unknown { - return Ok(self - .dir_cache_mut() - .at_index(top_result.index) - .map(DirInfoRef::from_slot)); - } - - let dir_info_uncached_path_buf = bufs!(dir_info_uncached_path); - - let mut i: i32 = 1; - let input_path_len = input_path.len(); - dir_info_uncached_path_buf[..input_path_len].copy_from_slice(input_path); - // The slice spans one byte past the copied path so the NUL-splice/restore at - // `input_path_len` (queue index 0, processed last in the open-dir loop below) - // writes through `path`'s own provenance. `input_path_len + 1 ≤ MAX_PATH_BYTES + 1` - // (checked above) and `PathBuffer` always carries the +1 sentinel slot, so the - // safe slice is in-bounds and the threadlocal buffer outlives this fn. - let path: &mut [u8] = &mut dir_info_uncached_path_buf[..input_path_len + 1]; - - bufs!(dir_entry_paths_to_resolve)[0].write(DirEntryResolveQueueItem { - result: top_result, - unsafe_path: bun_ptr::RawSlice::new(&path[..input_path_len]), - safe_path: bun_ptr::RawSlice::EMPTY, - fd: FD::INVALID, - }); - let mut top = Dirname::dirname(&path[..input_path_len]); - - let mut top_parent = allocators::Result { - index: allocators::NOT_FOUND, - hash: 0, - status: allocators::Status::NotFound, - }; - #[cfg(windows)] - let root_path = strings::without_trailing_slash_windows_path( - ResolvePath::windows_filesystem_root(path), - ); - #[cfg(not(windows))] - // we cannot just use "/" - // we will write to the buffer past the ptr len so it must be a non-const buffer - let root_path = &path[0..1]; - Self::assert_valid_cache_key(root_path); - - // PORT NOTE: hold RealFS as a raw `*mut` so the entries-mutex/close-dirs - // scopeguards can capture it by Copy without keeping a `self.rfs_ptr()` - // borrow live across the loop body (which calls `&mut self` methods). - // SAFETY: ARENA — `self.fs` points at the process-global FileSystem singleton. - // Derive provenance from the raw `*mut FileSystem` field directly so later - // `unsafe { &mut *self.fs() }` calls (e.g. `dirname_store.append_*`) cannot pop `rfs`'s tag - // under Stacked Borrows (PORTING.md §Forbidden: aliased-&mut). - let rfs: *mut Fs::file_system::RealFS = self.rfs_ptr(); - macro_rules! rfs { - () => { - unsafe { &mut *rfs } - }; - } - - // SAFETY: `rfs` points at process-global storage; outlives this guard. - let _entries_unlock = rfs!().entries_mutex.lock_guard(); - - while top.len() > root_path.len() { - debug_assert!(top.as_ptr() == root_path.as_ptr()); - let result = self.dir_cache_mut().get_or_put(top)?; - - if result.status != allocators::Status::Unknown { - top_parent = result; - break; - } - // Path has more uncached components than our fixed queue can hold. - // This only happens for user-controlled absolute import paths with - // hundreds of short components — no real directory is this deep. - if usize::try_from(i).expect("int cast") >= bufs!(dir_entry_paths_to_resolve).len() - { - return Ok(None); - } - bufs!(dir_entry_paths_to_resolve)[usize::try_from(i).expect("int cast")].write( - DirEntryResolveQueueItem { - unsafe_path: bun_ptr::RawSlice::new(top), - result, - safe_path: bun_ptr::RawSlice::EMPTY, - fd: FD::INVALID, - }, - ); - - if let Some(top_entry) = rfs!().entries.get(top) { - match top_entry { - Fs::file_system::real_fs::EntriesOption::Entries(entries) => { - // SAFETY: slot was written immediately above. - let slot = unsafe { - bufs!(dir_entry_paths_to_resolve) - [usize::try_from(i).expect("int cast")] - .assume_init_mut() - }; - slot.safe_path = bun_ptr::RawSlice::new(entries.dir); - slot.fd = entries.fd; - } - Fs::file_system::real_fs::EntriesOption::Err(err) => { - debuglog!( - "Failed to load DirEntry {} {} - {}", - bstr::BStr::new(top), - bstr::BStr::new(err.original_err.name()), - bstr::BStr::new(err.canonical_error.name()) - ); - break; - } - } - } - i += 1; - top = Dirname::dirname(top); - } - - if top == root_path { - let result = self.dir_cache_mut().get_or_put(root_path)?; - if result.status != allocators::Status::Unknown { - top_parent = result; - } else { - bufs!(dir_entry_paths_to_resolve)[usize::try_from(i).expect("int cast")].write( - DirEntryResolveQueueItem { - unsafe_path: bun_ptr::RawSlice::new(root_path), - result, - safe_path: bun_ptr::RawSlice::EMPTY, - fd: FD::INVALID, - }, - ); - if let Some(top_entry) = rfs!().entries.get(top) { - match top_entry { - Fs::file_system::real_fs::EntriesOption::Entries(entries) => { - // SAFETY: slot was written immediately above. - let slot = unsafe { - bufs!(dir_entry_paths_to_resolve) - [usize::try_from(i).expect("int cast")] - .assume_init_mut() - }; - slot.safe_path = bun_ptr::RawSlice::new(entries.dir); - slot.fd = entries.fd; - } - Fs::file_system::real_fs::EntriesOption::Err(err) => { - debuglog!( - "Failed to load DirEntry {} {} - {}", - bstr::BStr::new(top), - bstr::BStr::new(err.original_err.name()), - bstr::BStr::new(err.canonical_error.name()) - ); - return Err(err.canonical_error); - } - } - } - - i += 1; - } - } - - let mut queue_slice_len = usize::try_from(i).expect("int cast"); - if cfg!(debug_assertions) { - debug_assert!(queue_slice_len > 0); - } - let open_dir_count = core::cell::Cell::new(0usize); - - // When this function halts, any item not processed means it's not found. - // PORT NOTE: capture only what the cleanup needs by-value (store_fd) / by-Cell - // (open_dir_count) so the guard doesn't pin `&mut self` across the loop - // body. `need_to_close_files()` is evaluated AT DROP TIME (matching - // Zig's `defer`), not snapshotted up-front — the loop body calls - // `Fs.FileSystem.setMaxFd()` which can flip `needToCloseFiles()` - // mid-walk. Reach the RealFS via the `&'static` singleton accessor - // instead of capturing a raw `*mut RealFS` (the read is `&self`-only). - let close_dirs_store_fd = self.store_fd; - scopeguard::defer! { - let n = open_dir_count.get(); - if n > 0 && (!close_dirs_store_fd || Fs::FileSystem::get().fs.need_to_close_files()) { - let open_dirs = &bufs!(open_dirs)[0..n]; - for open_dir in open_dirs { - open_dir.close(); - } - } - } - - // We want to walk in a straight line from the topmost directory to the desired directory - // For each directory we visit, we get the entries, but not traverse into child directories - // (unless those child directories are in the queue) - // We go top-down instead of bottom-up to increase odds of reusing previously open file handles - // "/home/jarred/Code/node_modules/react/cjs/react.development.js" - // ^ - // If we start there, we will traverse all of /home/jarred, including e.g. /home/jarred/Downloads - // which is completely irrelevant. - - // After much experimentation... - // - fts_open is not the fastest way to read directories. fts actually just uses readdir!! - // - remember - let mut _safe_path: Option<&'static [u8]> = None; - - // Start at the top. - while queue_slice_len > 0 { - // SAFETY: every slot in `0..queue_slice_len` was `.write()`-initialised above. - let mut queue_top = unsafe { - bufs!(dir_entry_paths_to_resolve)[queue_slice_len - 1].assume_init_ref() - } - .clone(); - // `unsafe_path` was set to a slice of the threadlocal - // `dir_info_uncached_path` buffer earlier in this fn; valid for the - // remainder of the fn body. `safe_path` is either empty or a - // dirname_store-backed `&'static [u8]`. Copy the `RawSlice` handles - // out so the re-borrows below don't hold `queue_top` borrowed. - let (qt_unsafe_path, qt_safe_path) = (queue_top.unsafe_path, queue_top.safe_path); - let queue_top_unsafe_path: &[u8] = qt_unsafe_path.slice(); - let queue_top_safe_path: &[u8] = qt_safe_path.slice(); - // defer top_parent = queue_top.result — done at end of loop body - queue_slice_len -= 1; - - let open_dir: FD = if queue_top.fd.is_valid() { - queue_top.fd - } else { - 'open_dir: { - // This saves us N copies of .toPosixPath - // which was likely the perf gain from resolving directories relative to the parent directory, anyway. - // `queue_top_unsafe_path.len()` is ≤ `input_path_len` < `path.len()` for - // every queue item, so this indexes in-bounds (the +1 sentinel slot for - // queue index 0 — see the `path` construction above). - let nul_at = queue_top_unsafe_path.len(); - let prev_char = path[nul_at]; - path[nul_at] = 0; - let sentinel = bun_core::ZStr::from_buf(path, nul_at); - - #[cfg(unix)] - let open_req: core::result::Result< - FD, - bun_core::Error, - > = { - // TODO(port): std.fs.openDirAbsoluteZ — using bun_sys equivalent - bun_sys::open_dir_absolute_z( - sentinel, - bun_sys::OpenDirOptions { - no_follow: false, - iterate: true, - }, - ) - .map_err(Into::into) - }; - #[cfg(windows)] - let open_req: core::result::Result< - FD, - bun_core::Error, - > = { - bun_sys::open_dir_at_windows_a( - FD::INVALID, - sentinel.as_bytes(), - bun_sys::WindowsOpenDirOptions { - iterable: true, - no_follow: false, - read_only: true, - ..Default::default() - }, - ) - .map_err(Into::into) - }; - - // bun.fs.debug("open({s})", .{sentinel}) — TODO(port): scoped log - // Restore the byte we NUL-terminated above (Zig: `defer path[len] = prev_char`). - // No early-return path exists between the write and here, so a guard is unnecessary. - path[nul_at] = prev_char; - - match open_req { - Ok(fd) => break 'open_dir fd, - Err(err) => { - // Ignore "ENOTDIR" here so that calling "ReadDirectory" on a file behaves - // as if there is nothing there at all instead of causing an error due to - // the directory actually being a file. This is a workaround for situations - // where people try to import from a path containing a file as a parent - // directory. The "pnpm" package manager generates a faulty "NODE_PATH" - // list which contains such paths and treating them as missing means we just - // ignore them during path resolution. - if err == bun_core::err!("ENOTDIR") - || err == bun_core::err!("IsDir") - || err == bun_core::err!("NotDir") - { - return Ok(None); - } - let cached_dir_entry_result = rfs!() - .entries - .get_or_put(queue_top_unsafe_path) - .expect("unreachable"); - // If we don't properly cache not found, then we repeatedly attempt to open the same directories, - // which causes a perf trace that looks like this stupidity; - // - // openat(dfd: CWD, filename: "node_modules/react", flags: RDONLY|DIRECTORY) = -1 ENOENT (No such file or directory) - // ... - self.dir_cache_mut().mark_not_found(queue_top.result); - rfs!().entries.mark_not_found(cached_dir_entry_result); - if !(err == bun_core::err!("ENOENT") - || err == bun_core::err!("FileNotFound")) - { - if enable_logging { - let pretty = queue_top_unsafe_path; - let _ = self.log_mut().add_error_fmt( - None, - bun_ast::Loc::default(), - format_args!( - "Cannot read directory \"{}\": {}", - bstr::BStr::new(pretty), - bstr::BStr::new(err.name()) - ), - ); - } - } - - return Ok(None); - } - } - } - }; - - if !queue_top.fd.is_valid() { - Fs::FileSystem::set_max_fd(open_dir.native()); - // these objects mostly just wrap the file descriptor, so it's fine to keep it. - bufs!(open_dirs)[open_dir_count.get()] = open_dir; - open_dir_count.set(open_dir_count.get() + 1); - } - - let dir_path: &'static [u8] = if !queue_top_safe_path.is_empty() { - // SAFETY: non-empty `safe_path` is always a dirname_store-backed - // `&'static [u8]` (set from `entries.dir` above); widen the - // `RawSlice`-tied borrow back to its true `'static` lifetime. - unsafe { bun_ptr::detach_lifetime(queue_top_safe_path) } - } else { - // ensure trailing slash - if _safe_path.is_none() { - // Now that we've opened the topmost directory successfully, it's reasonable to store the slice. - // `path` spans `input_path_len + 1` for the NUL-splice above; the - // logical input is `path[..input_path_len]` (Zig resolver.zig:2750). - let input = &path[..input_path_len]; - if input[input.len() - 1] != SEP { - let parts: [&[u8]; 2] = [input, SEP_STR.as_bytes()]; - _safe_path = Some(self.fs_ref().dirname_store.append_parts(&parts)?); - } else { - _safe_path = Some(self.fs_ref().dirname_store.append_slice(input)?); - } - } - - let safe_path = _safe_path.unwrap(); - - // Spec resolver.zig:2965 calls `std.mem.indexOf` (returns 0 for an - // empty needle), not `bun.strings.indexOf` (returns null for an - // empty needle). On Windows `queue_top_unsafe_path` is empty when - // `windows_filesystem_root` cannot classify the input — e.g. - // `import(":://x")` is "absolute" per std but has no drive root, - // so `root_path` is `path[0..0]`. Match the spec so the resolver - // caches a not-found instead of panicking. - let dir_path_i = if queue_top_unsafe_path.is_empty() { - 0 - } else { - strings::index_of(safe_path, queue_top_unsafe_path).expect("unreachable") - }; - let mut end = dir_path_i + queue_top_unsafe_path.len(); - - // Directories must always end in a trailing slash or else various bugs can occur. - // This covers "what happens when the trailing" - end += usize::from( - safe_path.len() > end - && end > 0 - && safe_path[end - 1] != SEP - && safe_path[end] == SEP, - ); - &safe_path[dir_path_i..end] - }; - - let mut cached_dir_entry_result = - rfs!().entries.get_or_put(dir_path).expect("unreachable"); - - let mut dir_entries_option: *mut Fs::file_system::real_fs::EntriesOption = - core::ptr::null_mut(); - let mut needs_iter = true; - let mut in_place: Option<*mut Fs::file_system::DirEntry> = None; - - if let Some(cached_entry) = rfs!().entries.at_index(cached_dir_entry_result.index) { - if let Fs::file_system::real_fs::EntriesOption::Entries(entries) = cached_entry - { - if entries.generation >= self.generation { - dir_entries_option = cached_entry; - needs_iter = false; - } else { - in_place = Some(std::ptr::from_mut(*entries)); - } - } - } - - if needs_iter { - // SAFETY: (block-wide) `in_place`/`dir_entries_ptr`/`dir_entries_option` point to - // slots in `rfs.entries` (BSSMap singleton) or a fresh leaked Box; both outlive this - // fn and are accessed under `rfs.entries_mutex` (see LIFETIMES.tsv). - let mut new_entry = Fs::file_system::DirEntry::init( - if let Some(existing) = in_place { - // SAFETY: see block-wide note above. - unsafe { &*existing }.dir - } else { - Fs::file_system::DirnameStore::instance() - .append_slice(dir_path) - .expect("unreachable") - }, - self.generation, - ); - - // Pre-size `data` so the per-entry inserts below skip the - // 1→2→4→…→N hashbrown rehash cascade from an empty table. 64 - // covers a typical node_modules package dir; larger dirs - // still rehash from there (cheap relative to starting at 0). - new_entry.data.reserve(64); - - let mut dir_iterator = bun_sys::iterate_dir(open_dir); - // PORT NOTE: Zig `while (dir_iterator.next().unwrap()) |entry|` — - // `.unwrap()` was on the inner `Maybe(?Entry)`; the Rust `WrappedIterator::next` - // is already flattened to `Result>`, so the `.unwrap()` - // moved to `?`-style break-on-error. - // Hoist the `FilenameStore` singleton resolve out of the per-entry loop - // (see `DirEntry::add_entry` doc-comment) and reuse the appender state. - let mut filename_store = FilenameStoreAppender::new(); - loop { - let _value = match dir_iterator.next() { - Ok(Some(v)) => v, - Ok(None) => break, - Err(_) => break, - }; - new_entry - .add_entry_with_store( - // SAFETY: see block-wide note above. - in_place.map(|existing| unsafe { &mut (*existing).data }), - &_value, - &mut filename_store, - (), - ) - .expect("unreachable"); - } - if let Some(existing) = in_place { - // SAFETY: see block-wide note above. - // PORT NOTE: Zig `clear_and_free`; bun_collections::StringHashMap exposes `clear`. - unsafe { &mut *existing }.data.clear(); - } - new_entry.fd = if self.store_fd { open_dir } else { FD::INVALID }; - // PORT NOTE: Zig `entries_ptr = in_place orelse allocator.create(DirEntry)` then - // `entries_ptr.* = new_entry` (no drop glue). `DirEntry.data` is a `HashMap` - // (`NonNull` inside), so a zeroed slot is UB and `*ptr = new_entry` would drop it. - // Box `new_entry` directly for the fresh case; assign-into only for `in_place`. - let dir_entries_ptr = match in_place { - Some(p) => { - // SAFETY: dir_entries_ptr is a live BSSMap slot (`in_place`). - unsafe { *p = new_entry }; - p - } - None => bun_core::heap::into_raw(Box::new(new_entry)), - }; - dir_entries_option = rfs!() - .entries - // SAFETY: see block-wide note above. - .put( - &mut cached_dir_entry_result, - Fs::file_system::real_fs::EntriesOption::Entries(unsafe { - &mut *dir_entries_ptr - }), - )?; - // bun.fs.debug("readdir({f}, {s}) = {d}", ...) — TODO(port): scoped log - } - - // We must initialize it as empty so that the result index is correct. - // This is important so that browser_scope has a valid index. - // PORT NOTE: erase the `&mut DirInfo` borrow to `*mut` immediately so - // `self.dir_cache` (and `*self`) are reborrowable for the call below. - // SAFETY: ARENA — `dir_cache()` singleton (see PORT NOTE). Stacked Borrows: bind - // ONE `&mut HashMap` and derive BOTH slot pointers from it so they share a parent - // tag — a second `&mut *self.dir_cache()` Unique retag of the whole `BSSMapInner` - // (whose `backing_buf` is inline) would pop `dir_info_ptr`'s tag before - // `dir_info_uncached` writes through it. Spec resolver.zig:3022/3030 routes both - // through the single raw `r.dir_cache: *HashMap` with no intermediate retag. - // NOTE: erasing `&mut V` to `*mut V` does NOT, by itself, survive a sibling Unique - // retag of the parent allocation; the shared `dc` parent is what keeps both live. - let dc = self.dir_cache_mut(); - let dir_info_ptr: *mut DirInfo::DirInfo = - dc.put(&mut queue_top.result, DirInfo::DirInfo::default())?; - let parent_dir_ptr = dc.at_index(top_parent.index).map(DirInfoRef::from_slot); - - self.dir_info_uncached( - dir_info_ptr, - dir_path, - // SAFETY: ARENA — `dir_entries_option` is a slot in `rfs.entries` (BSSMap) and outlives the resolver. - dir_entries_option, - queue_top.result, - cached_dir_entry_result.index, - parent_dir_ptr, - top_parent.index, - open_dir, - None, - )?; - - top_parent = queue_top.result; - - if queue_slice_len == 0 { - // SAFETY: `dir_info_ptr` is the BSSMap slot just filled by `dir_info_uncached`. - return Ok(Some(unsafe { DirInfoRef::from_raw(dir_info_ptr) })); - - // Is the directory we're searching for actually a file? - } else if queue_slice_len == 1 { - // const next_in_queue = queue_slice[0]; - // const next_basename = std.fs.path.basename(next_in_queue.unsafe_path); - // if (dir_info_ptr.getEntries(r.generation)) |entries| { - // if (entries.get(next_basename) != null) { - // return null; - // } - // } - } - } - - unreachable!() - } - - // This closely follows the behavior of "tryLoadModuleUsingPaths()" in the - // official TypeScript compiler - pub fn match_tsconfig_paths( - &mut self, - tsconfig: &TSConfigJSON, - path: &[u8], - kind: ast::ImportKind, - ) -> Option { - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Matching \"{}\" against \"paths\" in \"{}\"", - bstr::BStr::new(path), - bstr::BStr::new(&tsconfig.abs_path) - )); - } - - let mut abs_base_url: &[u8] = &tsconfig.base_url_for_paths; - - // The explicit base URL should take precedence over the implicit base URL - // if present. This matters when a tsconfig.json file overrides "baseUrl" - // from another extended tsconfig.json file but doesn't override "paths". - if tsconfig.has_base_url() { - abs_base_url = &tsconfig.base_url; - } - - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Using \"{}\" as \"baseURL\"", - bstr::BStr::new(abs_base_url) - )); - } - - // Check for exact matches first - { - // PORT NOTE: ArrayHashMap has no `&self` (key,value) iterator; zip the - // parallel `keys()`/`values()` slices (insertion order). - for (key, value) in tsconfig - .paths - .keys() - .iter() - .zip(tsconfig.paths.values().iter()) - { - if strings::eql_long(key, path, true) { - for original_path in value.iter() { - let mut absolute_original_path: &[u8] = original_path; - - if !bun_paths::is_absolute(absolute_original_path) { - let parts: [&[u8]; 2] = [abs_base_url, original_path.as_ref()]; - absolute_original_path = - self.fs_ref().abs_buf(&parts, bufs!(tsconfig_path_abs)); - } - - if let Some(res) = - self.load_as_file_or_directory(absolute_original_path, kind) - { - return Some(res); - } - } - } - } - } - - struct TSConfigMatch<'b> { - prefix: &'b [u8], - suffix: &'b [u8], - original_paths: &'b [Box<[u8]>], - } - - let mut longest_match: Option = None; - let mut longest_match_prefix_length: i32 = -1; - let mut longest_match_suffix_length: i32 = -1; - - for (key, original_paths) in tsconfig - .paths - .keys() - .iter() - .zip(tsconfig.paths.values().iter()) - { - if let Some(star) = strings::index_of_char(key, b'*') { - let star = star as usize; - let prefix: &[u8] = if star == 0 { b"" } else { &key[0..star] }; - let suffix: &[u8] = if star == key.len() - 1 { - b"" - } else { - &key[star + 1..] - }; - - // Find the match with the longest prefix. If two matches have the same - // prefix length, pick the one with the longest suffix. This second edge - // case isn't handled by the TypeScript compiler, but we handle it - // because we want the output to always be deterministic - let plen = i32::try_from(prefix.len()).expect("int cast"); - let slen = i32::try_from(suffix.len()).expect("int cast"); - if path.starts_with(prefix) - && path.ends_with(suffix) - && (plen > longest_match_prefix_length - || (plen == longest_match_prefix_length - && slen > longest_match_suffix_length)) - { - longest_match_prefix_length = plen; - longest_match_suffix_length = slen; - longest_match = Some(TSConfigMatch { - prefix, - suffix, - original_paths, - }); - } - } - } - - // If there is at least one match, only consider the one with the longest - // prefix. This matches the behavior of the TypeScript compiler. - if longest_match_prefix_length != -1 { - let longest_match = longest_match.unwrap(); - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Found a fuzzy match for \"{}*{}\" in \"paths\"", - bstr::BStr::new(longest_match.prefix), - bstr::BStr::new(longest_match.suffix) - )); - } - - for original_path in longest_match.original_paths.iter() { - // Swap out the "*" in the original path for whatever the "*" matched - let matched_text = - &path[longest_match.prefix.len()..path.len() - longest_match.suffix.len()]; - - let total_length: Option = strings::index_of_char(original_path, b'*'); - let prefix_end = total_length - .map(|v| v as usize) - .unwrap_or(original_path.len()); - let prefix_parts: [&[u8]; 2] = [abs_base_url, &original_path[0..prefix_end]]; - - // Concatenate the matched text with the suffix from the wildcard path - let matched_text_with_suffix = bufs!(tsconfig_match_full_buf3); - let mut matched_text_with_suffix_len: usize = 0; - if total_length.is_some() { - let suffix = strings::trim_left(&original_path[prefix_end..], b"*"); - matched_text_with_suffix_len = matched_text.len() + suffix.len(); - if matched_text_with_suffix_len > matched_text_with_suffix.len() { - continue; - } - ::bun_core::concat_into(matched_text_with_suffix, &[matched_text, suffix]); - } - - // 1. Normalize the base path - // so that "/Users/foo/project/", "../components/*" => "/Users/foo/components/"" - let Some(prefix) = self - .fs_ref() - .abs_buf_checked(&prefix_parts, bufs!(tsconfig_match_full_buf2)) - else { - continue; - }; - - // 2. Join the new base path with the matched result - // so that "/Users/foo/components/", "/foo/bar" => /Users/foo/components/foo/bar - let parts: [&[u8]; 3] = [ - prefix, - if matched_text_with_suffix_len > 0 { - strings::trim_left( - &matched_text_with_suffix[0..matched_text_with_suffix_len], - b"/", - ) - } else { - b"" - }, - strings::trim_left(longest_match.suffix, b"/"), - ]; - let Some(absolute_original_path) = self - .fs_ref() - .abs_buf_checked(&parts, bufs!(tsconfig_match_full_buf)) - else { - continue; - }; - - if let Some(res) = self.load_as_file_or_directory(absolute_original_path, kind) - { - return Some(res); - } - } - } - - None - } - - pub fn load_package_imports( - &mut self, - import_path: &[u8], - // PORT NOTE: `DirInfoRef` (not `&mut`) — `handle_esm_resolution` re-enters - // `dir_cache` via `dir_info_cached(dirname(abs_esm_path))`; for any - // imports-map entry resolving to `./` that dirname equals - // `dir_info.abs_path`, re-deriving `&mut` to the SAME slot while a - // `&mut` param's FnEntry protector is live is aliased-&mut UB. - // Spec resolver.zig:3182 takes raw `*DirInfo`. - dir_info: DirInfoRef, - kind: ast::ImportKind, - global_cache: GlobalCache, - ) -> MatchResultUnion { - let package_json = dir_info.package_json().unwrap(); - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Looking for {} in \"imports\" map in {}", - bstr::BStr::new(import_path), - bstr::BStr::new(package_json.source.path.text) - )); - debug.increase_indent(); - // defer debug.decreaseIndent() — TODO(port): missing matching decrease in Zig too - } - let imports_map = package_json.imports.as_ref().unwrap(); - - if import_path.len() == 1 || import_path.starts_with(b"#/") { - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "The path \"{}\" must not equal \"#\" and must not start with \"#/\"", - bstr::BStr::new(import_path) - )); - } - return MatchResultUnion::NotFound; - } - let mut module_type = options::ModuleType::Unknown; - - // PORT NOTE: reshaped for borrowck — Zig kept a raw `*DebugLogs` inside - // `ESModule` across the subsequent `&mut self` calls. In Rust that is - // aliased-&mut UB, so the `ESModule` is constructed as a temporary whose - // borrow of `self.debug_logs` ends as soon as `resolve_imports` returns. - let esm_resolution = ESModule { - conditions: match kind { - ast::ImportKind::Require | ast::ImportKind::RequireResolve => { - self.opts.conditions.require.clone().expect("oom") - } - _ => self.opts.conditions.import.clone().expect("oom"), - }, - debug_logs: self.debug_logs.as_mut(), - module_type: &mut module_type, - } - .resolve_imports(import_path, &imports_map.root); - let _ = module_type; - - if esm_resolution.status == crate::package_json::Status::PackageResolve { - // https://github.com/oven-sh/bun/issues/4972 - // Resolve a subpath import to a Bun or Node.js builtin - // - // Code example: - // - // import { readFileSync } from '#fs'; - // - // package.json: - // - // "imports": { - // "#fs": "node:fs" - // } - // - if self.opts.mark_builtins_as_external || self.opts.target.is_bun() { - if let Some(alias) = HardcodedAlias::get( - &esm_resolution.path, - self.opts.target, - HardcodedAliasCfg::default(), - ) { - return MatchResultUnion::Success(MatchResult { - path_pair: PathPair { - primary: Fs::Path::init(alias.path.as_bytes()), - secondary: None, - }, - is_external: true, - ..Default::default() - }); - } - } - - return self.load_node_modules( - &esm_resolution.path, - kind, - dir_info, - global_cache, - true, - ); - } - - if let Some(result) = self.handle_esm_resolution( - esm_resolution, - package_json.source.path.name.dir, - kind, - package_json, - b"", - ) { - return MatchResultUnion::Success(result); - } - - MatchResultUnion::NotFound - } - - pub fn check_browser_map( - &mut self, - dir_info: &DirInfo::DirInfo, - input_path_: &[u8], - ) -> Option<&'static [u8]> { - let package_json = dir_info.package_json()?; - let browser_map = &package_json.browser_map; - - if browser_map.count() == 0 { - return None; - } - - let mut input_path = input_path_; - - if KIND == BrowserMapPathKind::AbsolutePath { - let abs_path = dir_info.abs_path; - // Turn absolute paths into paths relative to the "browser" map location - if !input_path.starts_with(abs_path) { - return None; - } - - input_path = &input_path[abs_path.len()..]; - } - - if input_path.is_empty() - || (input_path.len() == 1 && (input_path[0] == b'.' || input_path[0] == SEP)) - { - // No bundler supports remapping ".", so we don't either - return None; - } - - // Normalize the path so we can compare against it without getting confused by "./" - let cleaned = self - .fs_ref() - .normalize_buf(bufs!(check_browser_map), input_path); - - if cleaned.len() == 1 && cleaned[0] == b'.' { - // No bundler supports remapping ".", so we don't either - return None; - } - - let mut checker = BrowserMapPath { - remapped: b"", - cleaned, - input_path, - extension_order: self.opts.ext_order_slice(self.extension_order), - map: &package_json.browser_map, - }; - - if checker.check_path(input_path) { - return Some(checker.remapped); - } - - // First try the import path as a package path - if is_package_path(checker.input_path) { - let abs_to_rel = bufs!(abs_to_rel); - match KIND { - BrowserMapPathKind::AbsolutePath => { - abs_to_rel[0..2].copy_from_slice(b"./"); - abs_to_rel[2..2 + checker.input_path.len()] - .copy_from_slice(checker.input_path); - if checker.check_path(&abs_to_rel[0..checker.input_path.len() + 2]) { - return Some(checker.remapped); - } - } - BrowserMapPathKind::PackagePath => { - // Browserify allows a browser map entry of "./pkg" to override a package - // path of "require('pkg')". This is weird, and arguably a bug. But we - // replicate this bug for compatibility. However, Browserify only allows - // this within the same package. It does not allow such an entry in a - // parent package to override this in a child package. So this behavior - // is disallowed if there is a "node_modules" folder in between the child - // package and the parent package. - let is_in_same_package = match dir_info.get_parent() { - Some(parent) => !parent.is_node_modules(), - None => true, - }; - - if is_in_same_package { - abs_to_rel[0..2].copy_from_slice(b"./"); - abs_to_rel[2..2 + checker.input_path.len()] - .copy_from_slice(checker.input_path); - - if checker.check_path(&abs_to_rel[0..checker.input_path.len() + 2]) { - return Some(checker.remapped); - } - } - } - } - } - - None - } - - pub fn load_from_main_field( - &mut self, - path: &[u8], - // PORT NOTE: `DirInfoRef` (not `&mut`) — `get_enclosing_browser_scope()` - // may return `dir_info` itself (resolver.zig:4161 self-browser-scope), - // which would alias a live `&mut`. Spec uses raw `*DirInfo`. - dir_info: DirInfoRef, - _field_rel_path: &[u8], - field: &[u8], - extension_order: options::ExtOrder, - ) -> Option { - let mut field_rel_path = _field_rel_path; - // Is this a directory? - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Found main field \"{}\" with path \"{}\"", - bstr::BStr::new(field), - bstr::BStr::new(field_rel_path) - )); - debug.increase_indent(); - } - - // defer { debug.decreaseIndent() } — handled at returns - macro_rules! dec_ret { - ($e:expr) => {{ - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return $e; - }}; - } - - if self.care_about_browser_field { - // Potentially remap using the "browser" field - if let Some(browser_scope) = dir_info.get_enclosing_browser_scope() { - if let Some(browser_json) = browser_scope.package_json() { - if let Some(remap) = self - .check_browser_map::<{ BrowserMapPathKind::AbsolutePath }>( - &browser_scope, - field_rel_path, - ) - { - // Is the path disabled? - if remap.is_empty() { - let paths = [path, field_rel_path]; - let new_path = - self.fs_ref().abs_alloc(&paths).expect("unreachable"); - let mut _path = Path::init(new_path); - _path.is_disabled = true; - dec_ret!(Some(MatchResult { - path_pair: PathPair { - primary: _path, - secondary: None - }, - package_json: Some(std::ptr::from_ref(browser_json)), - ..Default::default() - })); - } - - field_rel_path = remap; - } - } - } - } - let _paths = [path, field_rel_path]; - let field_abs_path = self.fs_ref().abs_buf(&_paths, bufs!(field_abs_path)); - - // Is this a file? - if let Some(result) = self.load_as_file(field_abs_path, extension_order) { - if let Some(package_json) = dir_info.package_json() { - dec_ret!(Some(MatchResult { - path_pair: PathPair { - primary: Fs::Path::init(result.path), - secondary: None - }, - package_json: Some(std::ptr::from_ref(package_json)), - dirname_fd: result.dirname_fd, - ..Default::default() - })); - } - - dec_ret!(Some(MatchResult { - path_pair: PathPair { - primary: Fs::Path::init(result.path), - secondary: None - }, - dirname_fd: result.dirname_fd, - diff_case: result.diff_case, - ..Default::default() - })); - } - - // Is it a directory with an index? - let Some(field_dir_info) = self.dir_info_cached(field_abs_path).ok().flatten() else { - dec_ret!(None); - }; - - let r = self.load_as_index_with_browser_remapping( - field_dir_info, - field_abs_path, - extension_order, - ); - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - r - } - - // nodeModulePathsForJS / Resolver__propForRequireMainPaths: see src/jsc/resolver_jsc.zig - // (no Zig callers; exported to C++ only) - - // PORT NOTE: `dir_info` is a `DirInfoRef` (matching spec `*DirInfo`) so - // `load_index_with_extension` may re-borrow without aliasing the caller's `&mut`. - pub fn load_as_index( - &mut self, - dir_info: DirInfoRef, - extension_order: options::ExtOrder, - ) -> Option { - // Try the "index" file with extensions - // PORT NOTE: index by `0..len` so each iteration takes a fresh short - // borrow of `self.opts` that ends before `&mut self` is taken by - // `load_index_with_extension` (matches `extra_cjs_extensions` loop below). - let n = self.opts.ext_order_slice(extension_order).len(); - for i in 0..n { - // BACKREF: `RawSlice` detaches the `&self.opts` borrow so the loop - // body can take `&mut self`. Backing `Box<[u8]>` is owned by - // `self.opts` and never mutated while the resolver runs. - let ext = bun_ptr::RawSlice::new(&*self.opts.ext_order_slice(extension_order)[i]); - if let Some(result) = self.load_index_with_extension(dir_info, &ext) { - return Some(result); - } - } - // PORT NOTE: index by `0..len` so each iteration takes a fresh short - // borrow of `self.opts` that ends before `&mut self` is taken by - // `load_index_with_extension` (avoids the forbidden lifetime-extension cast). - let n = self.opts.extra_cjs_extensions.len(); - for i in 0..n { - // BACKREF: see `RawSlice` note above — backing `Box<[u8]>` in - // `extra_cjs_extensions` is heap-stable for the resolver's life. - let ext = bun_ptr::RawSlice::new(&*self.opts.extra_cjs_extensions[i]); - if let Some(result) = self.load_index_with_extension(dir_info, &ext) { - return Some(result); - } - } - - None - } - - fn load_index_with_extension( - &mut self, - dir_info: DirInfoRef, - ext: &[u8], - ) -> Option { - // SAFETY: PORT (Stacked Borrows) — derive `rfs` from the raw `*mut FileSystem` - // field so the `&mut *self.fs()` calls below (`abs_buf`/`dirname_store.append_slice`) - // don't pop its provenance. Re-borrow `&mut *rfs` at the single use site. - let rfs: *mut Fs::file_system::RealFS = self.rfs_ptr(); - - let ext_buf = bufs!(extension_path); - - let base = &mut ext_buf[0..b"index".len() + ext.len()]; - base[0..b"index".len()].copy_from_slice(b"index"); - base[b"index".len()..].copy_from_slice(ext); - - if let Some(entries) = dir_info.get_entries_ref(self.generation) { - if let Some(lookup) = entries.get(&base[..]) { - if lookup.entry().kind(rfs, self.store_fd) == Fs::file_system::EntryKind::File { - let out_buf: &[u8] = { - if lookup.entry().abs_path.is_empty() { - let parts = [dir_info.abs_path, &base[..]]; - let out_buf_ = self.fs_ref().abs_buf(&parts, bufs!(index)); - // SAFETY: EntryStore-owned slot; resolver mutex held. RHS fully - // evaluated before LHS `&mut Entry` is materialized. - unsafe { &mut *lookup.entry }.abs_path = PathString::init( - self.fs_ref() - .dirname_store - .append_slice(out_buf_) - .expect("unreachable"), - ); - } - lookup.entry().abs_path.slice() - }; - - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Found file: \"{}\"", - bstr::BStr::new(out_buf) - )); - } - - if let Some(package_json) = dir_info.package_json() { - return Some(MatchResult { - path_pair: PathPair { - primary: Path::init(out_buf), - secondary: None, - }, - diff_case: lookup.diff_case, - package_json: Some(std::ptr::from_ref(package_json)), - dirname_fd: dir_info.get_file_descriptor(), - ..Default::default() - }); - } - - return Some(MatchResult { - path_pair: PathPair { - primary: Path::init(out_buf), - secondary: None, - }, - diff_case: lookup.diff_case, - dirname_fd: dir_info.get_file_descriptor(), - ..Default::default() - }); - } - } - } - - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Failed to find file: \"{}/{}\"", - bstr::BStr::new(dir_info.abs_path), - bstr::BStr::new(&base[..]) - )); - } - - None - } - - pub fn load_as_index_with_browser_remapping( - &mut self, - // PORT NOTE: `DirInfoRef` (not `&mut`) — `get_enclosing_browser_scope()` - // may return `dir_info` itself (resolver.zig:4161 self-browser-scope), - // which would alias a live `&mut`. Spec uses raw `*DirInfo`. - dir_info: DirInfoRef, - path_: &[u8], - extension_order: options::ExtOrder, - ) -> Option { - // In order for our path handling logic to be correct, it must end with a trailing slash. - let mut path = path_; - // Hoisted to fn-body scope so the immutable reborrow taken below can outlive - // the `if` block without lifetime erasure; the field is not touched again in - // this fn (only `remap_path` is, via a separate `bufs!` raw-ptr projection). - let path_buf = bufs!(remap_path_trailing_slash); - if !strings::ends_with_char(path_, SEP) { - path_buf[..path.len()].copy_from_slice(path); - path_buf[path.len()] = SEP; - path_buf[path.len() + 1] = 0; - path = &path_buf[..path.len() + 1]; - } - - if self.care_about_browser_field { - if let Some(browser_scope) = dir_info.get_enclosing_browser_scope() { - const FIELD_REL_PATH: &[u8] = b"index"; - - if let Some(browser_json) = browser_scope.package_json() { - if let Some(remap) = self - .check_browser_map::<{ BrowserMapPathKind::AbsolutePath }>( - &browser_scope, - FIELD_REL_PATH, - ) - { - // Is the path disabled? - if remap.is_empty() { - let paths = [path, FIELD_REL_PATH]; - let new_path = self.fs_ref().abs_buf(&paths, bufs!(remap_path)); - let mut _path = Path::init(new_path); - _path.is_disabled = true; - return Some(MatchResult { - path_pair: PathPair { - primary: _path, - secondary: None, - }, - package_json: Some(std::ptr::from_ref(browser_json)), - ..Default::default() - }); - } - - let new_paths = [path, remap]; - let remapped_abs = self.fs_ref().abs_buf(&new_paths, bufs!(remap_path)); - - // Is this a file - if let Some(file_result) = - self.load_as_file(remapped_abs, extension_order) - { - return Some(MatchResult { - dirname_fd: file_result.dirname_fd, - path_pair: PathPair { - primary: Path::init(file_result.path), - secondary: None, - }, - diff_case: file_result.diff_case, - ..Default::default() - }); - } - - // Is it a directory with an index? - if let Ok(Some(new_dir)) = self.dir_info_cached(remapped_abs) { - if let Some(absolute) = self.load_as_index(new_dir, extension_order) - { - return Some(absolute); - } - } - - return None; - } - } - } - } - - self.load_as_index(dir_info, extension_order) - } - - pub fn load_as_file_or_directory( - &mut self, - path: &[u8], - kind: ast::ImportKind, - ) -> Option { - let extension_order = self.extension_order; - - // Is this a file? - if let Some(file) = self.load_as_file(path, extension_order) { - // Determine the package folder by looking at the last node_modules/ folder in the path - let nm_seg = const_format::concatcp!("node_modules", SEP_STR).as_bytes(); - if let Some(last_node_modules_folder) = strings::last_index_of(file.path, nm_seg) { - let node_modules_folder_offset = last_node_modules_folder + nm_seg.len(); - // Determine the package name by looking at the next separator - if let Some(package_name_length) = - strings::index_of_char(&file.path[node_modules_folder_offset..], SEP) - { - if let Ok(Some(package_dir_info)) = self.dir_info_cached( - &file.path - [0..node_modules_folder_offset + package_name_length as usize], - ) { - if let Some(package_json) = package_dir_info.package_json() { - return Some(MatchResult { - path_pair: PathPair { - primary: Path::init(file.path), - secondary: None, - }, - diff_case: file.diff_case, - dirname_fd: file.dirname_fd, - package_json: Some(std::ptr::from_ref(package_json)), - file_fd: file.file_fd, - ..Default::default() - }); - } - } - } - } - - if cfg!(debug_assertions) { - debug_assert!(bun_paths::is_absolute(file.path)); - } - - return Some(MatchResult { - path_pair: PathPair { - primary: Path::init(file.path), - secondary: None, - }, - diff_case: file.diff_case, - dirname_fd: file.dirname_fd, - file_fd: file.file_fd, - ..Default::default() - }); - } - - // Is this a directory? - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Attempting to load \"{}\" as a directory", - bstr::BStr::new(path) - )); - debug.increase_indent(); - } - // defer if (r.debug_logs) |*debug| debug.decreaseIndent(); - macro_rules! dec_ret { - ($e:expr) => {{ - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return $e; - }}; - } - - // PORT NOTE: `DirInfoRef` (matching spec resolver.zig:3674 raw `*DirInfo`). - // The callees fetch `get_enclosing_browser_scope()` which can resolve - // back to this same BSSMap slot — holding a `&mut` here would alias. - let dir_info: DirInfoRef = match self.dir_info_cached(path) { - Ok(Some(d)) => d, - Ok(None) => dec_ret!(None), - Err(err) => { - #[cfg(debug_assertions)] - Output::pretty_errorln(&format_args!( - "err: {} reading {}", - bstr::BStr::new(err.name()), - bstr::BStr::new(path) - )); - dec_ret!(None); - } - }; - let mut package_json: Option<*const PackageJSON> = None; - - // Try using the main field(s) from "package.json" - if let Some(pkg_json) = dir_info.package_json() { - package_json = Some(std::ptr::from_ref(pkg_json)); - if pkg_json.main_fields.count() > 0 { - let main_field_values = &pkg_json.main_fields; - // BACKREF: `RawSlice` detaches the `&self.opts.main_fields` - // borrow so the loop body can take `&mut self`. Backing - // `Box<[Box<[u8]>]>` heap buffer is owned by `self.opts` and - // never mutated during resolve. - let main_field_keys = - bun_ptr::RawSlice::>::new(&self.opts.main_fields); - let mf_ext_order = options::ExtOrder::MainField; - // Spec resolver.zig compares the *pointer* of `opts.main_fields` - // against the per-target default to detect "user did not pass - // --main-fields"; the bundler now projects that as an explicit - // bool because the owned `Box<[Box<[u8]>]>` can never alias a - // static. - let auto_main = self.opts.main_fields_is_default; - - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Searching for main fields in \"{}\"", - bstr::BStr::new(pkg_json.source.path.text) - )); - } - - for key in main_field_keys.iter() { - let key: &[u8] = key; - let field_rel_path = match main_field_values.get(key) { - Some(v) => v, - None => { - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Did not find main field \"{}\"", - bstr::BStr::new(key) - )); - } - continue; - } - }; - - let mut _result = match self.load_from_main_field( - path, - dir_info, - field_rel_path, - key, - if key == b"main" { - mf_ext_order - } else { - extension_order - }, - ) { - Some(r) => r, - None => continue, - }; - - // If the user did not manually configure a "main" field order, then - // use a special per-module automatic algorithm to decide whether to - // use "module" or "main" based on whether the package is imported - // using "import" or "require". - if auto_main && key == b"module" { - let mut absolute_result: Option = None; - - if let Some(main_rel_path) = main_field_values.get(b"main".as_slice()) { - if !main_rel_path.is_empty() { - absolute_result = self.load_from_main_field( - path, - dir_info, - main_rel_path, - b"main", - mf_ext_order, - ); - } - } else { - // Some packages have a "module" field without a "main" field but - // still have an implicit "index.js" file. In that case, treat that - // as the value for "main". - absolute_result = self.load_as_index_with_browser_remapping( - dir_info, - path, - mf_ext_order, - ); - } - - if let Some(auto_main_result) = absolute_result { - // If both the "main" and "module" fields exist, use "main" if the - // path is for "require" and "module" if the path is for "import". - // If we're using "module", return enough information to be able to - // fall back to "main" later if something ended up using "require()" - // with this same path. The goal of this code is to avoid having - // both the "module" file and the "main" file in the bundle at the - // same time. - // - // Additionally, if this is for the runtime, use the "main" field. - // If it doesn't exist, the "module" field will be used. - if self.prefer_module_field && kind != ast::ImportKind::Require { - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Resolved to \"{}\" using the \"module\" field in \"{}\"", - bstr::BStr::new(auto_main_result.path_pair.primary.text()), - bstr::BStr::new(pkg_json.source.path.text) - )); - debug.add_note_fmt(format_args!( - "The fallback path in case of \"require\" is {}", - bstr::BStr::new( - auto_main_result.path_pair.primary.text() - ) - )); - } - - dec_ret!(Some(MatchResult { - path_pair: PathPair { - primary: _result.path_pair.primary, - secondary: Some(auto_main_result.path_pair.primary), - }, - diff_case: _result.diff_case, - dirname_fd: _result.dirname_fd, - package_json, - file_fd: auto_main_result.file_fd, - ..Default::default() - })); - } else { - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Resolved to \"{}\" using the \"{}\" field in \"{}\"", - bstr::BStr::new( - auto_main_result.path_pair.primary.text() - ), - bstr::BStr::new(key), - bstr::BStr::new(pkg_json.source.path.text) - )); - } - let mut _auto_main_result = auto_main_result; - _auto_main_result.package_json = package_json; - dec_ret!(Some(_auto_main_result)); - } - } - } - - _result.package_json = _result.package_json.or(package_json); - dec_ret!(Some(_result)); - } - } - } - - // Look for an "index" file with known extensions - if let Some(res) = - self.load_as_index_with_browser_remapping(dir_info, path, extension_order) - { - let mut res_copy = res; - res_copy.package_json = res_copy.package_json.or(package_json); - dec_ret!(Some(res_copy)); - } - - dec_ret!(None); - } - - pub fn load_as_file( - &mut self, - path: &[u8], - extension_order: options::ExtOrder, - ) -> Option { - // SAFETY: PORT — RealFS is the global singleton (fs.zig); Zig held a raw - // pointer here (resolver.zig:3784). Derive provenance from the raw - // `*mut FileSystem` field so intervening `unsafe { &mut *self.fs() }` calls in - // `load_extension` / `dirname_store.append_slice` don't invalidate `rfs` - // under Stacked Borrows. We re-borrow `&mut *rfs` at each use site. - let rfs: *mut Fs::file_system::RealFS = self.rfs_ptr(); - #[allow(unused_macros)] - macro_rules! rfs { - () => { - unsafe { &mut *rfs } - }; - } - - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Attempting to load \"{}\" as a file", - bstr::BStr::new(path) - )); - debug.increase_indent(); - } - macro_rules! dec_ret { - ($e:expr) => {{ - if let Some(d) = self.debug_logs.as_mut() { - d.decrease_indent(); - } - return $e; - }}; - } - - let dir_path = strings::without_trailing_slash_windows_path(Dirname::dirname(path)); - - // PORT — `dir_entry` is a slot in the BSSMap singleton (ARENA, see - // LIFETIMES.tsv); wrap in `BackRef` so later `&mut self` calls - // (debug_logs / load_extension / dirname_store) don't trip borrowck - // while each read goes through safe `BackRef: Deref` (pointee outlives - // holder by ARENA invariant). - let dir_entry: bun_ptr::BackRef = - match unsafe { &mut *rfs }.read_directory( - dir_path, - None, - self.generation, - self.store_fd, - ) { - Ok(e) => bun_ptr::BackRef::new_mut(e), - Err(_) => dec_ret!(None), - }; - - if let Fs::file_system::real_fs::EntriesOption::Err(err) = dir_entry.get() { - match err.original_err { - e if e == bun_core::err!("ENOENT") - || e == bun_core::err!("FileNotFound") - || e == bun_core::err!("ENOTDIR") - || e == bun_core::err!("NotDir") => {} - _ => { - let _ = self.log_mut().add_error_fmt( - None, - bun_ast::Loc::EMPTY, - format_args!( - "Cannot read directory \"{}\": {}", - bstr::BStr::new(dir_path), - bstr::BStr::new(err.original_err.name()) - ), - ); - } - } - dec_ret!(None); - } - - // ARENA-backed `DirEntry` (see `dir_entry` note above) — `BackRef` so each - // `entries!()` is a fresh safe shared borrow instead of an open-coded raw deref. - let entries = bun_ptr::BackRef::new(dir_entry.entries()); - macro_rules! entries { - () => { - entries.get() - }; - } - - let base = bun_paths::basename(path); - - // Try the plain path without any extensions - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Checking for file \"{}\" ", - bstr::BStr::new(base) - )); - } - - if let Some(query) = entries!().get(base) { - if query.entry().kind(rfs, self.store_fd) == Fs::file_system::EntryKind::File { - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Found file \"{}\" ", - bstr::BStr::new(base) - )); - } - - let abs_path: &'static [u8] = { - if query.entry().abs_path.is_empty() { - let abs_path_parts = [query.entry().dir, query.entry().base()]; - let joined = - self.fs_ref().abs_buf(&abs_path_parts, bufs!(load_as_file)); - // SAFETY: EntryStore-owned slot; resolver mutex held. RHS fully - // evaluated before LHS `&mut Entry` is materialized. - unsafe { &mut *query.entry }.abs_path = PathString::init( - self.fs_ref() - .dirname_store - .append_slice(joined) - .expect("unreachable"), - ); - } - crate::path_string_static(&query.entry().abs_path) - }; - - dec_ret!(Some(LoadResult { - path: abs_path, - diff_case: query.diff_case, - dirname_fd: entries!().fd, - file_fd: query.entry().cache().fd, - dir_info: None, - })); - } - } - - // Try the path with extensions - bufs!(load_as_file)[..path.len()].copy_from_slice(path); - // PORT NOTE: index by `0..len` so each iteration takes a fresh short - // borrow of `self.opts` that ends before `&mut self` is taken by - // `load_extension` (matches `extra_cjs_extensions` loop below). - let n = self.opts.ext_order_slice(extension_order).len(); - for i in 0..n { - // BACKREF: `RawSlice` detaches the `&self.opts` borrow so the loop - // body can take `&mut self`. Backing `Box<[u8]>` is owned by - // `self.opts` and never mutated while the resolver runs. - let ext = bun_ptr::RawSlice::new(&*self.opts.ext_order_slice(extension_order)[i]); - if let Some(result) = self.load_extension(base, path, &ext, entries!()) { - dec_ret!(Some(result)); - } - } - - // PORT NOTE: index by `0..len` so each iteration takes a fresh short - // borrow of `self.opts` that ends before `&mut self` is taken by - // `load_extension` (avoids the forbidden lifetime-extension cast). - let n = self.opts.extra_cjs_extensions.len(); - for i in 0..n { - // BACKREF: see `RawSlice` note above — backing `Box<[u8]>` in - // `extra_cjs_extensions` is heap-stable for the resolver's life. - let ext = bun_ptr::RawSlice::new(&*self.opts.extra_cjs_extensions[i]); - if let Some(result) = self.load_extension(base, path, &ext, entries!()) { - dec_ret!(Some(result)); - } - } - - // TypeScript-specific behavior: if the extension is ".js" or ".jsx", try - // replacing it with ".ts" or ".tsx". At the time of writing this specific - // behavior comes from the function "loadModuleFromFile()" in the file - // "moduleNameThisResolver.ts" in the TypeScript compiler source code. It - // contains this comment: - // - // If that didn't work, try stripping a ".js" or ".jsx" extension and - // replacing it with a TypeScript one; e.g. "./foo.js" can be matched - // by "./foo.ts" or "./foo.d.ts" - // - // We don't care about ".d.ts" files because we can't do anything with - // those, so we ignore that part of the behavior. - // - // See the discussion here for more historical context: - // https://github.com/microsoft/TypeScript/issues/4595 - if let Some(last_dot) = strings::last_index_of_char(base, b'.') { - let ext = &base[last_dot..base.len()]; - // PORT NOTE: spec resolver.zig:3890-3891 — Zig `and` binds tighter than `or`, so the - // node_modules gate only applies to the `.mjs` arm. Mirror that precedence exactly. - if ext == b".js" - || ext == b".jsx" - || (ext == b".mjs" - && (!FeatureFlags::DISABLE_AUTO_JS_TO_TS_IN_NODE_MODULES - || !strings::path_contains_node_modules_folder(path))) - { - let segment = &base[0..last_dot]; - let tail = &mut bufs!(load_as_file)[path.len() - base.len()..]; - tail[..segment.len()].copy_from_slice(segment); - - let exts: &[&[u8]] = if ext == b".mjs" { - &[b".mts"] - } else { - &[b".ts", b".tsx", b".mts"] - }; - - for ext_to_replace in exts { - let buffer = &mut tail[0..segment.len() + ext_to_replace.len()]; - buffer[segment.len()..].copy_from_slice(ext_to_replace); - - if let Some(query) = entries!().get(&buffer[..]) { - if query.entry().kind(rfs, self.store_fd) - == Fs::file_system::EntryKind::File - { - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Rewrote to \"{}\" ", - bstr::BStr::new(&buffer[..]) - )); - } - - dec_ret!(Some(LoadResult { - path: { - if query.entry().abs_path.is_empty() { - // SAFETY: `dir` is `&'static [u8]` (DirnameStore-interned), - // copied out so no `&Entry` borrow survives into the - // `&mut Entry` write below. - let entry_dir = query.entry().dir; - let new_abs = if !entry_dir.is_empty() - && entry_dir[entry_dir.len() - 1] == SEP - { - let parts: [&[u8]; 2] = [entry_dir, &buffer[..]]; - PathString::init( - self.fs_ref() - .filename_store - .append_parts(&parts) - .expect("unreachable"), - ) - // the trailing path CAN be missing here - } else { - let parts: [&[u8]; 3] = - [entry_dir, SEP_STR.as_bytes(), &buffer[..]]; - PathString::init( - self.fs_ref() - .filename_store - .append_parts(&parts) - .expect("unreachable"), - ) - }; - // SAFETY: EntryStore-owned slot; resolver mutex held. RHS - // fully evaluated above — sole `&mut Entry` for this write. - unsafe { &mut *query.entry }.abs_path = new_abs; - } - crate::path_string_static(&query.entry().abs_path) - }, - diff_case: query.diff_case, - dirname_fd: entries!().fd, - file_fd: query.entry().cache().fd, - dir_info: None, - })); - } - } - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Failed to rewrite \"{}\" ", - bstr::BStr::new(base) - )); - } - } - } - } - - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Failed to find \"{}\" ", - bstr::BStr::new(path) - )); - } - - if FeatureFlags::WATCH_DIRECTORIES { - // For existent directories which don't find a match - // Start watching it automatically, - if let Some(watcher) = self.watcher.as_ref() { - watcher.watch(entries!().dir, entries!().fd); - } - } - dec_ret!(None); - } - - fn load_extension( - &mut self, - base: &[u8], - path: &[u8], - ext: &[u8], - entries: &Fs::file_system::DirEntry, - ) -> Option { - // SAFETY: PORT — see load_as_file; derive `rfs` from the raw `*mut FileSystem` - // field so `unsafe { &mut *self.fs() }` calls below (`filename_store.append_parts`) don't pop - // its provenance under Stacked Borrows. - let rfs: *mut Fs::file_system::RealFS = self.rfs_ptr(); - // BACKREF — `entries` is a slot in the BSSMap-backed `DirEntry` arena - // (see `load_as_file`); detach the borrowck lifetime via `BackRef` so the - // `&mut self` calls below (debug_logs / fs_ref) don't conflict, while - // each read stays a safe `BackRef: Deref`. - let entries = bun_ptr::BackRef::new(entries); - let buffer = &mut bufs!(load_as_file)[0..path.len() + ext.len()]; - buffer[path.len()..].copy_from_slice(ext); - let file_name = &buffer[path.len() - base.len()..buffer.len()]; - - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Checking for file \"{}\" ", - bstr::BStr::new(&buffer[..]) - )); - } - - if let Some(query) = entries.get().get(file_name) { - if query.entry().kind(rfs, self.store_fd) == Fs::file_system::EntryKind::File { - if let Some(debug) = self.debug_logs.as_mut() { - debug.add_note_fmt(format_args!( - "Found file \"{}\" ", - bstr::BStr::new(&buffer[..]) - )); - } - - // now that we've found it, we allocate it. - return Some(LoadResult { - path: { - // SAFETY: EntryStore-owned slot; resolver mutex held. RHS is fully - // evaluated (shared reads) before the LHS `&mut Entry` is - // materialized for the write — no overlapping unique borrow. - unsafe { &mut *query.entry }.abs_path = - if query.entry().abs_path.is_empty() { - PathString::init( - self.fs_ref() - .dirname_store - .append_slice(&buffer[..]) - .expect("unreachable"), - ) - } else { - query.entry().abs_path - }; - crate::path_string_static(&query.entry().abs_path) - }, - diff_case: query.diff_case, - dirname_fd: entries.fd, - file_fd: query.entry().cache().fd, - dir_info: None, - }); - } - } - - None - } - - fn dir_info_uncached( - &mut self, - info: *mut DirInfo::DirInfo, - path: &'static [u8], - _entries: *mut Fs::file_system::real_fs::EntriesOption, - _result: allocators::Result, - dir_entry_index: allocators::IndexType, - parent: Option, - parent_index: allocators::IndexType, - fd: FD, - package_id: Option, - ) -> core::result::Result<(), bun_core::Error> { - let result = _result; - - // SAFETY: PORT — RealFS / DirEntry are global ARENA singletons (BSSMap-backed); - // Zig held raw pointers here (resolver.zig:4004 `rfs: *Fs.FileSystem.RealFS`). - // Derive `rfs_ptr` from the raw `*mut FileSystem` field so later `unsafe { &mut *self.fs() }` calls - // (`abs_buf` / `dirname_store.append_slice` in the parent-symlink block) cannot - // invalidate it under Stacked Borrows. Re-borrow at EACH use site so no `&mut` - // outlives a `unsafe { &mut *self.fs() }` / `get_entries()` / `parse_package_json()` call. - // TODO(port): split RealFS borrow once entries iteration is interior-mutability-backed. - let rfs_ptr: *mut Fs::file_system::RealFS = self.rfs_ptr(); - let entries_ptr: *mut Fs::file_system::DirEntry = - unsafe { &mut *_entries }.entries_mut(); - // PORT NOTE: re-borrow per use; see SAFETY note above. - macro_rules! rfs { - () => { - unsafe { &mut *rfs_ptr } - }; - } - macro_rules! entries { - () => { - unsafe { &mut *entries_ptr } - }; - } - - if cfg!(debug_assertions) { - // `path` is stored in the permanent `dir_cache` as `DirInfo.abs_path`. It must not - // point into a reused threadlocal scratch buffer, or a later resolution will - // corrupt cached entries. Callers must intern it (e.g. via `DirnameStore`) first. - ::bun_core::assertf!( - !allocators::is_slice_in_buffer(path, &bufs!(path_in_global_disk_cache)[..]), - "DirInfo.abs_path must not point into the threadlocal path_in_global_disk_cache buffer (got \"{}\")", - bstr::BStr::new(path) - ); - } - - // SAFETY: info is a slot in the BSSMap-backed dir_cache - let info = unsafe { &mut *info }; - *info = DirInfo::DirInfo { - abs_path: path, - // .abs_real_path = path, - parent: parent_index, - entries: dir_entry_index, - ..Default::default() - }; - - // A "node_modules" directory isn't allowed to directly contain another "node_modules" directory - let mut base = bun_paths::basename(path); - - // base must - if base.len() > 1 && base[base.len() - 1] == SEP { - base = &base[0..base.len() - 1]; - } - - info.flags - .set_present(DirInfo::Flag::IsNodeModules, base == b"node_modules"); - - // if (entries != null) { - if !info.is_node_modules() { - if let Some(entry) = entries!().get_comptime_query(b"node_modules") { - info.flags.set_present( - DirInfo::Flag::HasNodeModules, - entry.entry().kind(rfs!(), self.store_fd) - == Fs::file_system::EntryKind::Dir, - ); - } - } - - if self.care_about_bin_folder { - 'append_bin_dir: { - if info.has_node_modules() { - if entries!().has_comptime_query(b"node_modules") { - // SAFETY: BIN_FOLDERS guarded by BIN_FOLDERS_LOCK below - if !BIN_FOLDERS_LOADED.load(core::sync::atomic::Ordering::Acquire) { - // SAFETY: callers hold RESOLVER_MUTEX; first init. - unsafe { (*BIN_FOLDERS.get()).write(BinFolderArray::default()) }; - BIN_FOLDERS_LOADED - .store(true, core::sync::atomic::Ordering::Release); - } - - // TODO(port): std.fs.Dir.openDirZ → bun_sys - let Ok(file) = bun_sys::open_dir_z( - fd, - bun_paths::path_literal!(b"node_modules/.bin"), - Default::default(), - ) else { - break 'append_bin_dir; - }; - let _close = bun_sys::CloseOnDrop::new(file); - let Ok(bin_path) = file.get_fd_path(bufs!(node_bin_path)) else { - break 'append_bin_dir; - }; - let _unlock = BIN_FOLDERS_LOCK.lock_guard(); - - // SAFETY: BIN_FOLDERS guarded by BIN_FOLDERS_LOCK acquired above. - unsafe { - for existing_folder in - (*BIN_FOLDERS.get()).assume_init_ref().const_slice() - { - if *existing_folder == bin_path { - break 'append_bin_dir; - } - } - - let Ok(stored) = self.fs_ref().dirname_store.append_slice(bin_path) - else { - break 'append_bin_dir; - }; - let _ = (*BIN_FOLDERS.get()).assume_init_mut().append(stored); - } - } - } - - if info.is_node_modules() { - if let Some(q) = entries!().get_comptime_query(b".bin") { - if q.entry().kind(rfs!(), self.store_fd) - == Fs::file_system::EntryKind::Dir - { - // SAFETY: BIN_FOLDERS_LOADED is single-thread init-once; protected by RESOLVER_MUTEX held by callers. - if !BIN_FOLDERS_LOADED.load(core::sync::atomic::Ordering::Acquire) { - // SAFETY: callers hold RESOLVER_MUTEX; first init. - unsafe { - (*BIN_FOLDERS.get()).write(BinFolderArray::default()) - }; - BIN_FOLDERS_LOADED - .store(true, core::sync::atomic::Ordering::Release); - } - - let Ok(file) = - bun_sys::open_dir_z(fd, b".bin\0", Default::default()) - else { - break 'append_bin_dir; - }; - let _close = bun_sys::CloseOnDrop::new(file); - let Ok(bin_path) = bun_sys::get_fd_path(file, bufs!(node_bin_path)) - else { - break 'append_bin_dir; - }; - let _unlock = BIN_FOLDERS_LOCK.lock_guard(); - - // SAFETY: BIN_FOLDERS guarded by BIN_FOLDERS_LOCK acquired above. - unsafe { - for existing_folder in - (*BIN_FOLDERS.get()).assume_init_ref().const_slice() - { - if *existing_folder == bin_path { - break 'append_bin_dir; - } - } - - let Ok(stored) = - self.fs_ref().dirname_store.append_slice(bin_path) - else { - break 'append_bin_dir; - }; - let _ = (*BIN_FOLDERS.get()).assume_init_mut().append(stored); - } - } - } - } - } - } - // } - - if let Some(parent_) = parent { - // Propagate the browser scope into child directories - info.enclosing_browser_scope = parent_.enclosing_browser_scope; - info.package_json_for_browser_field = parent_.package_json_for_browser_field; - info.enclosing_tsconfig_json = parent_.enclosing_tsconfig_json; - - if let Some(parent_package_json) = parent_.package_json() { - // https://github.com/oven-sh/bun/issues/229 - if !parent_package_json.name.is_empty() || self.care_about_bin_folder { - info.enclosing_package_json = Some(parent_package_json); - } - - if parent_package_json.dependencies.map.count() > 0 - || parent_package_json.package_manager_package_id - != Install::INVALID_PACKAGE_ID - { - // PORT NOTE: store the raw `NonNull` field (not the - // `&'static` accessor result) so mut-provenance flows - // through to `enqueue_dependency_to_resolve`. - info.package_json_for_dependencies = parent_.package_json; - } - } - - info.enclosing_package_json = info - .enclosing_package_json - .or(parent_.enclosing_package_json); - info.package_json_for_dependencies = info - .package_json_for_dependencies - .or(parent_.package_json_for_dependencies); - - // Make sure "absRealPath" is the real path of the directory (resolving any symlinks) - if !self.opts.preserve_symlinks { - if let Some(parent_entries) = parent_.get_entries_ref(self.generation) { - if let Some(lookup) = parent_entries.get(base) { - // `entries_ptr` is a slot in the BSSMap-backed entries singleton — - // route the read-only `.fd` access through the existing - // `entries!()` re-borrow macro instead of a raw-ptr deref. - let entries_fd = entries!().fd; - if entries_fd.is_valid() - && !lookup.entry().cache().fd.is_valid() - && self.store_fd - { - lookup.entry().set_cache_fd(entries_fd); - } - // SAFETY: EntryStore-owned slot; `entries_mutex` held — read-only borrow, - // dies (NLL) before any later `&mut` to this slot. - let entry = lookup.entry(); - - let mut symlink = entry.symlink(rfs!(), self.store_fd); - if !symlink.is_empty() { - if let Some(logs) = self.debug_logs.as_mut() { - let mut buf = Vec::new(); - write!( - &mut buf, - "Resolved symlink \"{}\" to \"{}\"", - bstr::BStr::new(path), - bstr::BStr::new(symlink) - ) - .ok(); - logs.add_note(buf); - } - info.abs_real_path = symlink; - } else if !parent_.abs_real_path.is_empty() { - // this might leak a little i'm not sure - let parts = [parent_.abs_real_path, base]; - // PORT NOTE: split into two statements so the two `&mut FileSystem` - // borrows from `unsafe { &mut *self.fs() }` don't overlap (Stacked Borrows). - let joined = self - .fs_ref() - .abs_buf(&parts, bufs!(dir_info_uncached_filename)); - symlink = self - .fs_ref() - .dirname_store - .append_slice(joined) - .expect("unreachable"); - - if let Some(logs) = self.debug_logs.as_mut() { - let mut buf = Vec::new(); - write!( - &mut buf, - "Resolved symlink \"{}\" to \"{}\"", - bstr::BStr::new(path), - bstr::BStr::new(symlink) - ) - .ok(); - logs.add_note(buf); - } - lookup.entry().set_cache_symlink(PathString::init(symlink)); - info.abs_real_path = symlink; - } - } - } - } - - if parent_.is_node_modules() || parent_.is_inside_node_modules() { - info.flags - .set_present(DirInfo::Flag::InsideNodeModules, true); - } - } - - // Record if this directory has a package.json file - if self.opts.load_package_json { - if let Some(lookup) = entries!().get_comptime_query(b"package.json") { - // SAFETY: EntryStore-owned slot; `entries_mutex` held — read-only borrow, - // dies (NLL) before any later `&mut` to this slot. - let entry = lookup.entry(); - if entry.kind(rfs!(), self.store_fd) == Fs::file_system::EntryKind::File { - info.package_json = if self.use_package_manager() - && !info.has_node_modules() - && !info.is_node_modules() - { - self.parse_package_json::( - path, - if FeatureFlags::STORE_FILE_DESCRIPTORS { - fd - } else { - FD::INVALID - }, - package_id, - ) - .ok() - .flatten() - } else { - self.parse_package_json::( - path, - if FeatureFlags::STORE_FILE_DESCRIPTORS { - fd - } else { - FD::INVALID - }, - None, - ) - .ok() - .flatten() - }; - - if let Some(pkg) = info.package_json() { - if pkg.browser_map.count() > 0 { - info.enclosing_browser_scope = result.index; - info.package_json_for_browser_field = Some(pkg); - } - - if !pkg.name.is_empty() || self.care_about_bin_folder { - info.enclosing_package_json = Some(pkg); - } - - if pkg.dependencies.map.count() > 0 - || pkg.package_manager_package_id != Install::INVALID_PACKAGE_ID - { - // PORT NOTE: store the raw `NonNull` field (not the - // `&'static` accessor result) so mut-provenance flows - // through to `enqueue_dependency_to_resolve`. - info.package_json_for_dependencies = info.package_json; - } - - if let Some(logs) = self.debug_logs.as_mut() { - logs.add_note_fmt(format_args!( - "Resolved package.json in \"{}\"", - bstr::BStr::new(path) - )); - } - } - } - } - } - - // Record if this directory has a tsconfig.json or jsconfig.json file - if self.opts.load_tsconfig_json { - let mut tsconfig_path: Option<&[u8]> = None; - if self.opts.tsconfig_override.is_none() { - if let Some(lookup) = entries!().get_comptime_query(b"tsconfig.json") { - // SAFETY: EntryStore-owned slot; `entries_mutex` held — read-only borrow, - // dies (NLL) before any later `&mut` to this slot. - let entry = lookup.entry(); - if entry.kind(rfs!(), self.store_fd) == Fs::file_system::EntryKind::File { - let parts = [path, b"tsconfig.json".as_slice()]; - tsconfig_path = Some( - self.fs_ref() - .abs_buf(&parts, bufs!(dir_info_uncached_filename)), - ); - } - } - if tsconfig_path.is_none() { - if let Some(lookup) = entries!().get_comptime_query(b"jsconfig.json") { - // SAFETY: EntryStore-owned slot; `entries_mutex` held — read-only borrow, - // dies (NLL) before any later `&mut` to this slot. - let entry = lookup.entry(); - if entry.kind(rfs!(), self.store_fd) == Fs::file_system::EntryKind::File - { - let parts = [path, b"jsconfig.json".as_slice()]; - tsconfig_path = Some( - self.fs_ref() - .abs_buf(&parts, bufs!(dir_info_uncached_filename)), - ); - } - } - } - } else if parent.is_none() { - // PORT NOTE: re-borrow as 'static so the `&self.opts` borrow ends before - // `self.parse_tsconfig(&mut self, ...)`. `tsconfig_override` is owned by - // BundleOptions (lives for the resolver's lifetime). - tsconfig_path = self - .opts - .tsconfig_override - .as_deref() - .map(|s| unsafe { &*std::ptr::from_ref::<[u8]>(s) }); - } - - if let Some(tsconfigpath) = tsconfig_path { - let parsed_tsconfig: Option<*mut TSConfigJSON> = match self.parse_tsconfig( - tsconfigpath, - if FeatureFlags::STORE_FILE_DESCRIPTORS { - fd - } else { - FD::ZERO - }, - ) { - Ok(v) => v.map(bun_core::heap::into_raw), - Err(err) => { - let pretty = tsconfigpath; - if err == bun_core::err!("ENOENT") - || err == bun_core::err!("FileNotFound") - { - let _ = self.log_mut().add_error_fmt( - None, - bun_ast::Loc::EMPTY, - format_args!( - "Cannot find tsconfig file {}", - bun_core::fmt::quote(pretty) - ), - ); - } else if err != bun_core::err!("ParseErrorAlreadyLogged") - && err != bun_core::err!("IsDir") - && err != bun_core::err!("EISDIR") - { - let _ = self.log_mut().add_error_fmt( - None, - bun_ast::Loc::EMPTY, - format_args!( - "Cannot read file {}: {}", - bun_core::fmt::quote(pretty), - bstr::BStr::new(err.name()) - ), - ); - } - None - } - }; - // PORT NOTE: spec resolver.zig:4207 assigns info.tsconfig_json here (a raw - // ?*TSConfigJSON), then frees that allocation in the merge loop below before - // reassigning. With Rust references (Option<&'static TSConfigJSON>, dir_info.rs) - // that briefly-dangling state is UB. Defer the assignment to after the merge — - // it is always overwritten when parsed_tsconfig.is_some(), and DirInfo defaults - // tsconfig_json to None otherwise. - if let Some(tsconfig_json) = parsed_tsconfig { - let mut parent_configs: BoundedArray<*mut TSConfigJSON, 64> = - BoundedArray::default(); - parent_configs.append(tsconfig_json)?; - // `current`/`parent_config_ptr`/`merged_config` are heap TSConfigJSON - // allocations from `parse_tsconfig` (heap::alloc); uniquely owned by - // this extends-chain walk and freed via heap::take below. Hold as - // `BackRef` (pointee outlives holder) so the loop body reads via safe - // `Deref` instead of three open-coded raw-ptr derefs. - let mut current = bun_ptr::BackRef::from( - core::ptr::NonNull::new(tsconfig_json).expect("heap alloc"), - ); - while !current.extends.is_empty() { - let ts_dir_name = Dirname::dirname(¤t.abs_path); - let abs_path = ResolvePath::join_abs_string_buf( - ts_dir_name, - bufs!(tsconfig_path_abs), - &[ts_dir_name, ¤t.extends], - bun_paths::Platform::AUTO, - ); - let parent_config_maybe: Option<*mut TSConfigJSON> = - match self.parse_tsconfig(abs_path, FD::INVALID) { - Ok(v) => v.map(bun_core::heap::into_raw), - Err(err) => { - let _ = self.log_mut().add_debug_fmt( - None, - bun_ast::Loc::EMPTY, - format_args!( - "{} loading tsconfig.json extends {}", - bstr::BStr::new(err.name()), - bun_core::fmt::quote(abs_path) - ), - ); - break; - } - }; - if let Some(parent_config) = parent_config_maybe { - parent_configs.append(parent_config)?; - current = bun_ptr::BackRef::from( - core::ptr::NonNull::new(parent_config).expect("heap alloc"), - ); - } else { - break; - } - } - - let mut merged_config = parent_configs.pop().unwrap(); - // starting from the base config (end of the list) - // successively apply the inheritable attributes to the next config - while let Some(parent_config_ptr) = parent_configs.pop() { - // SAFETY: see loop-wide note above. - let parent_config = unsafe { &mut *parent_config_ptr }; - // SAFETY: see loop-wide note above. - let mc = unsafe { &mut *merged_config }; - mc.emit_decorator_metadata = - mc.emit_decorator_metadata || parent_config.emit_decorator_metadata; - if !parent_config.base_url.is_empty() { - mc.base_url = core::mem::take(&mut parent_config.base_url); - } - mc.jsx = parent_config.merge_jsx(mc.jsx.clone()); - mc.jsx_flags.insert_all(parent_config.jsx_flags); - - if let Some(value) = parent_config.preserve_imports_not_used_as_values { - mc.preserve_imports_not_used_as_values = Some(value); - } - - // TypeScript replaces paths across extends (child overrides parent - // entirely), so when a more-specific config defines paths, replace - // rather than merge. base_url_for_paths is set whenever the paths - // key is present in the JSON (even if empty), so it discriminates - // "not defined" from "defined as {}" — the latter clears inherited - // paths per TypeScript semantics. - if !parent_config.base_url_for_paths.is_empty() { - // The previous merged_config.paths is being replaced; free its - // backing storage before overwriting so the PathsMap from the - // deeper config doesn't leak. Each value is a []string slice - // that was separately heap-allocated in TSConfigJSON.parse() - // (tsconfig_json.zig), so free those before the map itself. - // (In Rust, dropping the map frees values automatically.) - mc.paths = core::mem::take(&mut parent_config.paths); - mc.base_url_for_paths = - core::mem::take(&mut parent_config.base_url_for_paths); - } else { - // paths were not moved to merged_config, so they're still owned - // by parent_config. base_url_for_paths.len == 0 implies the map - // is empty (it's only set when the `paths` key is present in the - // JSON), so this is a no-op but documents the ownership. - // (Drop handles parent_config.paths.) - } - // Every scalar/reference we need has been copied into merged_config - // (strings live in dirname_store or default_allocator and outlive the - // struct). The heap-allocated TSConfigJSON itself is no longer needed; - // without this, every intermediate config in an extends chain leaks on - // each dirInfoUncached() call, which is especially bad under HMR where - // bustDirCache triggers a re-parse of the whole chain on every reload. - // SAFETY: parent_config_ptr came from TSConfigJSON::new (heap::alloc) - TSConfigJSON::destroy(unsafe { - bun_core::heap::take(parent_config_ptr) - }); - } - // `merged_config` is a leaked Box (heap::alloc) interned into DirInfo; outlives the resolver. - info.tsconfig_json = Some( - core::ptr::NonNull::new(merged_config) - .expect("heap::alloc is non-null"), - ); - } - info.enclosing_tsconfig_json = info.tsconfig_json(); - } - } - - Ok(()) - } - } - - impl<'a> Resolver<'a> { - /// Port of `pub fn deinit(r: *ThisResolver)` (resolver.zig:601-604). - /// - /// PORT NOTE: NOT `impl Drop` — the bundler builds a `Resolver` per worker - /// thread (see `for_worker`), and all instances share the same `dir_cache` - /// singleton. A `Drop` impl would fire once per worker going out of scope, - /// resetting the SHARED cache (freeing PackageJSON/TSConfigJSON, closing cached - /// fds) while other live Resolvers still hold pointers into it. Spec calls - /// `deinit` explicitly exactly once at shutdown; mirror that. - pub fn deinit(&mut self) { - // Caller is the sole remaining owner at shutdown; no other Resolver alias is live. - for di in self.dir_cache_mut().values_mut() { - // Zig: `di.deinit()` — releases owned PackageJSON / TSConfigJSON resources - // in-place (side effects beyond memory: those Drops close cached fds / - // deref intrusive refcounts). Ported as `DirInfo::reset`. - di.reset(); - } - // dir_cache is &'static — do not deinit the singleton here - // TODO(port): Zig calls dir_cache.deinit() but it's a global BSSMap; revisit ownership - } - } - - // ─── nested helper types ─────────────────────────────────────────────────── - - enum DependencyToResolve { - NotFound, - Pending(PendingResolution), - Failure(bun_core::Error), - Resolution(Resolution), - } - - #[derive(Clone, Copy, PartialEq, Eq, core::marker::ConstParamTy)] - pub enum BrowserMapPathKind { - PackagePath, - AbsolutePath, - } - - pub struct BrowserMapPath<'b> { - pub remapped: &'static [u8], - pub cleaned: &'b [u8], - pub input_path: &'b [u8], - pub extension_order: &'b [Box<[u8]>], - pub map: &'b BrowserMap, - } - - impl<'b> BrowserMapPath<'b> { - pub fn check_path(&mut self, path_to_check: &[u8]) -> bool { - let map = self.map; - - let cleaned = self.cleaned; - // Check for equality - if let Some(result) = map.get(path_to_check) { - // SAFETY: ARENA — `BrowserMap` values are `Box<[u8]>` owned by a `'static` - // PackageJSON (allocated in `parse_package_json`, never freed — DirInfo - // cache is process-global); the `'b` borrow on `map` artificially shortens - // what is process-lifetime storage. `Interned` is the canonical proof type. - self.remapped = unsafe { bun_ptr::Interned::assume(result) }.as_bytes(); - // SAFETY: TODO(port): lifetime — extending borrow of caller-owned slice; consumed before checker is dropped. - self.input_path = unsafe { &*std::ptr::from_ref::<[u8]>(path_to_check) }; - return true; - } - - let ext_buf = bufs!(extension_path); - - if cleaned.len() <= ext_buf.len() { - ext_buf[..cleaned.len()].copy_from_slice(cleaned); - - // If that failed, try adding implicit extensions - for ext in self.extension_order.iter() { - let ext: &[u8] = ext; - if cleaned.len() + ext.len() > ext_buf.len() { - continue; - } - ext_buf[cleaned.len()..cleaned.len() + ext.len()].copy_from_slice(ext); - let new_path = &ext_buf[0..cleaned.len() + ext.len()]; - // if let Some(debug) = r.debug_logs.as_mut() { - // debug.add_note_fmt(format_args!("Checking for \"{}\" ", bstr::BStr::new(new_path))); - // } - if let Some(_remapped) = map.get(new_path) { - // SAFETY: ARENA — see `result` note above. - self.remapped = unsafe { bun_ptr::Interned::assume(_remapped) }.as_bytes(); - // SAFETY: TODO(port): lifetime — `new_path` borrows the threadlocal `extension_path` buf; consumed before next overwrite. - self.cleaned = unsafe { &*std::ptr::from_ref::<[u8]>(new_path) }; - // SAFETY: same as above. - self.input_path = unsafe { &*std::ptr::from_ref::<[u8]>(new_path) }; - return true; - } - } - } - - // If that failed, try assuming this is a directory and looking for an "index" file - - let index_path: &[u8] = { - let trimmed = strings::trim_right(path_to_check, &[SEP]); - let parts = [ - trimmed, - const_format::concatcp!(SEP_STR, "index").as_bytes(), - ]; - ResolvePath::join_string_buf( - bufs!(tsconfig_base_url), - &parts, - bun_paths::Platform::AUTO, - ) - }; - - if let Some(_remapped) = map.get(index_path) { - // SAFETY: ARENA — see `result` note above. - self.remapped = unsafe { bun_ptr::Interned::assume(_remapped) }.as_bytes(); - // SAFETY: TODO(port): lifetime — `index_path` borrows the threadlocal `extension_path` buf; consumed before next overwrite. - self.input_path = unsafe { &*std::ptr::from_ref::<[u8]>(index_path) }; - return true; - } - - if index_path.len() <= ext_buf.len() { - ext_buf[..index_path.len()].copy_from_slice(index_path); - - for ext in self.extension_order.iter() { - let ext: &[u8] = ext; - if index_path.len() + ext.len() > ext_buf.len() { - continue; - } - ext_buf[index_path.len()..index_path.len() + ext.len()].copy_from_slice(ext); - let new_path = &ext_buf[0..index_path.len() + ext.len()]; - // if let Some(debug) = r.debug_logs.as_mut() { - // debug.add_note_fmt(format_args!("Checking for \"{}\" ", bstr::BStr::new(new_path))); - // } - if let Some(_remapped) = map.get(new_path) { - // SAFETY: ARENA — see `result` note above. - self.remapped = unsafe { bun_ptr::Interned::assume(_remapped) }.as_bytes(); - // SAFETY: TODO(port): lifetime — `new_path` borrows the threadlocal `extension_path` buf; consumed before next overwrite. - self.cleaned = unsafe { &*std::ptr::from_ref::<[u8]>(new_path) }; - // SAFETY: same as above. - self.input_path = unsafe { &*std::ptr::from_ref::<[u8]>(new_path) }; - return true; - } - } - } - - false - } - } - - #[inline] - fn is_dot_slash(path: &[u8]) -> bool { - #[cfg(not(windows))] - { - path == b"./" - } - #[cfg(windows)] - { - path.len() == 2 && path[0] == b'.' && strings::char_is_any_slash(path[1]) - } - } - - // ModuleTypeMap = bun.ComptimeStringMap(options.ModuleType, .{...}) - // - // PERF(port): was `phf::Map<&[u8], ModuleType>`. With only 4 keys — all - // length 4 — the phf hash + index probe is strictly more work than a single - // length gate followed by 4-byte compares (which LLVM lowers to one u32 - // load + compare per arm once `len == 4` is established). Mirrors the - // length-gated dispatch used in `clap::find_param`. - #[inline] - fn module_type_from_ext(ext: &[u8]) -> Option { - if ext.len() != 4 { - return None; - } - match ext { - b".mjs" | b".mts" => Some(options::ModuleType::Esm), - b".cjs" | b".cts" => Some(options::ModuleType::Cjs), - _ => None, - } - } - - const NODE_MODULE_ROOT_STRING: &[u8] = - const_format::concatcp!(SEP_STR, "node_modules", SEP_STR).as_bytes(); - - // `dev` scope (Output.scoped(.Resolver, .visible)) — same scope name as `debuglog` but visible. - // Folded into the same `Resolver` declared scope; visibility distinction handled in Phase B. - - pub struct Dirname; - - impl Dirname { - /// NOT `std.fs.path.dirname`. Resolver-specific upward-traversal dirname - /// (resolver.zig:4297): returns trailing-sep-INCLUSIVE slice, never `None`, - /// `is_sep_any` on all platforms. Do NOT replace with `bun_core::dirname`. - pub fn dirname(path: &[u8]) -> &[u8] { - if path.is_empty() { - return SEP_STR.as_bytes(); - } - - let root: &[u8] = { - #[cfg(windows)] - { - let root = ResolvePath::windows_filesystem_root(path); - // Preserve the trailing slash for UNC paths. - // Going from `\\server\share\folder` should end up - // at `\\server\share\`, not `\\server\share` - if root.len() >= 5 && path.len() > root.len() { - &path[0..root.len() + 1] - } else { - root - } - } - #[cfg(not(windows))] - { - b"/" - } - }; - - let mut end_index: usize = path.len() - 1; - while bun_paths::is_sep_any(path[end_index]) { - if end_index == 0 { - return root; - } - end_index -= 1; - } - - while !bun_paths::is_sep_any(path[end_index]) { - if end_index == 0 { - return root; - } - end_index -= 1; - } - - if end_index == 0 && bun_paths::is_sep_any(path[0]) { - return &path[0..1]; - } - - if end_index == 0 { - return root; - } - - &path[0..end_index + 1] - } - } - - pub struct RootPathPair<'b> { - pub base_path: &'b [u8], - pub package_json: *const PackageJSON, - } - - // ported from: src/resolver/resolver.zig -} // end mod __phase_a_body +// Resolver implementation modules. Each file declares the sibling-crate `use`s +// it needs; cross-file references go through `crate::*` paths. +pub mod allocators; +pub mod options; +pub mod resolver; +pub mod result; +pub mod standalone_module_graph; diff --git a/src/resolver/options.rs b/src/resolver/options.rs new file mode 100644 index 00000000000..dab1bd9fd3c --- /dev/null +++ b/src/resolver/options.rs @@ -0,0 +1,357 @@ +//! Resolver-tier `options` — the canonical resolver-input types. +//! +//! MOVE_DOWN COMPLETE for the resolver↔bundler cycle: these are the types the +//! resolver reads, defined at the lowest tier that can name all their parts +//! (`jsx::Pragma`/`ConditionsMap` live in this crate; `Target`/`Loader` in +//! `bun_options_types`). `bun_bundler::options::BundleOptions` is the ~200-field +//! CLI/config aggregate; `bun_bundler::transpiler::resolver_bundle_options_subset` +//! projects it into this struct for `Resolver::init1`. These are NOT a re-decl +//! of the bundler type — the bundler depends on this crate and re-exports them. + +pub use crate::tsconfig_json::options::jsx; +pub(crate) use bun_ast::{Loader, LoaderHashTable, Target}; +pub use bun_options_types::bundle_enums::ModuleType; + +/// Port of `bundler/options.zig` `Packages`. +#[derive(Clone, Copy, PartialEq, Eq, Default)] +pub enum Packages { + #[default] + Bundle, + External, +} + +/// Port of `bundler/options.zig` `ExternalModules`. +#[derive(Default)] +pub struct ExternalModules { + pub patterns: Vec, + pub abs_paths: StringSet, + pub node_modules: StringSet, +} +impl Clone for ExternalModules { + fn clone(&self) -> Self { + // `StringSet::clone` is an inherent fallible method (returns + // `Result<_, AllocError>`), so this can't be `#[derive(Clone)]`. + Self { + patterns: self.patterns.clone(), + abs_paths: self.abs_paths.clone().expect("oom"), + node_modules: self.node_modules.clone().expect("oom"), + } + } +} +#[derive(Debug, Clone)] +pub struct WildcardPattern { + pub prefix: Box<[u8]>, + pub suffix: Box<[u8]>, +} +/// Re-export the real set type so `bun_bundler` can project user-supplied +/// `--external` `abs_paths`/`node_modules` through. The previous local ZST +/// stub returned `count() == 0` / `contains(..) == false`, so the resolver +/// silently ignored every `--external` absolute path / package name. +pub use bun_collections::StringSet; + +/// Port of `bundler/options.zig` `Conditions`. +#[derive(Default)] +pub struct Conditions { + pub import: crate::package_json::ConditionsMap, + pub require: crate::package_json::ConditionsMap, + pub style: crate::package_json::ConditionsMap, +} + +/// `Copy` tag selecting one of the extension-order lists owned by +/// [`BundleOptions`]. Replaces the previous `*const [Box<[u8]>]` +/// self-reference (`Resolver.extension_order` pointing into +/// `Resolver.opts`) with a value type — the Zig save/restore pattern +/// (`resolver.zig:691-696` etc.) survives unchanged because the tag is +/// `Copy`, and the actual slice is resolved on demand via +/// [`BundleOptions::ext_order_slice`] / [`Resolver::extension_order`]. +#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] +pub enum ExtOrder { + /// `opts.extension_order.default.default` + #[default] + DefaultDefault, + /// `opts.extension_order.default.esm` + DefaultEsm, + /// `opts.extension_order.node_modules.default` + NodeModulesDefault, + /// `opts.extension_order.node_modules.esm` + NodeModulesEsm, + /// `opts.extension_order.css` (Zig reads `Defaults.CssExtensionOrder` directly) + Css, + /// `opts.main_field_extension_order` — used when resolving the `"main"` + /// package.json field (`resolver.zig:3703,3715,3721`). + MainField, +} + +/// Convert a `&[&[u8]]` default constant into the owned form the resolver +/// stores. Mirrors `bun_bundler::options::owned_string_list`. +pub fn owned_string_list(s: &[&[u8]]) -> Box<[Box<[u8]>]> { + s.iter().map(|s| Box::<[u8]>::from(*s)).collect() +} + +/// Port of `bundler/options.zig` `ResolveFileExtensions`. +pub struct ExtensionOrder { + pub default: ExtensionOrderGroup, + pub node_modules: ExtensionOrderGroup, + /// Not on the bundler-side struct — the spec resolver reads + /// `Defaults.CssExtensionOrder` directly. Stored here so every + /// [`ExtOrder`] tag resolves into storage with the same owner/lifetime. + pub css: Box<[Box<[u8]>]>, +} +pub struct ExtensionOrderGroup { + pub default: Box<[Box<[u8]>]>, + pub esm: Box<[Box<[u8]>]>, +} +impl Default for ExtensionOrderGroup { + fn default() -> Self { + ExtensionOrderGroup { + default: owned_string_list(bundle_options::defaults::EXTENSION_ORDER), + esm: owned_string_list(bundle_options::defaults::MODULE_EXTENSION_ORDER), + } + } +} +impl Default for ExtensionOrder { + fn default() -> Self { + ExtensionOrder { + default: ExtensionOrderGroup::default(), + node_modules: ExtensionOrderGroup { + default: owned_string_list(bundle_options::defaults::node_modules::EXTENSION_ORDER), + esm: owned_string_list( + bundle_options::defaults::node_modules::MODULE_EXTENSION_ORDER, + ), + }, + css: owned_string_list(bundle_options::defaults::CSS_EXTENSION_ORDER), + } + } +} +impl ExtensionOrder { + /// Port of `options.zig` `ResolveFileExtensions.kind`. Returns the + /// [`ExtOrder`] tag; resolve to a slice via + /// [`BundleOptions::ext_order_slice`]. + pub fn kind(&self, kind: bun_ast::ImportKind, is_node_modules: bool) -> ExtOrder { + use bun_ast::ImportKind as K; + match kind { + K::Url | K::AtConditional | K::At => ExtOrder::Css, + K::Stmt | K::EntryPointBuild | K::EntryPointRun | K::Dynamic => { + if is_node_modules { + ExtOrder::NodeModulesEsm + } else { + ExtOrder::DefaultEsm + } + } + _ => { + if is_node_modules { + ExtOrder::NodeModulesDefault + } else { + ExtOrder::DefaultDefault + } + } + } + } +} + +impl BundleOptions { + /// Resolve an [`ExtOrder`] tag to the slice it names inside `self`. + /// All targets are `Box<[Box<[u8]>]>` owned by `self` and never + /// reallocated after `Resolver::init1`, so the returned borrow is + /// stable for the resolver's lifetime. + #[inline] + pub fn ext_order_slice(&self, tag: ExtOrder) -> &[Box<[u8]>] { + match tag { + ExtOrder::DefaultDefault => &self.extension_order.default.default, + ExtOrder::DefaultEsm => &self.extension_order.default.esm, + ExtOrder::NodeModulesDefault => &self.extension_order.node_modules.default, + ExtOrder::NodeModulesEsm => &self.extension_order.node_modules.esm, + ExtOrder::Css => &self.extension_order.css, + ExtOrder::MainField => &self.main_field_extension_order, + } + } +} + +pub mod bundle_options { + pub use super::ForceNodeEnv; + pub mod defaults { + pub const CSS_EXTENSION_ORDER: &[&[u8]] = &[b".css"]; + // Mirrors `bun_bundler::options::bundle_options_defaults::EXTENSION_ORDER` + // / `MODULE_EXTENSION_ORDER` — duplicated so `Default for BundleOptions` + // below is self-contained (resolver sits below bundler in the dep graph). + pub const EXTENSION_ORDER: &[&[u8]] = &[ + b".tsx", b".ts", b".jsx", b".cts", b".cjs", b".js", b".mjs", b".mts", b".json", + ]; + pub const MODULE_EXTENSION_ORDER: &[&[u8]] = &[ + b".tsx", b".jsx", b".mts", b".ts", b".mjs", b".js", b".cts", b".cjs", b".json", + ]; + /// Mirrors `bun_bundler::options::bundle_options_defaults::node_modules`. + pub mod node_modules { + pub const EXTENSION_ORDER: &[&[u8]] = &[ + b".jsx", b".cjs", b".js", b".mjs", b".mts", b".tsx", b".ts", b".cts", b".json", + ]; + pub const MODULE_EXTENSION_ORDER: &[&[u8]] = &[ + b".mjs", b".jsx", b".js", b".mts", b".tsx", b".ts", b".cjs", b".cts", b".json", + ]; + } + } +} + +// B-3 UNIFIED: FORWARD_DECL dropped — canonical type moved down to +// `bun_options_types::bundle_enums::ForceNodeEnv`. Re-exported so the +// `options::ForceNodeEnv` / `bundle_options::ForceNodeEnv` paths and the +// field on the local `BundleOptions` subset stay source-compatible. +pub use ::bun_options_types::ForceNodeEnv; + +/// Port of `bundler/options.zig` `Framework` (Bake) — only the +/// `built_in_modules` field, which is the sole resolver-read member. +pub struct Framework { + pub built_in_modules: bun_collections::StringArrayHashMap, +} + +/// Resolver-tier `BundleOptions` — the canonical resolver-input struct. +/// `bun_bundler::options::BundleOptions` (the ~200-field CLI/config +/// aggregate) projects into this via +/// `bun_bundler::transpiler::resolver_bundle_options_subset`; the bundler +/// depends on this crate, so this type is the lower-tier source of truth +/// for everything resolution reads. +pub struct BundleOptions { + pub target: Target, + pub packages: Packages, + pub jsx: jsx::Pragma, + pub extension_order: ExtensionOrder, + pub conditions: Conditions, + pub external: ExternalModules, + pub extra_cjs_extensions: Box<[Box<[u8]>]>, + pub framework: Option, + pub global_cache: bun_options_types::global_cache::GlobalCache, + // Zig: `?*api.BunInstall` (options.zig:1753). Spec consumer + // `PackageManagerOptions.zig:load` only reads through it; the bundler + // projects this from its own `Option>` field + // (CLI-owned `Box`, process-lifetime). + pub install: Option>, + pub load_package_json: bool, + pub load_tsconfig_json: bool, + pub main_field_extension_order: Box<[Box<[u8]>]>, + pub main_fields: Box<[Box<[u8]>]>, + /// Spec resolver.zig `auto_main` compares the *pointer* of + /// `opts.main_fields` against `Target.DefaultMainFields.get(target)` to + /// detect "user did not pass --main-fields". The bundler stores an owned + /// `Box<[Box<[u8]>]>` whose pointer can never match a static, so the + /// bundler projects this flag explicitly instead. + pub main_fields_is_default: bool, + pub mark_builtins_as_external: bool, + pub polyfill_node_globals: bool, + pub prefer_offline_install: bool, + pub preserve_symlinks: bool, + pub rewrite_jest_for_tests: bool, + pub tsconfig_override: Option>, + pub production: bool, + pub force_node_env: ForceNodeEnv, + // Bundler-only fields read via `c.resolver.opts` in + // `linker_context/*` (Zig stores the full `BundleOptions` on the + // resolver). Projected by `bun_bundler` at link time. + pub output_dir: Box<[u8]>, + pub root_dir: Box<[u8]>, + pub public_path: Box<[u8]>, + pub compile: bool, + pub supports_multiple_outputs: bool, + pub tree_shaking: bool, + pub allow_runtime: bool, +} + +impl Default for BundleOptions { + /// Spec: `options.zig` field-init defaults. Only the fields the resolver + /// reads — `bun_bundler::Transpiler::init` overlays the per-field + /// projections it can map (target/packages/jsx/bools/global_cache/…) + /// before handing this to `Resolver::init1`. + fn default() -> Self { + BundleOptions { + target: Target::default(), + packages: Packages::default(), + jsx: jsx::Pragma::default(), + extension_order: ExtensionOrder::default(), + conditions: Conditions::default(), + external: ExternalModules::default(), + extra_cjs_extensions: Box::default(), + framework: None, + global_cache: Default::default(), + install: None, + load_package_json: true, + load_tsconfig_json: true, + main_field_extension_order: owned_string_list( + bundle_options::defaults::EXTENSION_ORDER, + ), + main_fields: owned_string_list(DEFAULT_MAIN_FIELDS.get(Target::default())), + main_fields_is_default: true, + mark_builtins_as_external: false, + polyfill_node_globals: false, + prefer_offline_install: false, + preserve_symlinks: false, + rewrite_jest_for_tests: false, + tsconfig_override: None, + output_dir: Box::default(), + root_dir: Box::default(), + public_path: Box::default(), + compile: false, + supports_multiple_outputs: true, + tree_shaking: false, + allow_runtime: true, + production: false, + force_node_env: ForceNodeEnv::default(), + } + } +} + +impl BundleOptions { + /// Port of `options.zig:1825 BundleOptions.setProduction`. + pub fn set_production(&mut self, value: bool) { + if self.force_node_env == ForceNodeEnv::Unspecified { + self.production = value; + self.jsx.development = !value; + } + } +} + +// Port of `bundler/options.zig` `Target.DefaultMainFields`. +// +// These are the per-target default `--main-fields` orderings. `BundleOptions.main_fields` +// is initialised to alias one of these slices (see options.zig:1712 / 2022), and the +// resolver's `auto_main` heuristic at `load_as_main_field` compares the *pointer* of +// `opts.main_fields` against `DEFAULT_MAIN_FIELDS.get(opts.target)` to detect whether the +// user explicitly set a main-fields list. The previous `&[]` stub made that check always +// false, silently disabling the module-vs-main dual-resolution path. +pub struct TargetMainFields; + +// Note that this means if a package specifies "module" and "main", the ES6 +// module will not be selected. This means tree shaking will not work when +// targeting node environments. +// +// Some packages incorrectly treat the "module" field as "code for the browser". It +// actually means "code for ES6 environments" which includes both node and the browser. +// +// For example, the package "@firebase/app" prints a warning on startup about +// the bundler incorrectly using code meant for the browser if the bundler +// selects the "module" field instead of the "main" field. +// +// This is unfortunate but it's a problem on the side of those packages. +// They won't work correctly with other popular bundlers (with node as a target) anyway. +static DEFAULT_MAIN_FIELDS_NODE: &[&[u8]] = &[b"main", b"module"]; + +// Note that this means if a package specifies "main", "module", and +// "browser" then "browser" will win out over "module". This is the +// same behavior as webpack: https://github.com/webpack/webpack/issues/4674. +// +// This is deliberate because the presence of the "browser" field is a +// good signal that this should be preferred. Some older packages might only use CJS in their "browser" +// but in such a case they probably don't have any ESM files anyway. +static DEFAULT_MAIN_FIELDS_BROWSER: &[&[u8]] = &[b"browser", b"module", b"jsnext:main", b"main"]; +static DEFAULT_MAIN_FIELDS_BUN: &[&[u8]] = &[b"module", b"main", b"jsnext:main"]; + +impl TargetMainFields { + pub fn get(&self, t: Target) -> &'static [&'static [u8]] { + match t { + Target::Node => DEFAULT_MAIN_FIELDS_NODE, + Target::Browser => DEFAULT_MAIN_FIELDS_BROWSER, + Target::Bun | Target::BunMacro | Target::BakeServerComponentsSsr => { + DEFAULT_MAIN_FIELDS_BUN + } + } + } +} +pub const DEFAULT_MAIN_FIELDS: TargetMainFields = TargetMainFields; diff --git a/src/resolver/resolver.rs b/src/resolver/resolver.rs new file mode 100644 index 00000000000..bb3da02f206 --- /dev/null +++ b/src/resolver/resolver.rs @@ -0,0 +1,6640 @@ +//! The [`Resolver`] state machine: import-path resolution against the on-disk +//! filesystem, `node_modules` tree, `tsconfig.json` paths, package `exports`, +//! `browser` maps, and the standalone (compiled) module graph. Holds the +//! Zig→Rust port of `src/resolver/resolver.zig`'s `Resolver` struct and its +//! impl, plus the local helper shims (`bun_paths` value-dispatch joins, +//! `bun_sys` dir-open wrappers, `FdExt`) and threadlocal scratch buffers. + +use crate::{is_package_path, is_package_path_not_absolute}; + +use core::ptr::NonNull; +use std::io::Write as _; + +// ── Cross-crate type surface ────────────────────────────────────────────── +// Higher-tier symbols are reached through lower-tier crates: +// • install value types + AutoInstaller trait — bun_install_types (MOVE_DOWN) +// • HardcodedModule alias table — bun_resolve_builtins +// • StandaloneModuleGraph — trait below; impl in bun_standalone_graph +// • perf / crash_handler — real bun_perf / bun_crash_handler +use ::bun_install_types::resolver_hooks as Install; +use ::bun_install_types::resolver_hooks::{ + AutoInstaller, EnqueueResult, Features as InstallFeatures, PreinstallState, Resolution, + TaskCallbackContext, WakeHandler, +}; +use ::bun_semver as Semver; +// Re-exported so downstream (bun_bundler) can name the trait in +// `Transpiler::get_package_manager`'s return type without a direct +// `bun_install_types` dep (LAYERING: pass-through, no new edge). +pub use ::bun_install_types::resolver_hooks::AutoInstaller as PackageManagerTrait; + +// LAYERING: `PackageManager.initWithRuntime` (Zig resolver.zig:540) lives in +// `bun_install`, which depends on this crate. The lazy-init body is defined +// `#[no_mangle]` in `bun_install::auto_installer` and resolved at link time +// (same pattern as `__bun_regex_*` / `__BUN_RUNTIME_HOOKS`). `install` is the +// `?*Api.BunInstall` (`self.opts.install`); `env` is the `*DotEnv.Loader` +// (lifetime-erased to `'static` — the install crate stores it as a raw +// `NonNull>`, matching Zig's untracked `*DotEnv.Loader`). +unsafe extern "Rust" { + /// SAFETY (genuine FFI precondition — NOT a `safe fn` candidate): impl + /// reborrows `&mut *log` / `&mut *env` and reads `*install` if non-null. + /// All three must point at process-lifetime Transpiler-owned storage; the + /// returned `NonNull` names the `'static` `PackageManager` singleton. + fn __bun_resolver_init_package_manager( + log: NonNull, + install: Option>, + env: NonNull>, + ) -> NonNull; +} +use crate::cache::Set as CacheSet; +use ::bun_resolve_builtins::{Alias as HardcodedAlias, Cfg as HardcodedAliasCfg}; + +/// `Dependency` namespace as the body spells it (Zig: `Dependency.Version` / +/// `Dependency.Behavior`). Re-exports the canonical `bun_install_types` items. +pub mod Dependency { + pub use ::bun_install_types::resolver_hooks::{ + Behavior, Dependency, DependencyVersion as Version, DependencyVersionTag, + }; + pub mod version { + pub use ::bun_install_types::resolver_hooks::DependencyVersionTag as Tag; + } +} + +/// Transitional re-export module: `package_json.rs` and a few external crates +/// still spell these paths via `__forward_decls`; the items are now real +/// re-exports of `bun_install_types` (no local stubs). +pub(crate) mod __forward_decls { + pub(crate) use crate::cache::{Entry as FsCacheEntry, Fs as FsCache, Set as CacheSet}; + pub(crate) use ::bun_install_types::resolver_hooks as Install; + pub(crate) use ::bun_install_types::resolver_hooks::Resolution; +} +// bun_paths shim — value-dispatched join helpers over `resolve_path::Platform`. +// `dirname` (`Option`-returning, `std.fs.path.dirname` semantics) and +// `PosixToWinNormalizer` are the real `::bun_paths` items — brought in by the +// glob / explicit re-export below, no local re-implementation. +mod bun_paths { + pub(super) use ::bun_paths::resolve_path::PosixToWinNormalizer; + pub(super) use ::bun_paths::resolve_path::is_sep_any; + pub(super) use ::bun_paths::*; + + /// Value-dispatch over `Platform` to the const-generic `PlatformT` + /// monomorphizations in `resolve_path`. The resolver body threads + /// `Platform::AUTO` / `Platform::Loose` at runtime (carried over from Zig's + /// `comptime _platform: Platform` callsites that took a function param). + macro_rules! dispatch_platform { + ($p:expr, |$P:ident| $body:expr) => {{ + use ::bun_paths::resolve_path::{self as rp, platform}; + match $p { + rp::Platform::Loose => { + type $P = platform::Loose; + $body + } + rp::Platform::Windows => { + type $P = platform::Windows; + $body + } + rp::Platform::Posix => { + type $P = platform::Posix; + $body + } + rp::Platform::Nt => { + type $P = platform::Nt; + $body + } + } + }}; + } + pub(super) fn dirname_platform(p: &[u8], platform: Platform) -> &[u8] { + dispatch_platform!(platform, |P| ::bun_paths::resolve_path::dirname::

(p)) + } + /// Port of `bun.path.joinAbsStringBuf` (value-dispatched). + pub(super) fn join_abs_string_buf<'b>( + cwd: &'b [u8], + buf: &'b mut [u8], + parts: &[&[u8]], + platform: Platform, + ) -> &'b [u8] { + dispatch_platform!( + platform, + |P| ::bun_paths::resolve_path::join_abs_string_buf::

(cwd, buf, parts) + ) + } + pub(super) fn join_abs(cwd: &[u8], platform: Platform, part: &[u8]) -> &'static [u8] { + // PORT NOTE: `resolve_path::join_abs` ties the result lifetime to `cwd`, but the + // returned slice always points into the threadlocal `PARSER_JOIN_INPUT_BUFFER` + // (or is `cwd` itself when `parts.is_empty()`, which never happens here — we + // pass exactly one part). Re-erase to `'static` so the resolver can hold it + // across `&mut self` calls. + let s = dispatch_platform!(platform, |P| ::bun_paths::resolve_path::join_abs::

( + cwd, part + )); + // SAFETY: see PORT NOTE — slice borrows threadlocal storage, valid 'static per-thread. + unsafe { bun_ptr::detach_lifetime(s) } + } + pub(super) fn join(parts: &[&[u8]], platform: Platform) -> &'static [u8] { + dispatch_platform!(platform, |P| ::bun_paths::resolve_path::join::

(parts)) + } + pub(super) fn join_string_buf<'b>( + buf: &'b mut [u8], + parts: &[&[u8]], + platform: Platform, + ) -> &'b [u8] { + dispatch_platform!( + platform, + |P| ::bun_paths::resolve_path::join_string_buf::

(buf, parts) + ) + } + /// Zig `bun.pathLiteral` — compile-time platform-separator literal. Zig + /// rewrites `/` → `\` at comptime; Rust can't transform a borrowed + /// `&'static [u8]` in a const fn, so this is a macro that emits a fresh + /// const array with the swap applied. Result is `&'static [u8; N]` + /// (coerces to `&[u8]`). + #[macro_export] + #[doc(hidden)] + macro_rules! __resolver_path_literal { + ($p:expr) => {{ + const __IN: &[u8] = $p; + const __N: usize = __IN.len(); + const fn __swap(input: &[u8]) -> [u8; __N] { + let mut out = [0u8; __N]; + let mut i = 0; + while i < __N { + out[i] = if cfg!(windows) && input[i] == b'/' { + b'\\' + } else { + input[i] + }; + i += 1; + } + out + } + const __OUT: [u8; __N] = __swap(__IN); + &__OUT + }}; + } + pub(super) use __resolver_path_literal as path_literal; + pub(super) fn windows_filesystem_root(p: &[u8]) -> &[u8] { + ::bun_paths::resolve_path::windows_filesystem_root(p) + } +} +// bun_core::strings shim — re-export the canonical `immutable/paths` helpers +// (`without_trailing_slash_windows_path` / `path_contains_node_modules_folder` / +// `without_leading_path_separator` / `char_is_any_slash`) instead of locally +// re-implementing them. The previous local copies diverged from the spec +// (single-strip vs. while-loop, `is_sep_any` vs. platform `SEP`). +mod strings { + pub(super) use bun_paths::strings::paths::{ + char_is_any_slash, path_contains_node_modules_folder, without_leading_path_separator, + without_trailing_slash_windows_path, + }; + pub(super) use bun_paths::strings::*; + #[inline] + pub(super) fn index_of_any(slice: &[u8], chars: &'static [u8]) -> Option { + bun_core::strings::index_of_any(slice, chars).map(|v| v as usize) + } +} +// bun_sys shim — adds the `std.fs`-shaped dir-open surface the resolver names +// (`openDirAbsoluteZ` / `Dir.openDirZ`) on top of the real `::bun_sys` crate. +// `open` / `open_dir_for_iteration` / `get_fd_path` / `OpenDirOptions` / +// `iterate_dir` are now provided by the `pub use ::bun_sys::*` glob. +mod bun_sys { + pub(super) use ::bun_sys::*; + + /// Port of `std.fs.openDirAbsoluteZ` — `open(path, O_DIRECTORY|O_RDONLY|O_CLOEXEC[|O_NOFOLLOW])`. + /// `opts.iterate` is a no-op on POSIX (Zig only used it to pick `iterate=true` + /// on `IterableDir`, which is just an open mode hint). + pub(super) fn open_dir_absolute_z( + path: &::bun_core::ZStr, + opts: OpenDirOptions, + ) -> core::result::Result { + #[cfg(unix)] + let nofollow = if opts.no_follow { libc::O_NOFOLLOW } else { 0 }; + #[cfg(not(unix))] + let nofollow = { + let _ = opts; + 0 + }; + ::bun_sys::open(path, O::DIRECTORY | O::CLOEXEC | O::RDONLY | nofollow, 0) + .map_err(Into::into) + } + /// Port of `std.fs.Dir.openDirZ` — `openat(dir, path, O_DIRECTORY|O_RDONLY|O_CLOEXEC)`. + pub(super) fn open_dir_z( + dir: Fd, + path: &[u8], + _opts: OpenDirOptions, + ) -> core::result::Result { + // PORT NOTE: callers pass either a `&'static [u8]` literal or a NUL-terminated + // slice; `open_dir_at` builds its own ZStr internally so we strip the sentinel. + let path = if path.last() == Some(&0) { + &path[..path.len() - 1] + } else { + path + }; + ::bun_sys::open_dir_at(dir, path).map_err(Into::into) + } + // `iterate_dir` / `dir_iterator::WrappedIterator` are real ports in + // `::bun_sys::dir_iterator` (POSIX getdents / Windows NtQueryDirectoryFile) + // and reach this module via the `pub use ::bun_sys::*` glob above. + pub(super) use ::bun_sys::RawFd; +} + +/// `bun_sys::Fd` extension surface — thin method-syntax wrappers over the +/// free functions `::bun_sys::{close, get_fd_path}` and `Fd::native`, so the +/// resolver body can spell `fd.close()` / `fd.get_fd_path(buf)` per the Zig. +trait FdExt: Sized { + fn close(self); + fn cast(self) -> bun_sys::RawFd; + fn native(self) -> bun_sys::RawFd; + fn get_fd_path<'b>( + self, + buf: &'b mut ::bun_paths::PathBuffer, + ) -> core::result::Result<&'b [u8], ::bun_core::Error>; +} +impl FdExt for ::bun_sys::Fd { + #[inline] + fn close(self) { + let _ = ::bun_sys::close(self); + } + #[inline] + fn cast(self) -> bun_sys::RawFd { + ::bun_sys::Fd::native(self) + } + #[inline] + fn native(self) -> bun_sys::RawFd { + ::bun_sys::Fd::native(self) + } + #[inline] + fn get_fd_path<'b>( + self, + buf: &'b mut ::bun_paths::PathBuffer, + ) -> core::result::Result<&'b [u8], ::bun_core::Error> { + ::bun_sys::get_fd_path(self, buf) + .map(|s| &*s) + .map_err(Into::into) + } +} +trait FdZero { + const ZERO: ::bun_sys::Fd; +} +impl FdZero for ::bun_sys::Fd { + const ZERO: ::bun_sys::Fd = ::bun_sys::Fd::INVALID; +} + +use self::bun_paths as ResolvePath; +use ::bun_ast::import_record as ast; +use ::bun_core::Output; +use ::bun_core::{Environment, FeatureFlags, Generation}; +use bun_ast::Msg; +use bun_collections::{BoundedArray, MultiArrayList}; +use bun_core::{MutableString, PathString}; +use bun_dotenv::env_loader as DotEnv; +use bun_paths::{MAX_PATH_BYTES, PathBuffer, SEP, SEP_STR}; +use bun_perf::system_timer::Timer; +use bun_sys::Fd as FD; +use bun_threading::Mutex; + +use crate::fs as Fs; +use crate::fs::FilenameStoreAppender; +use crate::node_fallbacks as NodeFallbackModules; +use crate::package_json::{BrowserMap, ESModule, PackageJSON}; +use crate::tsconfig_json::TSConfigJSON; + +pub use crate::data_url::DataURL; +pub use crate::dir_info as DirInfo; +pub use crate::dir_info::DirInfoRef; +pub use ::bun_options_types::global_cache::GlobalCache; + +// Sibling resolver modules. They retain the same item names so cross-references +// inside `impl Resolver` resolve unchanged. +use crate::result::{ + DebugLogs, DebugMeta, DirEntryResolveQueueItem, FlushMode, LoadResult, MatchResult, + MatchResultUnion, PathPair, PathPairIter, PendingResolution, PendingResolutionList, + PendingResolutionTag, Result, ResultFlags, ResultUnion, SideEffectsData, SuggestionRange, +}; +use crate::standalone_module_graph::StandaloneModuleGraph; +use crate::{allocators, options}; +// `bun.resolver.SideEffects` — same type as `Result.primary_side_effects_data` +// (re-exported from `bun_ast`; see `result.rs`). +use bun_ast::SideEffects; + +// ── Process-lifetime arenas for DirInfo-cached parses ───────────────────── +// The DirInfo cache (`DirInfo::hash_map_instance()`) is a true process-lifetime +// singleton; entries hold `&'static PackageJSON` / `&'static TSConfigJSON` and +// borrow `&'static [u8]` source bytes. Zig models this with `bun.TrivialNew` +// (heap-allocate, never free). PORTING.md §Forbidden bars `Box::leak`/ +// `mem::forget` for this — process-lifetime storage must go through +// `LazyLock`. These append-only arenas are that storage; the `Box` heap +// address is stable across `Vec` growth, so handing out `&'static T` is sound. + +/// Intern a parsed `PackageJSON` into the process-lifetime DirInfo arena. +/// Returns `NonNull` (not `&'static`) so the mut-provenance survives into +/// `DirInfo::reset()`'s `drop_in_place` -- handing out `&T` here and casting +/// back to `*mut T` at the drop site would be UB under Stacked Borrows. +fn intern_package_json(pkg: PackageJSON) -> core::ptr::NonNull { + static ARENA: std::sync::LazyLock>>> = + std::sync::LazyLock::new(Default::default); + let mut guard = ARENA.lock(); + guard.push(Box::new(pkg)); + // SAFETY: ARENA is `'static` (LazyLock); entries are never removed; the + // `Box` heap address is stable across `Vec` reallocation. + // Derive from `&mut **last` so the returned pointer carries mut-provenance. + core::ptr::NonNull::from(&mut **guard.last_mut().unwrap()) +} + +/// Intern tsconfig.json source bytes into the process-lifetime DirInfo arena. +/// `use_shared_buffer = false` at the read site guarantees `Owned`/`Empty`. +fn intern_tsconfig_contents(contents: crate::cache::Contents) -> &'static [u8] { + use crate::cache::Contents; + let owned: Box<[u8]> = match contents { + Contents::Empty => return b"", + Contents::Owned(v) => v.into_boxed_slice(), + // Unreachable for the `parse_tsconfig` caller (use_shared_buffer=false); + // fall back to a copy so we never hand out a dangling slice. + other => Box::from(other.as_slice()), + }; + // `Interned::leak` is the centralized process-lifetime byte-slice store + // (PORTING.md §Forbidden bars open-coded `Box::leak` + `from_raw_parts`; + // `bun_ptr::Interned` is the sanctioned wrapper that consumes the `Box` + // and hands back a proven `&'static [u8]`). + bun_ptr::Interned::leak(owned).as_bytes() +} + +// Port of `const debuglog = Output.scoped(.Resolver, .hidden)` (resolver.zig:4). +// `bun_core::declare_scope!` emits the per-scope `static ScopedLogger`; the +// `debuglog!` macro forwards to the real `bun_core::scoped_log!` so debug builds +// emit and release builds dead-strip (PORTING.md §Logging). +// +// PORT NOTE: resolver.zig:1692 also binds `const dev = Output.scoped(.Resolver, +// .visible)` for `bustDirCache` — same scope name, different visibility. Rust's +// `declare_scope!` is one static per ident; route both through the `.hidden` +// declaration (matches the file-top binding) and let `BUN_DEBUG_Resolver=1` +// surface the bust log. +bun_core::define_scoped_log!(debuglog, Resolver, hidden); + +// PORT NOTE: `Path` in the body is the `'static`-interned variant (paths borrow +// DirnameStore/FilenameStore). Alias here so the ~80 bare-`Path` use sites +// resolve without a per-site lifetime annotation. +type Path = crate::fs::Path<'static>; +type DifferentCase = crate::fs::DifferentCase<'static>; + +use crate::dir_info::HashMapExt as _; + +/// A temporary threadlocal buffer with a lifetime more than the current +/// function call. +/// +/// These used to be individual `threadlocal var x: bun.PathBuffer = undefined` +/// declarations. On Windows each `PathBuffer` is 96 KB (vs 4 KB on POSIX) and +/// PE/COFF has no TLS-BSS, so 25 of them here cost ~2.5 MB of raw zeros in +/// bun.exe and in every thread's TLS block. Grouping them behind a lazily +/// allocated pointer brings that down to 8 bytes. See `bun.ThreadlocalBuffers`. +/// +/// Experimenting with making this one struct instead of a bunch of different +/// threadlocal vars yielded no performance improvement on macOS when bundling +/// 10 copies of Three.js. Potentially revisit after https://github.com/oven-sh/bun/issues/2716 +pub struct Bufs { + pub extension_path: PathBuffer, + pub tsconfig_match_full_buf: PathBuffer, + pub tsconfig_match_full_buf2: PathBuffer, + pub tsconfig_match_full_buf3: PathBuffer, + + pub esm_subpath: [u8; 512], + pub esm_absolute_package_path: PathBuffer, + pub esm_absolute_package_path_joined: PathBuffer, + + // PORT NOTE: Zig left this `= undefined`; `DirEntryResolveQueueItem` holds + // `&'static [u8]` fields, so a zeroed bit-pattern is UB in Rust. Use + // `MaybeUninit` and `assume_init_{ref,mut}` at the (linear write-then-read) + // use sites in `dir_info_cached_maybe_log`. + pub dir_entry_paths_to_resolve: [core::mem::MaybeUninit; 256], + pub open_dirs: [FD; 256], + pub resolve_without_remapping: PathBuffer, + pub index: PathBuffer, + pub dir_info_uncached_filename: PathBuffer, + pub node_bin_path: PathBuffer, + pub dir_info_uncached_path: PathBuffer, + pub tsconfig_base_url: PathBuffer, + pub relative_abs_path: PathBuffer, + pub load_as_file_or_directory_via_tsconfig_base_path: PathBuffer, + pub node_modules_check: PathBuffer, + pub field_abs_path: PathBuffer, + pub tsconfig_path_abs: PathBuffer, + pub check_browser_map: PathBuffer, + pub remap_path: PathBuffer, + pub load_as_file: PathBuffer, + pub remap_path_trailing_slash: PathBuffer, + pub path_in_global_disk_cache: PathBuffer, + pub abs_to_rel: PathBuffer, + pub node_modules_paths_buf: PathBuffer, + pub import_path_for_standalone_module_graph: PathBuffer, + + #[cfg(windows)] + pub win32_normalized_dir_info_cache: [u8; MAX_PATH_BYTES * 2], + #[cfg(not(windows))] + pub win32_normalized_dir_info_cache: (), +} +// TODO(port): bun.ThreadlocalBuffers(Bufs) — lazily-allocated threadlocal Box. +// In Rust we model it as a `thread_local! { static BUFS_PTR: Cell<*mut Bufs> }` +// caching a leaked `Box` pointer (the Box is never freed in Zig either — +// process-lifetime scratch storage). The `bufs!()` macro hands out `&mut` to a +// single field. This relies on the caller never holding two `bufs!()` borrows +// simultaneously across the same field; the Zig code already obeys that invariant. +thread_local! { + static BUFS_PTR: core::cell::Cell<*mut Bufs> = const { core::cell::Cell::new(core::ptr::null_mut()) }; +} + +#[inline(always)] +fn bufs_storage_get() -> *mut Bufs { + // Fast path: single TLS pointer load + null check. `LocalKey>::get` + // (T: Copy) compiles to a plain `__tls_get_addr` + load with no + // RefCell/Option/closure machinery on the hot path (benches: misc/require-fs). + let p = BUFS_PTR.get(); + if !p.is_null() { + return p; + } + bufs_storage_init() +} + +#[cold] +fn bufs_storage_init() -> *mut Bufs { + // SAFETY: every field of `Bufs` is a byte/integer array + // (`PathBuffer` = `[u8; N]`, `[FD; 256]` where `Fd` is a + // `#[repr(C)]` integer newtype, `[MaybeUninit<_>; 256]` which has + // no validity requirement, `()`), so EVERY bit-pattern — not just + // all-zero — is a valid `Bufs`. Zig left these `= undefined`; each + // field is scratch (write-then-read within a single resolve call, + // including `open_dirs` which is bounded by `open_dir_count`), so + // there is no need to pay for zero-filling ~100 KiB on first use. + let p: *mut Bufs = Box::leak(unsafe { Box::::new_uninit().assume_init() }); + BUFS_PTR.set(p); + p +} + +/// `bufs(.field)` → `bufs!(field)` returns `&mut `. +/// // SAFETY: callers must not alias the same field; threadlocal so no cross-thread races. +macro_rules! bufs { + ($field:ident) => { + // SAFETY: threadlocal storage; callers must not alias the same field within one call frame. + unsafe { &mut (*bufs_storage_get()).$field } + }; +} + +// This is a global so even if multiple resolvers are created, the mutex will still work +// TODO(port): `bun_threading::Mutex` has no `const fn new()`; use LazyLock until it does. +// `pub(crate)` so the `fs::EntriesMap::inner` debug-assert can verify it is held +// (the resolver mutex is one of the two documented guards for the entries singleton). +pub(crate) static RESOLVER_MUTEX: std::sync::LazyLock = + std::sync::LazyLock::new(Mutex::default); +// Zig had `resolver_Mutex_loaded` to lazily zero-init; Rust const init handles that. + +type BinFolderArray = BoundedArray<&'static [u8], 128>; +// TODO(port): `BoundedArray` has no const constructor; init lazily under +// `BIN_FOLDERS_LOADED` (matches Zig's `bin_folders_loaded` lazy zero-init). +static BIN_FOLDERS: bun_core::RacyCell> = + bun_core::RacyCell::new(core::mem::MaybeUninit::uninit()); +static BIN_FOLDERS_LOCK: std::sync::LazyLock = std::sync::LazyLock::new(Mutex::default); +static BIN_FOLDERS_LOADED: core::sync::atomic::AtomicBool = + core::sync::atomic::AtomicBool::new(false); + +// LAYERING: `AnyResolveWatcher` is the erased vtable the resolver calls to +// register directory watches. The concrete callback lives in `bun_watcher` +// (lower tier); defining the vtable shape there and re-exporting here keeps a +// single type so `Watcher::get_resolve_watcher()` flows directly into +// `Resolver.watcher` without a seam converter. +pub use bun_watcher::AnyResolveWatcher; + +// Zig: `pub fn ResolveWatcher(comptime Context: type, comptime onWatch: anytype) type` — +// type-generator returning a struct with `.init(ctx) -> AnyResolveWatcher` and a +// monomorphized `watch` shim. Per PORTING.md (`fn Foo(comptime T) type` → `struct Foo`). +// +// PORT NOTE: const fn-pointer generics (`adt_const_params` for fn ptrs) and +// const params depending on type params are both forbidden. Reshape to a +// runtime fn-pointer carried alongside the context — `init` produces the same +// `AnyResolveWatcher` erased shim as Zig's monomorphized `wrap`. + +pub struct ResolveWatcher { + on_watch: fn(*mut C, &[u8], FD), + _marker: core::marker::PhantomData<*mut C>, +} +impl ResolveWatcher { + pub const fn new(on_watch: fn(*mut C, &[u8], FD)) -> Self { + Self { + on_watch, + _marker: core::marker::PhantomData, + } + } + pub fn init(self, ctx: *mut C) -> AnyResolveWatcher { + AnyResolveWatcher { + context: ctx.cast(), + // SAFETY: `fn(*mut C, ..)` and `fn(*mut (), ..)` are ABI-identical + // (Rust-ABI, thin-ptr first arg); the `wrap` shim in Zig did the + // same erase. The callback body discharges its own type-recovery. + callback: unsafe { + bun_ptr::cast_fn_ptr::(self.on_watch) + }, + } + } +} + +pub struct Resolver<'a> { + pub opts: options::BundleOptions, + // PORT NOTE: Zig `fs: *Fs.FileSystem` / `log: *logger.Log` are raw aliasing + // pointers — the bundler builds a `Resolver` per worker thread sharing the + // process-wide `FileSystem` singleton, so `&'a mut` here would manufacture + // aliased unique refs across threads (instant UB). Model as `*mut` / + // `NonNull` (never-null but raw-aliasing) and deref through the `fs()` / + // `log()` accessors below. + pub fs: *mut Fs::FileSystem, + pub log: NonNull, + // allocator dropped — global mimalloc + /// PORT NOTE: Zig stores `[]const []const u8` aliasing into + /// `r.opts.extension_order` and saves/restores it across nested resolves. + /// Stored as a `Copy` enum tag (no self-reference) and resolved on demand + /// via [`Self::extension_order`] / [`options::BundleOptions::ext_order_slice`]. + pub extension_order: options::ExtOrder, + pub timer: Timer, + + pub care_about_bin_folder: bool, + pub care_about_scripts: bool, + + /// Read the "browser" field in package.json files? + /// For Bun's runtime, we don't. + pub care_about_browser_field: bool, + + pub debug_logs: Option, + pub elapsed: u64, // tracing + + pub watcher: Option, + + pub caches: CacheSet, + pub generation: Generation, + + /// Auto-install backend. `bun_install::PackageManager` implements + /// [`AutoInstaller`]; the resolver only sees the trait object so it stays + /// below `bun_install` in the dep graph. The runtime/bundler that enables + /// auto-install (`opts.global_cache != .disable`) is responsible for + /// constructing the `PackageManager` (Zig: `PackageManager.initWithRuntime`) + /// and assigning it here BEFORE resolution; the resolver no longer + /// constructs it lazily — that would require depending on `bun_install`, + /// which depends on us. When `None`, [`get_package_manager`] panics if the + /// auto-install path is reached. + pub package_manager: Option>, + pub on_wake_package_manager: Install::WakeHandler, + // Spec resolver.zig:477 `env_loader: ?*DotEnv.Loader` — raw nullable pointer. + // Stored as `NonNull` (not `&'a Loader`) because the same allocation is + // mutably reborrowed via `Transpiler.env: *mut Loader` after this field is + // set (e.g. bake/production.rs assigns this then calls `configure_defines()` + // → `run_env_loader()` which takes `&mut *self.env`). Holding a live + // `&Loader` across that `&mut Loader` would be aliased-&mut UB; a raw + // pointer carries no aliasing guarantee and matches the Zig shape. + pub env_loader: Option>>, + pub store_fd: bool, + + pub standalone_module_graph: Option<&'a dyn StandaloneModuleGraph>, + + // These are sets that represent various conditions for the "exports" field + // in package.json. + // esm_conditions_default: bun.StringHashMap(bool), + // esm_conditions_import: bun.StringHashMap(bool), + // esm_conditions_require: bun.StringHashMap(bool), + + // A special filtered import order for CSS "@import" imports. + // + // The "resolve extensions" setting determines the order of implicit + // extensions to try when resolving imports with the extension omitted. + // Sometimes people create a JavaScript/TypeScript file and a CSS file with + // the same name when they create a component. At a high level, users expect + // implicit extensions to resolve to the JS file when being imported from JS + // and to resolve to the CSS file when being imported from CSS. + // + // Different bundlers handle this in different ways. Parcel handles this by + // having the resolver prefer the same extension as the importing file in + // front of the configured "resolve extensions" order. Webpack's "css-loader" + // plugin just explicitly configures a special "resolve extensions" order + // consisting of only ".css" for CSS files. + // + // It's unclear what behavior is best here. What we currently do is to create + // a special filtered version of the configured "resolve extensions" order + // for CSS files that filters out any extension that has been explicitly + // configured with a non-CSS loader. This still gives users control over the + // order but avoids the scenario where we match an import in a CSS file to a + // JavaScript-related file. It's probably not perfect with plugins in the + // picture but it's better than some alternatives and probably pretty good. + // atImportExtensionOrder []string + + // This mutex serves two purposes. First of all, it guards access to "dirCache" + // which is potentially mutated during path resolution. But this mutex is also + // necessary for performance. The "React admin" benchmark mysteriously runs + // twice as fast when this mutex is locked around the whole resolve operation + // instead of around individual accesses to "dirCache". For some reason, + // reducing parallelism in the resolver helps the rest of the bundler go + // faster. I'm not sure why this is but please don't change this unless you + // do a lot of testing with various benchmarks and there aren't any regressions. + pub mutex: &'static Mutex, + + /// This cache maps a directory path to information about that directory and + /// all parent directories. When interacting with this structure, make sure + /// to validate your keys with `Resolver.assertValidCacheKey` + // PORT NOTE: Zig `dir_cache: *DirInfo.HashMap` is a raw aliasing pointer to the + // `DirInfo::hash_map_instance()` singleton. Modeled as `*mut` (not `&'static mut`) + // for the same reason as `fs`/`log` above — every per-worker `Resolver` shares the + // singleton, so a `&'static mut` here would manufacture aliased unique refs (UB). + // Deref through the `dir_cache()` accessor below. + pub dir_cache: *mut DirInfo::HashMap, + + /// This is set to false for the runtime. The runtime should choose "main" + /// over "module" in package.json + pub prefer_module_field: bool, + + /// This is an array of paths to resolve against. Used for passing an + /// object '{ paths: string[] }' to `require` and `resolve`; This field + /// is overwritten while the resolution happens. + /// + /// When this is null, it is as if it is set to `&.{ path.dirname(referrer) }`. + pub custom_dir_paths: Option<&'a [bun_core::String]>, +} + +/// RAII guard returned by [`Resolver::scoped_log`]. Restores the previous +/// `Resolver::log` pointer on drop — port of the Zig +/// `defer resolver.log = orig_log` save/restore pattern. +pub struct ResolverLogScope { + slot: *mut NonNull, + prev: NonNull, +} + +impl Drop for ResolverLogScope { + #[inline] + fn drop(&mut self) { + // SAFETY: `slot` was derived via `addr_of_mut!` from the raw resolver + // pointer in `scoped_log` (SharedReadWrite provenance); caller contract + // guarantees the resolver outlives this guard. + unsafe { *self.slot = self.prev }; + } +} + +impl<'a> Resolver<'a> { + /// Per-worker constructor — replaces the bundler's prior bitwise + /// `transpiler.* = from.*` (Zig ThreadPool.zig:308) for the resolver + /// portion. Every `Copy` / raw-pointer field is copied from `from`; the + /// per-worker `caches` (the only `Drop`-carrying field, via the + /// `Json` cache's `MimallocArena`) and `debug_logs`/`timer` are freshly + /// constructed so nothing the parent owns is aliased into the worker. + /// + /// `opts` and `log` are supplied by the caller (the worker projects a + /// fresh `BundleOptions` subset and arena-allocates its own `Log`). + /// + /// # Safety + /// `from`'s `standalone_module_graph` / `env_loader` borrow data that + /// outlives the returned resolver (process-lifetime singletons in every + /// caller). The lifetime is widened from `'from` to `'a` here; callers + /// must uphold that the borrowed data outlives `'a`. + pub unsafe fn for_worker( + from: &Resolver<'_>, + log: NonNull, + opts: options::BundleOptions, + ) -> Resolver<'a> { + Resolver { + opts, + fs: from.fs, + log, + extension_order: from.extension_order, + timer: Timer::start().unwrap_or_else(|_| panic!("Timer fail")), + care_about_bin_folder: from.care_about_bin_folder, + care_about_scripts: from.care_about_scripts, + care_about_browser_field: from.care_about_browser_field, + // `DebugLogs` owns Vecs — per-worker fresh. + debug_logs: None, + elapsed: 0, + watcher: from.watcher, + // Spec ThreadPool.zig:313 `transpiler.resolver.caches = CacheSet.Set.init(allocator)`. + caches: CacheSet::init(), + generation: from.generation, + package_manager: from.package_manager, + on_wake_package_manager: from.on_wake_package_manager.clone(), + // SAFETY: see fn doc — pointee outlives `'a`. + env_loader: from.env_loader.map(|p| p.cast::>()), + store_fd: from.store_fd, + // SAFETY: see fn doc — lifetime-widen the trait-object borrow. The + // vtable layout is identical (only the borrow-checker tag differs); + // a raw-pointer `as`-cast cannot change the `+ 'b` bound, so widen + // via a layout-preserving transmute on the `Option<&dyn>`. + standalone_module_graph: unsafe { + core::mem::transmute::< + Option<&'_ dyn StandaloneModuleGraph>, + Option<&'a dyn StandaloneModuleGraph>, + >(from.standalone_module_graph) + }, + mutex: from.mutex, + dir_cache: from.dir_cache, + prefer_module_field: from.prefer_module_field, + // Transient per-resolve scratch (only set for `require(..., {paths})`); + // never carried across worker init. + custom_dir_paths: None, + } + } + + /// Port of Zig `r.fs` deref. + /// + /// PORT NOTE (Stacked Borrows): returns the RAW `*mut` (NOT `&'a mut`). A + /// `&'a mut` accessor would let two `fs()` calls manufacture coexisting + /// aliased unique refs to the same singleton (PORTING.md §Forbidden: + /// aliased-&mut), and any later `&mut *self.fs` retag would pop a previously + /// returned `&'a mut`'s SB tag while it's still nominally live for `'a`. + /// Callers must `unsafe { &mut *r.fs() }` at the narrowest use site and let + /// the projection die at end-of-expression. Spec resolver.zig:455 stores raw + /// `*Fs.FileSystem` and dereferences per-use. + #[inline(always)] + pub fn fs(&self) -> *mut Fs::FileSystem { + self.fs + } + + /// Shared-borrow of the FileSystem singleton for read-only methods + /// (`abs_buf*`, `normalize_buf`, `dirname_store`, `filename_store`, + /// `top_level_dir`). Preferred over `unsafe { &mut *self.fs() }` whenever + /// the callee takes `&self` — avoids materializing a `&mut FileSystem` + /// that could (under Stacked Borrows) pop a coexisting `rfs_ptr()` / + /// `&mut *query.entry` tag derived from the same allocation. + #[inline(always)] + pub fn fs_ref(&self) -> &Fs::FileSystem { + // SAFETY: BACKREF — `self.fs` is the process-global FileSystem singleton + // (LIFETIMES.tsv: STATIC); resolver mutex serializes all mutation. A + // shared `&` cannot alias-UB with the raw `*mut RealFS` projections + // used elsewhere because no Unique tag is pushed. + unsafe { &*self.fs } + } + + /// Unique-borrow of the `FileSystem` singleton. Centralizes the + /// `unsafe { &mut *self.fs() }` retag for call sites that hold no other + /// borrow of `self` across the call. Sites that need `&mut FileSystem` + /// while also borrowing a disjoint `self.` (e.g. + /// `self.caches.fs.read_file_with_allocator`) cannot route through + /// `&mut self` and continue to narrow-retag via the raw [`fs()`](Self::fs) + /// accessor — same caveat as [`log_mut`](Self::log_mut). + #[inline(always)] + pub fn fs_mut(&mut self) -> &mut Fs::FileSystem { + // SAFETY: BACKREF — `self.fs` is the never-null process-global + // `FileSystem` singleton (set in `init1`); resolver mutex serializes + // all mutation across worker clones; `&mut self` rules out + // intra-instance aliasing. + unsafe { &mut *self.fs } + } + + /// Resolve the current [`options::ExtOrder`] tag to the slice it names + /// inside `self.opts`. Port of Zig `r.extension_order` field read. + #[inline(always)] + pub fn extension_order(&self) -> &[Box<[u8]>] { + self.opts.ext_order_slice(self.extension_order) + } + + /// Raw-pointer projection to the inner `RealFS` (`self.fs.fs`). + /// + /// PORT NOTE (Stacked Borrows): derived directly from the raw `*mut + /// FileSystem` field via `addr_of_mut!` so the resulting `*mut RealFS` + /// carries SharedReadWrite provenance — later `fs_ref()` (Shared) or + /// short-lived `&mut *self.fs()` retags do NOT invalidate it. Callers + /// re-borrow `&mut *self.rfs_ptr()` per use; do not bind a `&mut RealFS` + /// across another `fs()` deref. + #[inline(always)] + pub fn rfs_ptr(&self) -> *mut Fs::file_system::RealFS { + // SAFETY: `self.fs` is the process-global FileSystem singleton; valid + // for the resolver's lifetime. `addr_of_mut!` creates a raw place + // projection without an intermediate `&mut FileSystem`. + unsafe { core::ptr::addr_of_mut!((*self.fs).fs) } + } + + /// Port of Zig `r.log` deref. + /// + /// PORT NOTE (Stacked Borrows): returns RAW `*mut` (see `fs()` note). BACKREF + /// — owner (Transpiler/BundleV2) outlives the Resolver; worker clones share + /// the same Log under the resolver mutex. Caller `unsafe { &mut *r.log() }` + /// at each use site; do not bind the projected `&mut Log` across another + /// `log()` deref. + #[inline(always)] + pub fn log(&self) -> *mut bun_ast::Log { + self.log.as_ptr() + } + + /// Temporarily redirect `self.log` to `log`, returning a guard that + /// restores the previous pointer on drop. Port of the Zig + /// `const orig = r.log; r.log = &tmp; defer r.log = orig;` pattern. + /// + /// Takes a raw `*mut Self` (not `&mut self`) so the stored slot pointer + /// carries SharedReadWrite provenance and stays valid under Stacked + /// Borrows when the caller subsequently reborrows the resolver + /// (`read_dir_info` etc.) before the guard drops. + /// + /// # Safety + /// `self_` must point at a `Resolver` that outlives the returned guard, + /// and `log` must remain valid for that same duration (declare the guard + /// *after* the temporary `Log` so it drops first). + #[inline] + pub unsafe fn scoped_log(self_: *mut Self, log: NonNull) -> ResolverLogScope { + // SAFETY: caller contract — `self_` is live; `addr_of_mut!` projects + // the field place without an intermediate `&mut Resolver`. + let slot = unsafe { core::ptr::addr_of_mut!((*self_).log) }; + // SAFETY: `slot` just derived from a live resolver. + let prev = unsafe { *slot }; + unsafe { *slot = log }; + ResolverLogScope { slot, prev } + } + + /// Shared-borrow of the resolver's `Log` for read-only inspection + /// (e.g. `log.level`). Preferred over `unsafe { &*self.log() }`. + #[inline(always)] + pub fn log_ref(&self) -> &bun_ast::Log { + // SAFETY: BACKREF — `self.log` is set in `init1` / `scoped_log`, + // owner-allocated, outlives the Resolver. Resolver mutex serializes + // mutation; a Shared `&` here pushes no Unique tag and so cannot + // alias-UB with the narrow `log_mut()` retags elsewhere (none are live + // across a `log_ref()` call). + unsafe { self.log.as_ref() } + } + + /// Unique-borrow of the resolver's `Log` for `add_*_fmt` / `add_msg`. + /// + /// Centralizes the per-site `unsafe { &mut *self.log() }` retag. `&mut + /// self` rules out two coexisting `&mut Log` from the SAME `Resolver`; + /// cross-clone aliasing (worker copies share the owner's `Log`) is + /// guarded by the resolver `mutex` — same invariant the open-coded sites + /// already relied on. + /// + /// Sites that need `&mut Log` while holding a disjoint `&mut self.` + /// (`flush_debug_logs` ↔ `self.debug_logs`, `parse_tsconfig` ↔ + /// `self.caches.json`) cannot route through `&mut self` and continue to + /// narrow-retag via the raw [`log()`](Self::log) accessor. + #[inline(always)] + pub fn log_mut(&mut self) -> &mut bun_ast::Log { + // SAFETY: BACKREF — `self.log` is set in `init1` / `scoped_log`; the + // pointee (owner-allocated `Log`, or a stack `Log` pinned by a live + // `ResolverLogScope`) outlives every borrow returned here. Resolver + // mutex serializes mutation across worker clones. + unsafe { self.log.as_mut() } + } + + /// Port of Zig `r.dir_cache` deref. + /// + /// PORT NOTE (Stacked Borrows): returns RAW `*mut` (see `fs()` note). ARENA — + /// `DirInfo::hash_map_instance()` singleton; never freed. Caller + /// `unsafe { &mut *r.dir_cache() }` at each use site. + #[inline(always)] + pub fn dir_cache(&self) -> *mut DirInfo::HashMap { + self.dir_cache + } + + /// Unique-borrow of the `DirInfo` BSSMap singleton. + /// + /// Centralizes the `unsafe { &mut *self.dir_cache() }` retag that every + /// call site previously open-coded. `&mut self` ensures no two coexisting + /// `&mut HashMap` are produced from the SAME `Resolver`; cross-clone + /// aliasing (per-worker `Resolver`s share the singleton) is + /// guarded by the resolver `mutex` — identical invariant to the prior + /// per-site `unsafe`. + /// + /// Stacked Borrows: each call pushes a fresh Unique tag on the BSSMap + /// allocation, so any `*mut DirInfo` previously projected from an earlier + /// `dir_cache_mut()` borrow is popped. Callers that need a slot pointer to + /// survive a subsequent map access must route both through ONE bound + /// `&mut HashMap` (see `dir_info_for_resolution` / `dir_info_cached_maybe_log`). + #[inline(always)] + pub fn dir_cache_mut(&mut self) -> &mut DirInfo::HashMap { + // SAFETY: ARENA — `self.dir_cache` is the never-null + // `DirInfo::hash_map_instance()` static (set in `init1`, never + // reassigned, never freed). Resolver mutex serializes all mutation + // across worker clones; `&mut self` rules out intra-instance aliasing. + unsafe { &mut *self.dir_cache } + } + + /// Port of resolver.zig `getPackageManager`. The Zig spec lazily calls + /// `PackageManager.initWithRuntime` here; in the Rust crate graph that + /// would be a `bun_resolver → bun_install` cycle, so the lazy init is + /// dispatched through the link-time `extern "Rust"` factory + /// [`__bun_resolver_init_package_manager`] (defined `#[no_mangle]` in + /// `bun_install::auto_installer`). The factory performs + /// `HTTPThread.init` + `PackageManager.initWithRuntime` and returns the + /// process-static singleton as a `dyn AutoInstaller`. We then wire + /// `on_wake` and cache the pointer — exactly the Zig body. Reached from + /// the auto-install path (`load_node_modules` global-cache block) when + /// [`use_package_manager`] is `true`. + pub fn get_package_manager(&mut self) -> *mut dyn AutoInstaller { + if let Some(pm) = self.package_manager { + return pm.as_ptr(); + } + // Zig: `bun.HTTPThread.init(&.{}); const pm = PackageManager.initWithRuntime( + // this.log, this.opts.install, bun.default_allocator, .{}, this.env_loader.?);` + // SAFETY: `DotEnv::Loader<'a>` is layout-identical across `'a`; + // `init_with_runtime` only borrows it for the synchronous init (the + // static `PackageManager` retains a raw `NonNull>`, + // matching Zig's untracked `*DotEnv.Loader` aliasing). + let env: NonNull> = self + .env_loader + .expect("Resolver.env_loader must be set before auto-install") + .cast::>(); + // SAFETY: `__bun_resolver_init_package_manager` is defined + // `#[no_mangle]` in `bun_install::auto_installer` and linked into the + // final binary; `self.log` / `self.opts.install` / `env` point at + // process-lifetime storage (Transpiler-owned). The returned pointer + // names the `PackageManager` singleton (`'static`). + let pm: NonNull = + unsafe { __bun_resolver_init_package_manager(self.log, self.opts.install, env) }; + // Zig: `pm.onWake = this.onWakePackageManager;` + // SAFETY: `pm` is the just-initialized singleton; sole `&mut` here. + unsafe { (*pm.as_ptr()).set_on_wake(self.on_wake_package_manager.clone()) }; + self.package_manager = Some(pm); + pm.as_ptr() + } + + /// Safe accessor for the optional [`AutoInstaller`] back-reference. + /// + /// Single `unsafe` deref site for the `package_manager: + /// Option>` field. The pointee is the + /// process-static `PackageManager` singleton (set via + /// [`get_package_manager`](Self::get_package_manager) / + /// `__bun_resolver_init_package_manager`), so it strictly outlives the + /// resolver. `&mut self` ensures the returned `&mut dyn AutoInstaller` is + /// the only live reference for its lifetime. + #[inline] + pub fn auto_installer(&mut self) -> Option<&mut dyn AutoInstaller> { + // SAFETY: BACKREF — `package_manager` names the bun_install-owned + // singleton, live for the resolver's lifetime once installed; `&mut + // self` ⇒ exclusive access to the only Rust handle. + self.package_manager.map(|mut pm| unsafe { pm.as_mut() }) + } + + /// Safe read-only accessor for the optional `DotEnv::Loader` back-reference. + /// + /// Single `unsafe` deref site for the `env_loader: Option>` + /// field. The pointee is the Transpiler-owned loader (set from + /// `transpiler.env`) and strictly outlives the resolver. Only called once + /// resolution has begun (after `run_env_loader()`), so no `&mut Loader` is + /// live concurrently — see the field comment for why this is *not* stored + /// as `Option<&'a Loader>`. + #[inline] + pub fn env_loader(&self) -> Option<&'a DotEnv::Loader<'a>> { + // SAFETY: BACKREF — `env_loader` names the Transpiler-owned + // `DotEnv::Loader`, live for the resolver's lifetime `'a`; resolution + // never mutates the env, so no `&mut Loader` overlaps this shared + // borrow. Returned as `&'a` (not tied to `&self`) so callers may keep + // the env borrow across `&mut self` resolver calls. + self.env_loader.map(|p| unsafe { p.as_ref() }) + } + + #[inline] + pub fn use_package_manager(&self) -> bool { + // TODO(@paperclover): make this configurable. the rationale for disabling + // auto-install in standalone mode is that such executable must either: + // + // - bundle the dependency itself. dynamic `require`/`import` could be + // changed to bundle potential dependencies specified in package.json + // + // - want to load the user's node_modules, which is what currently happens. + // + // auto install, as of writing, is also quite buggy and untested, it always + // installs the latest version regardless of a user's package.json or specifier. + // in addition to being not fully stable, it is completely unexpected to invoke + // a package manager after bundling an executable. if enough people run into + // this, we could implement point 1 + if self.standalone_module_graph.is_some() { + return false; + } + + self.opts.global_cache.is_enabled() + } + + pub fn init1( + log: NonNull, + _fs: *mut Fs::FileSystem, + opts: options::BundleOptions, + ) -> Self { + // resolver_Mutex_loaded check elided; static is const-inited in Rust. + + let care_about_browser_field = opts.target == options::Target::Browser; + Resolver { + // allocator dropped + // Route through the per-monomorphization singleton so this field and + // `DirInfo::get_parent()` / `get_enclosing_browser_scope()` share storage + // (Zig `BSSMap.init()` is a per-type singleton, not a fresh alloc). + dir_cache: DirInfo::hash_map_instance(), + mutex: &*RESOLVER_MUTEX, + caches: CacheSet::init(), + opts, + timer: Timer::start().unwrap_or_else(|_| panic!("Timer fail")), + fs: _fs, + log, + extension_order: options::ExtOrder::DefaultDefault, + care_about_browser_field, + care_about_bin_folder: false, + care_about_scripts: false, + debug_logs: None, + elapsed: 0, + watcher: None, + generation: 0, + package_manager: None, + on_wake_package_manager: Default::default(), + env_loader: None, + store_fd: false, + standalone_module_graph: None, + prefer_module_field: true, + custom_dir_paths: None, + } + } + + pub fn is_external_pattern(&self, import_path: &[u8]) -> bool { + if self.opts.packages == options::Packages::External && is_package_path(import_path) { + return true; + } + self.matches_user_external_pattern(import_path) + } + + /// True iff `import_path` matches a user-supplied `--external` wildcard + /// pattern. Does NOT consider `packages = external`; use + /// `isExternalPattern` for the combined check. + pub fn matches_user_external_pattern(&self, import_path: &[u8]) -> bool { + for pattern in self.opts.external.patterns.iter() { + if import_path.len() >= pattern.prefix.len() + pattern.suffix.len() + && (import_path.starts_with(pattern.prefix.as_ref()) + && import_path.ends_with(pattern.suffix.as_ref())) + { + return true; + } + } + false + } + + /// Resolves `import_path` via the enclosing tsconfig's `paths`. Returns + /// the `MatchResult` iff a key matches AND the mapped target exists on + /// disk. Used to let path-aliased local files win over `packages=external` + /// without breaking catch-all `"*"` paths entries that only cover ambient + /// type stubs. + pub fn resolve_via_tsconfig_paths( + &mut self, + source_dir: &[u8], + import_path: &[u8], + kind: ast::ImportKind, + ) -> Option { + // SAFETY: PORT — `import_path` is caller-interned (DirnameStore/source text) + // and outlives the returned MatchResult. Zig used raw `[]const u8` here. + // TODO(port): thread an explicit `'a` through MatchResult instead. + let import_path: &'static [u8] = unsafe { &*std::ptr::from_ref::<[u8]>(import_path) }; + if source_dir.is_empty() { + return None; + } + if !bun_paths::is_absolute(source_dir) { + return None; + } + let dir_info = self.dir_info_cached(source_dir).ok().flatten()?; + let tsconfig = dir_info.enclosing_tsconfig_json?; + if tsconfig.paths.count() == 0 { + return None; + } + self.match_tsconfig_paths(tsconfig, import_path, kind) + } + + pub fn flush_debug_logs( + &mut self, + flush_mode: FlushMode, + ) -> core::result::Result<(), bun_core::Error> { + // TODO(port): narrow error set + // PORT NOTE: capture `log` before partially borrowing `self.debug_logs` + // so the method call doesn't conflict with the field borrow (`log()` + // derefs the raw `*mut Log` and is lifetime-decoupled from `&self`). + // SAFETY: BACKREF — `self.log` points at owner-allocated `Log`; disjoint from + // `self.debug_logs` (separate allocation), so the `&mut Log` does not alias the + // `self.debug_logs.as_mut()` borrow below. + let log = unsafe { &mut *self.log() }; + if let Some(debug) = self.debug_logs.as_mut() { + // PORT NOTE: spec resolver.zig:650-658 — only consume `what`/`notes` inside + // the arm that actually emits, so the success-at-non-verbose path touches + // nothing. `add_range_debug_with_notes`/`add_verbose_with_notes` take + // `&'static [u8]`; bypass them and build the `Msg` directly so the Log owns + // the `what` buffer via `Data.text: Cow::Owned` (no `Box::leak`, PORTING.md + // §Forbidden). The `should_print` gate mirrors the bypassed wrappers. + if flush_mode == FlushMode::Fail { + if bun_ast::Kind::Debug.should_print(log.level) { + let what = core::mem::take(&mut debug.what); + let notes = core::mem::take(&mut debug.notes).into_boxed_slice(); + log.add_msg(Msg { + kind: bun_ast::Kind::Debug, + data: bun_ast::range_data( + None, + bun_ast::Range { + loc: bun_ast::Loc::default(), + ..Default::default() + }, + what, + ), + notes, + ..Default::default() + }); + } + } else if (log.level as u32) <= (bun_ast::Level::Verbose as u32) { + if bun_ast::Kind::Verbose.should_print(log.level) { + let what = core::mem::take(&mut debug.what); + let notes = core::mem::take(&mut debug.notes).into_boxed_slice(); + log.add_msg(Msg { + kind: bun_ast::Kind::Verbose, + data: bun_ast::range_data( + None, + bun_ast::Range { + loc: bun_ast::Loc::EMPTY, + ..Default::default() + }, + what, + ), + notes, + ..Default::default() + }); + } + } + } + Ok(()) + } + + // var tracing_start: i128 — unused; dropped. + + pub fn resolve_and_auto_install( + &mut self, + source_dir: &[u8], + import_path: &[u8], + kind: ast::ImportKind, + global_cache: GlobalCache, + ) -> ResultUnion { + // SAFETY: PORT — `import_path` is caller-interned (source text / DirnameStore) + // and outlives the returned Result. Zig used raw `[]const u8` here. + // TODO(port): thread an explicit lifetime through Result instead. + let import_path: &'static [u8] = unsafe { &*std::ptr::from_ref::<[u8]>(import_path) }; + let _tracer = ::bun_perf::trace(::bun_perf::PerfEvent::ModuleResolverResolve); + + // Only setting 'current_action' in debug mode because module resolution + // is done very often, and has a very low crash rate. + // TODO(port): bun.crash_handler.current_action save/restore (Environment.show_crash_trace gated) + #[cfg(debug_assertions)] + let _crash_guard = + ::bun_crash_handler::set_current_action_resolver(source_dir, import_path, kind); + + #[cfg(debug_assertions)] + if bun_core::debug_flags::has_resolve_breakpoint(import_path) { + bun_core::Output::debug(&format_args!( + "Resolving {} from {}", + bstr::BStr::new(import_path), + bstr::BStr::new(source_dir), + )); + // @breakpoint() — no Rust equiv; left as TODO(port) + } + + let original_order = self.extension_order; + // PORT NOTE: Zig `defer r.extension_order = original_order` — reshaped for + // borrowck so the restore happens explicitly at every return point below. + self.extension_order = match kind { + ast::ImportKind::Url | ast::ImportKind::AtConditional | ast::ImportKind::At => { + options::ExtOrder::Css + } + ast::ImportKind::EntryPointBuild + | ast::ImportKind::EntryPointRun + | ast::ImportKind::Stmt + | ast::ImportKind::Dynamic => options::ExtOrder::DefaultEsm, + _ => options::ExtOrder::DefaultDefault, + }; + + if FeatureFlags::TRACING { + self.timer.reset(); + } + + // Spec resolver.zig:703-707: `defer { if (tracing) r.elapsed += r.timer.read() }` + // — fires on EVERY return path. Capture raw field ptrs (Copy) so the closure + // does not hold a `&mut self` borrow across the function body. + let elapsed_ptr: *mut u64 = core::ptr::addr_of_mut!(self.elapsed); + let timer_ptr: *const Timer = core::ptr::addr_of!(self.timer); + scopeguard::defer! { + if FeatureFlags::TRACING { + // SAFETY: `self` outlives this guard (drops at end of fn body); + // `elapsed`/`timer` are not borrowed when the guard fires. + unsafe { *elapsed_ptr += (*timer_ptr).read(); } + } + } + + if self.log_ref().level == bun_ast::Level::Verbose { + if self.debug_logs.is_some() { + // deinit → drop + self.debug_logs = None; + } + self.debug_logs = Some(DebugLogs::init().expect("unreachable")); + } + + if import_path.is_empty() { + self.extension_order = original_order; + return ResultUnion::NotFound; + } + + if self.opts.mark_builtins_as_external { + if import_path.starts_with(b"node:") + || import_path.starts_with(b"bun:") + || HardcodedAlias::has( + import_path, + self.opts.target, + HardcodedAliasCfg { + rewrite_jest_for_tests: self.opts.rewrite_jest_for_tests, + }, + ) + { + self.extension_order = original_order; + return ResultUnion::Success(Result { + import_kind: kind, + path_pair: PathPair { + primary: Path::init(import_path), + secondary: None, + }, + module_type: options::ModuleType::Cjs, + primary_side_effects_data: SideEffects::NoSideEffectsPureData, + flags: ResultFlags::IS_EXTERNAL, + ..Default::default() + }); + } + } + + // #29590: a tsconfig `paths` key can look bare (e.g. "@/*") and + // otherwise collide with `packages=external + isPackagePath`. Try + // the alias first, but only follow it when it actually resolves to + // a file on disk — a catch-all `"*": ["./types/*"]` for ambient + // .d.ts stubs must still let real bare imports stay external. + if kind != ast::ImportKind::EntryPointBuild + && kind != ast::ImportKind::EntryPointRun + && self.opts.packages == options::Packages::External + && is_package_path(import_path) + && !self.matches_user_external_pattern(import_path) + { + if let Some(res) = self.resolve_via_tsconfig_paths(source_dir, import_path, kind) { + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note( + b"Resolved via tsconfig.json \"paths\" before applying packages=external" + .to_vec(), + ); + } + let _ = self.flush_debug_logs(FlushMode::Success); + self.extension_order = original_order; + return ResultUnion::Success(Result { + import_kind: kind, + path_pair: res.path_pair, + diff_case: res.diff_case, + package_json: res.package_json, + dirname_fd: res.dirname_fd, + file_fd: res.file_fd, + jsx: self.opts.jsx.clone(), + ..Default::default() + }); + } + } + + // Certain types of URLs default to being external for convenience, + // while these rules should not be applied to the entrypoint as it is never external (#12734) + if kind != ast::ImportKind::EntryPointBuild + && kind != ast::ImportKind::EntryPointRun + && (self.is_external_pattern(import_path) + // "fill: url(#filter);" + || (kind.is_from_css() && import_path.starts_with(b"#")) + // "background: url(http://example.com/images/image.png);" + || import_path.starts_with(b"http://") + // "background: url(https://example.com/images/image.png);" + || import_path.starts_with(b"https://") + // "background: url(//example.com/images/image.png);" + || import_path.starts_with(b"//")) + { + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note(b"Marking this path as implicitly external".to_vec()); + } + let _ = self.flush_debug_logs(FlushMode::Success); + + self.extension_order = original_order; + return ResultUnion::Success(Result { + import_kind: kind, + path_pair: PathPair { + primary: Path::init(import_path), + secondary: None, + }, + module_type: if !kind.is_from_css() { + options::ModuleType::Esm + } else { + options::ModuleType::Unknown + }, + flags: ResultFlags::IS_EXTERNAL, + ..Default::default() + }); + } + + match DataURL::parse(import_path) { + Err(_) => { + self.extension_order = original_order; + return ResultUnion::Failure(bun_core::err!("InvalidDataURL")); + } + Ok(Some(data_url)) => { + // "import 'data:text/javascript,console.log(123)';" + // "@import 'data:text/css,body{background:white}';" + let mime = data_url.decode_mime_type(); + use ::bun_http_types::MimeType::Category; + if matches!( + mime.category, + Category::Javascript | Category::Css | Category::Json | Category::Text + ) { + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note(b"Putting this path in the \"dataurl\" namespace".to_vec()); + } + let _ = self.flush_debug_logs(FlushMode::Success); + + self.extension_order = original_order; + return ResultUnion::Success(Result { + path_pair: PathPair { + primary: Path::init_with_namespace(import_path, b"dataurl"), + secondary: None, + }, + ..Default::default() + }); + } + + // "background: url(data:image/png;base64,iVBORw0KGgo=);" + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note(b"Marking this \"dataurl\" as external".to_vec()); + } + let _ = self.flush_debug_logs(FlushMode::Success); + + self.extension_order = original_order; + return ResultUnion::Success(Result { + path_pair: PathPair { + primary: Path::init_with_namespace(import_path, b"dataurl"), + secondary: None, + }, + flags: ResultFlags::IS_EXTERNAL, + ..Default::default() + }); + } + Ok(None) => {} + } + + // When using `bun build --compile`, module resolution is never + // relative to our special /$bunfs/ directory. + // + // It's always relative to the current working directory of the project root. + // + // ...unless you pass a relative path that exists in the standalone module graph executable. + let mut source_dir_resolver = bun_paths::PosixToWinNormalizer::default(); + let source_dir_normalized: &[u8] = 'brk: { + if let Some(graph) = self.standalone_module_graph { + if ::bun_options_types::standalone_path::is_bun_standalone_file_path(import_path) { + if graph.find_assume_standalone_path(import_path).is_some() { + self.extension_order = original_order; + return ResultUnion::Success(Result { + import_kind: kind, + path_pair: PathPair { + primary: Path::init(import_path), + secondary: None, + }, + module_type: options::ModuleType::Esm, + flags: ResultFlags::IS_STANDALONE_MODULE, + ..Default::default() + }); + } + + self.extension_order = original_order; + return ResultUnion::NotFound; + } else if ::bun_options_types::standalone_path::is_bun_standalone_file_path( + source_dir, + ) { + if import_path.len() > 2 && is_dot_slash(&import_path[0..2]) { + let buf = bufs!(import_path_for_standalone_module_graph); + let joined = bun_paths::join_abs_string_buf( + source_dir, + buf, + &[import_path], + bun_paths::Platform::Loose, + ); + + // Support relative paths in the graph + if let Some(file_name) = graph.find_assume_standalone_path(joined) { + // Intern: trait borrows into the graph; `Path::init` + // needs `'static` (DirnameStore-backed). + let file_name = Fs::file_system::DirnameStore::instance() + .append_slice(file_name) + .expect("unreachable"); + self.extension_order = original_order; + return ResultUnion::Success(Result { + import_kind: kind, + path_pair: PathPair { + primary: Path::init(file_name), + secondary: None, + }, + module_type: options::ModuleType::Esm, + flags: ResultFlags::IS_STANDALONE_MODULE, + ..Default::default() + }); + } + } + break 'brk Fs::FileSystem::instance().top_level_dir; + } + } + + // Fail now if there is no directory to resolve in. This can happen for + // virtual modules (e.g. stdin) if a resolve directory is not specified. + // + // TODO: This is skipped for now because it is impossible to set a + // resolveDir so we default to the top level directory instead (this + // is backwards compat with Bun 1.0 behavior) + // See https://github.com/oven-sh/bun/issues/8994 for more details. + if source_dir.is_empty() { + // if let Some(debug) = self.debug_logs.as_mut() { + // debug.add_note(b"Cannot resolve this path without a directory".to_vec()); + // let _ = self.flush_debug_logs(FlushMode::Fail); + // } + // return ResultUnion::Failure(bun_core::err!("MissingResolveDir")); + break 'brk Fs::FileSystem::instance().top_level_dir; + } + + // This can also be hit if you use plugins with non-file namespaces, + // or call the module resolver from javascript (Bun.resolveSync) + // with a faulty parent specifier. + if !bun_paths::is_absolute(source_dir) { + // if let Some(debug) = self.debug_logs.as_mut() { + // debug.add_note(b"Cannot resolve this path without an absolute directory".to_vec()); + // let _ = self.flush_debug_logs(FlushMode::Fail); + // } + // return ResultUnion::Failure(bun_core::err!("InvalidResolveDir")); + break 'brk Fs::FileSystem::instance().top_level_dir; + } + + break 'brk source_dir_resolver + .resolve_cwd(source_dir) + .unwrap_or_else(|_| panic!("Failed to query CWD")); + }; + + // r.mutex.lock(); + // defer r.mutex.unlock(); + // errdefer (r.flushDebugLogs(.fail) catch {}) — handled at each error return below + + // A path with a null byte cannot exist on the filesystem. Continuing + // anyways would cause assertion failures. + if strings::index_of_char(import_path, 0).is_some() { + let _ = self.flush_debug_logs(FlushMode::Fail); + self.extension_order = original_order; + return ResultUnion::NotFound; + } + + let mut tmp = + self.resolve_without_symlinks(source_dir_normalized, import_path, kind, global_cache); + + // Fragments in URLs in CSS imports are technically expected to work + if matches!(tmp, ResultUnion::NotFound) && kind.is_from_css() { + 'try_without_suffix: { + // If resolution failed, try again with the URL query and/or hash removed + let maybe_suffix = strings::index_of_any(import_path, b"?#"); + let Some(suffix) = maybe_suffix else { + break 'try_without_suffix; + }; + if suffix < 1 { + break 'try_without_suffix; + } + + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Retrying resolution after removing the suffix {}", + bstr::BStr::new(&import_path[suffix..]) + )); + } + let result2 = self.resolve_without_symlinks( + source_dir_normalized, + &import_path[0..suffix], + kind, + global_cache, + ); + if matches!(result2, ResultUnion::NotFound) { + break 'try_without_suffix; + } + tmp = result2; + } + } + + let ret = match tmp { + ResultUnion::Success(mut result) => { + if result.path_pair.primary.namespace() != b"node" + && !result.flags.is_standalone_module() + { + if let Err(err) = self.finalize_result(&mut result, kind) { + self.extension_order = original_order; + return ResultUnion::Failure(err); + } + } + + let _ = self.flush_debug_logs(FlushMode::Success); + result.import_kind = kind; + if cfg!(feature = "debug_logs") { + // TODO(port): debuglog! with bun.fmt.fmtPath formatting + } + ResultUnion::Success(result) + } + ResultUnion::Failure(e) => { + let _ = self.flush_debug_logs(FlushMode::Fail); + ResultUnion::Failure(e) + } + ResultUnion::Pending(pending) => { + let _ = self.flush_debug_logs(FlushMode::Fail); + ResultUnion::Pending(pending) + } + ResultUnion::NotFound => { + let _ = self.flush_debug_logs(FlushMode::Fail); + ResultUnion::NotFound + } + }; + + // (tracing `elapsed` accumulation handled by `_elapsed_guard` above on all paths) + self.extension_order = original_order; + ret + } + + pub fn resolve( + &mut self, + source_dir: &[u8], + import_path: &[u8], + kind: ast::ImportKind, + ) -> core::result::Result { + // TODO(port): narrow error set + match self.resolve_and_auto_install(source_dir, import_path, kind, GlobalCache::disable) { + ResultUnion::Success(result) => Ok(result), + ResultUnion::Pending(_) | ResultUnion::NotFound => { + Err(bun_core::err!("ModuleNotFound")) + } + ResultUnion::Failure(e) => Err(e), + } + } + + /// Runs a resolution but also checking if a Bun Bake framework has an + /// override. This is used in one place in the bundler. + pub fn resolve_with_framework( + &mut self, + source_dir: &[u8], + import_path: &[u8], + kind: ast::ImportKind, + ) -> core::result::Result { + // SAFETY: PORT — `import_path` is caller-interned (source text / DirnameStore) + // and outlives the returned Result. TODO(port): thread explicit lifetime. + let import_path: &'static [u8] = unsafe { &*std::ptr::from_ref::<[u8]>(import_path) }; + // TODO(port): narrow error set + if let Some(f) = self.opts.framework.as_ref() { + if let Some(mod_) = f.built_in_modules.get(import_path) { + match mod_ { + // TYPE_ONLY(b0): BuiltInModule relocated bun_runtime::bake::framework → bun_options_types (T3) + bun_options_types::BuiltInModule::Code(_) => { + return Ok(Result { + import_kind: kind, + path_pair: PathPair { + primary: Fs::Path::init_with_namespace(import_path, b"node"), + secondary: None, + }, + module_type: options::ModuleType::Esm, + primary_side_effects_data: SideEffects::NoSideEffectsPureData, + flags: ResultFlags::default(), + ..Default::default() + }); + } + bun_options_types::BuiltInModule::Import(path) => { + // PORT NOTE: copy out `path` so the `&self.opts.framework` borrow + // ends before `self.resolve(&mut self, ...)`. + let path: &'static [u8] = + unsafe { &*std::ptr::from_ref::<[u8]>(path.as_ref()) }; + let top = self.fs_ref().top_level_dir; + return self.resolve(top, path, ast::ImportKind::EntryPointBuild); + } + } + // unreachable in Zig (return after switch) + } + } + self.resolve(source_dir, import_path, kind) + } + + pub fn finalize_result( + &mut self, + result: &mut Result, + kind: ast::ImportKind, + ) -> core::result::Result<(), bun_core::Error> { + // TODO(port): narrow error set + if result.flags.is_external() { + return Ok(()); + } + + let mut iter = result.path_pair.iter(); + let mut module_type = result.module_type; + while let Some(path) = iter.next() { + let Ok(Some(dir)) = self.read_dir_info(path.name.dir) else { + continue; + }; + let mut needs_side_effects = true; + if let Some(existing) = Result::deref_package_json(result.package_json) { + // if we don't have it here, they might put it in a sideEfffects + // map of the parent package.json + // TODO: check if webpack also does this parent lookup + use crate::package_json::SideEffects as PJSideEffects; + needs_side_effects = matches!( + existing.side_effects, + PJSideEffects::Unspecified | PJSideEffects::Glob(_) | PJSideEffects::Mixed(_) + ); + + result.primary_side_effects_data = match &existing.side_effects { + PJSideEffects::Unspecified => SideEffects::HasSideEffects, + PJSideEffects::False => SideEffects::NoSideEffectsPackageJson, + PJSideEffects::Map(map) => { + if map.contains_key(&crate::package_json::StringHashMapUnownedKey::init( + path.text(), + )) { + SideEffects::HasSideEffects + } else { + SideEffects::NoSideEffectsPackageJson + } + } + PJSideEffects::Glob(_) => { + if existing.side_effects.has_side_effects(path.text()) { + SideEffects::HasSideEffects + } else { + SideEffects::NoSideEffectsPackageJson + } + } + PJSideEffects::Mixed(_) => { + if existing.side_effects.has_side_effects(path.text()) { + SideEffects::HasSideEffects + } else { + SideEffects::NoSideEffectsPackageJson + } + } + }; + + if existing.name.is_empty() || self.care_about_bin_folder { + result.package_json = None; + } + } + + result.package_json = result + .package_json + .or(dir.enclosing_package_json.map(|p| std::ptr::from_ref(p))); + + if needs_side_effects { + if let Some(package_json) = Result::deref_package_json(result.package_json) { + use crate::package_json::SideEffects as PJSideEffects; + result.primary_side_effects_data = match &package_json.side_effects { + PJSideEffects::Unspecified => SideEffects::HasSideEffects, + PJSideEffects::False => SideEffects::NoSideEffectsPackageJson, + PJSideEffects::Map(map) => { + if map.contains_key( + &crate::package_json::StringHashMapUnownedKey::init(path.text()), + ) { + SideEffects::HasSideEffects + } else { + SideEffects::NoSideEffectsPackageJson + } + } + PJSideEffects::Glob(_) => { + if package_json.side_effects.has_side_effects(path.text()) { + SideEffects::HasSideEffects + } else { + SideEffects::NoSideEffectsPackageJson + } + } + PJSideEffects::Mixed(_) => { + if package_json.side_effects.has_side_effects(path.text()) { + SideEffects::HasSideEffects + } else { + SideEffects::NoSideEffectsPackageJson + } + } + }; + } + } + + if let Some(tsconfig) = dir.enclosing_tsconfig_json { + result.jsx = tsconfig.merge_jsx(result.jsx.clone()); + result.flags.set_emit_decorator_metadata( + result.flags.emit_decorator_metadata() || tsconfig.emit_decorator_metadata, + ); + result.flags.set_experimental_decorators( + result.flags.experimental_decorators() || tsconfig.experimental_decorators, + ); + } + + // If you use mjs or mts, then you're using esm + // If you use cjs or cts, then you're using cjs + // This should win out over the module type from package.json + if !kind.is_from_css() + && module_type == options::ModuleType::Unknown + && path.name.ext.len() == 4 + { + module_type = + module_type_from_ext(path.name.ext).unwrap_or(options::ModuleType::Unknown); + } + + if let Some(entries) = dir.get_entries_ref(self.generation) { + if let Some(query) = entries.get(path.name.filename) { + let symlink_path = query.entry().symlink(self.rfs_ptr(), self.store_fd); + if !symlink_path.is_empty() { + path.set_realpath(symlink_path); + if !result.file_fd.is_valid() { + result.file_fd = query.entry().cache().fd; + } + + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Resolved symlink \"{}\" to \"{}\"", + bstr::BStr::new(path.text()), + bstr::BStr::new(symlink_path) + )); + } + } else if !dir.abs_real_path.is_empty() { + // When the directory is a symlink, we don't need to call getFdPath. + let parts = [dir.abs_real_path.as_ref(), query.entry().base()]; + let mut buf = bun_paths::PathBuffer::uninit(); + + // PORT NOTE: `abs_buf` returns a borrow of `buf`; capture only the + // length so `buf` can be re-borrowed for null-termination below. + let out_len = self.fs_ref().abs_buf(&parts, &mut buf).len(); + + let store_fd = self.store_fd; + + if !query.entry().cache().fd.is_valid() && store_fd { + buf[out_len] = 0; + // SAFETY: buf[out_len] == 0 written above + let span = bun_core::ZStr::from_buf(&buf[..], out_len); + // Spec resolver.zig:1099 uses `try std.fs.openFileAbsoluteZ`, + // which propagates I/O errors so `resolveAndAutoInstall` can + // return them as `Result.Union.failure`. Mirror that — never + // panic on EACCES/EMFILE/ELOOP here. + let file = bun_sys::open(span, bun_sys::O::RDONLY, 0) + .map_err(Into::::into)?; + query.entry().set_cache_fd(file); + Fs::FileSystem::set_max_fd(file.native()); + } + + // PORT NOTE: snapshot `need_to_close_files` and raw-ptr the entry so + // the closure captures only Copy values — keeps `self` and + // `query.entry` reborrowable across the guard's lifetime. + let need_close = self.fs_ref().fs.need_to_close_files(); + // ARENA — Entry lives in the BSSMap singleton; guard runs before + // the slot is reused (resolver mutex held). Capture as `BackRef` + // (Copy, Deref) so the closure stays Copy-only while the read is + // a safe `BackRef::get()` instead of a raw-ptr deref. + let entry_ref = bun_ptr::BackRef::::from( + core::ptr::NonNull::new(query.entry).expect("EntryStore slot"), + ); + scopeguard::defer! { + if need_close { + let e = entry_ref.get(); + let fd = e.cache().fd; + if fd.is_valid() { + fd.close(); + e.set_cache_fd(FD::INVALID); + } + } + } + + let symlink = + Fs::FilenameStore::instance().append_slice(&buf[..out_len])?; + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Resolved symlink \"{}\" to \"{}\"", + bstr::BStr::new(symlink), + bstr::BStr::new(path.text()) + )); + } + query.entry().set_cache_symlink(PathString::init(symlink)); + if !result.file_fd.is_valid() && store_fd { + result.file_fd = query.entry().cache().fd; + } + + path.set_realpath(symlink); + } + } + } + } + + if !kind.is_from_css() && module_type == options::ModuleType::Unknown { + if let Some(pkg) = result.package_json_ref() { + module_type = pkg.module_type; + } + } + + result.module_type = module_type; + Ok(()) + } + + pub fn resolve_without_symlinks( + &mut self, + source_dir: &[u8], + input_import_path: &'static [u8], + kind: ast::ImportKind, + global_cache: GlobalCache, + ) -> ResultUnion { + debug_assert!(bun_paths::is_absolute(source_dir)); + + let mut import_path = input_import_path; + + // This implements the module resolution algorithm from node.js, which is + // described here: https://nodejs.org/api/modules.html#modules_all_together + let mut result = Result { + path_pair: PathPair { + primary: Path::empty(), + secondary: None, + }, + jsx: self.opts.jsx.clone(), + ..Default::default() + }; + + // Return early if this is already an absolute path. In addition to asking + // the file system whether this is an absolute path, we also explicitly check + // whether it starts with a "/" and consider that an absolute path too. This + // is because relative paths can technically start with a "/" on Windows + // because it's not an absolute path on Windows. Then people might write code + // with imports that start with a "/" that works fine on Windows only to + // experience unexpected build failures later on other operating systems. + // Treating these paths as absolute paths on all platforms means Windows + // users will not be able to accidentally make use of these paths. + if bun_paths::is_absolute(import_path) { + // Collapse relative directory specifiers if they exist. Extremely + // loose check to avoid always doing this copy, but avoid spending + // too much time on the check. + if strings::index_of(import_path, b"..").is_some() { + let platform = bun_paths::Platform::AUTO; + let ends_with_dir = platform.is_separator(import_path[import_path.len() - 1]) + || (import_path.len() > 3 + && platform.is_separator(import_path[import_path.len() - 3]) + && import_path[import_path.len() - 2] == b'.' + && import_path[import_path.len() - 1] == b'.'); + let buf = bufs!(relative_abs_path); + let Some(abs) = self.fs_ref().abs_buf_checked(&[import_path], buf) else { + return ResultUnion::NotFound; + }; + let mut len = abs.len(); + if ends_with_dir { + buf[len] = platform.separator(); + len += 1; + } + // `bufs!` hands out an unconstrained-lifetime `&mut PathBuffer` + // (threadlocal storage); a safe reborrow satisfies `&'static [u8]`. + import_path = &buf[..len]; + } + + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "The import \"{}\" is being treated as an absolute path", + bstr::BStr::new(import_path) + )); + } + + // First, check path overrides from the nearest enclosing TypeScript "tsconfig.json" file + if let Ok(Some(dir_info)) = self.dir_info_cached(source_dir) { + if let Some(tsconfig) = dir_info.enclosing_tsconfig_json { + if tsconfig.paths.count() > 0 { + if let Some(res) = self.match_tsconfig_paths(tsconfig, import_path, kind) { + // We don't set the directory fd here because it might remap an entirely different directory + return ResultUnion::Success(Result { + path_pair: res.path_pair, + diff_case: res.diff_case, + package_json: res.package_json, + dirname_fd: res.dirname_fd, + file_fd: res.file_fd, + jsx: tsconfig.merge_jsx(result.jsx), + ..Default::default() + }); + } + } + } + } + + if self.opts.external.abs_paths.count() > 0 + && self.opts.external.abs_paths.contains(import_path) + { + // If the string literal in the source text is an absolute path and has + // been marked as an external module, mark it as *not* an absolute path. + // That way we preserve the literal text in the output and don't generate + // a relative path from the output directory to that path. + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "The path \"{}\" is marked as external by the user", + bstr::BStr::new(import_path) + )); + } + + return ResultUnion::Success(Result { + path_pair: PathPair { + primary: Path::init(import_path), + secondary: None, + }, + flags: ResultFlags::IS_EXTERNAL, + ..Default::default() + }); + } + + // Run node's resolution rules (e.g. adding ".js") + let mut normalizer = ResolvePath::PosixToWinNormalizer::default(); + if let Some(entry) = + self.load_as_file_or_directory(normalizer.resolve(source_dir, import_path), kind) + { + return ResultUnion::Success(Result { + dirname_fd: entry.dirname_fd, + path_pair: entry.path_pair, + diff_case: entry.diff_case, + package_json: entry.package_json, + file_fd: entry.file_fd, + jsx: self.opts.jsx.clone(), + ..Default::default() + }); + } + + return ResultUnion::NotFound; + } + + // Check both relative and package paths for CSS URL tokens, with relative + // paths taking precedence over package paths to match Webpack behavior. + let is_package_path_ = + kind != ast::ImportKind::EntryPointRun && is_package_path_not_absolute(import_path); + let check_relative = !is_package_path_ || kind.is_from_css(); + let check_package = is_package_path_; + + if check_relative { + if let Some(custom_paths) = self.custom_dir_paths { + // @branchHint(.unlikely) + bun_core::hint::cold(); + for custom_path in custom_paths { + let custom_utf8 = custom_path.to_utf8_without_ref(); + match self.check_relative_path( + custom_utf8.slice(), + import_path, + kind, + global_cache, + ) { + ResultUnion::Success(res) => return ResultUnion::Success(res), + ResultUnion::Pending(p) => return ResultUnion::Pending(p), + ResultUnion::Failure(p) => return ResultUnion::Failure(p), + ResultUnion::NotFound => {} + } + } + debug_assert!(!check_package); // always from JavaScript + return ResultUnion::NotFound; // bail out now since there isn't anywhere else to check + } else { + match self.check_relative_path(source_dir, import_path, kind, global_cache) { + ResultUnion::Success(res) => return ResultUnion::Success(res), + ResultUnion::Pending(p) => return ResultUnion::Pending(p), + ResultUnion::Failure(p) => return ResultUnion::Failure(p), + ResultUnion::NotFound => {} + } + } + } + + if check_package { + if self.opts.polyfill_node_globals { + let had_node_prefix = import_path.starts_with(b"node:"); + let import_path_without_node_prefix = if had_node_prefix { + &import_path[b"node:".len()..] + } else { + import_path + }; + + if let Some(fallback_module) = + NodeFallbackModules::map().get(import_path_without_node_prefix) + { + result.path_pair.primary = fallback_module.path.clone(); + result.module_type = options::ModuleType::Cjs; + // @ptrFromInt(@intFromPtr(...)) — cast away constness + result.package_json = Some(std::ptr::from_ref::( + fallback_module.package_json, + )); + result.flags.set_is_from_node_modules(true); + return ResultUnion::Success(result); + } + + if had_node_prefix { + // Module resolution fails automatically for unknown node builtins + if !HardcodedAlias::has( + import_path_without_node_prefix, + options::Target::Node, + HardcodedAliasCfg::default(), + ) { + return ResultUnion::NotFound; + } + + // Valid node:* modules becomes {} in the output + result.path_pair.primary.namespace = b"node"; + result.path_pair.primary.text = import_path_without_node_prefix; + result.path_pair.primary.name = + Fs::PathName::init(import_path_without_node_prefix); + result.module_type = options::ModuleType::Cjs; + result.path_pair.primary.is_disabled = true; + result.flags.set_is_from_node_modules(true); + result.primary_side_effects_data = SideEffects::NoSideEffectsPureData; + return ResultUnion::Success(result); + } + + // Always mark "fs" as disabled, matching Webpack v4 behavior + if import_path_without_node_prefix.starts_with(b"fs") + && (import_path_without_node_prefix.len() == 2 + || import_path_without_node_prefix[2] == b'/') + { + result.path_pair.primary.namespace = b"node"; + result.path_pair.primary.text = import_path_without_node_prefix; + result.path_pair.primary.name = + Fs::PathName::init(import_path_without_node_prefix); + result.module_type = options::ModuleType::Cjs; + result.path_pair.primary.is_disabled = true; + result.flags.set_is_from_node_modules(true); + result.primary_side_effects_data = SideEffects::NoSideEffectsPureData; + return ResultUnion::Success(result); + } + } + + // Check for external packages first + if self.opts.external.node_modules.count() > 0 + // Imports like "process/" need to resolve to the filesystem, not a builtin + && !import_path.ends_with(b"/") + { + let mut query = import_path; + loop { + if self.opts.external.node_modules.contains(query) { + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "The path \"{}\" was marked as external by the user", + bstr::BStr::new(query) + )); + } + return ResultUnion::Success(Result { + path_pair: PathPair { + primary: Path::init(query), + secondary: None, + }, + flags: ResultFlags::IS_EXTERNAL, + ..Default::default() + }); + } + + // If the module "foo" has been marked as external, we also want to treat + // paths into that module such as "foo/bar" as external too. + let Some(slash) = strings::last_index_of_char(query, b'/') else { + break; + }; + query = &query[0..slash]; + } + } + + if let Some(custom_paths) = self.custom_dir_paths { + bun_core::hint::cold(); + for custom_path in custom_paths { + let custom_utf8 = custom_path.to_utf8_without_ref(); + match self.check_package_path( + custom_utf8.slice(), + import_path, + kind, + global_cache, + ) { + ResultUnion::Success(res) => return ResultUnion::Success(res), + ResultUnion::Pending(p) => return ResultUnion::Pending(p), + ResultUnion::Failure(p) => return ResultUnion::Failure(p), + ResultUnion::NotFound => {} + } + } + } else { + match self.check_package_path(source_dir, import_path, kind, global_cache) { + ResultUnion::Success(res) => return ResultUnion::Success(res), + ResultUnion::Pending(p) => return ResultUnion::Pending(p), + ResultUnion::Failure(p) => return ResultUnion::Failure(p), + ResultUnion::NotFound => {} + } + } + } + + ResultUnion::NotFound + } + + pub fn check_relative_path( + &mut self, + source_dir: &[u8], + import_path: &[u8], + kind: ast::ImportKind, + global_cache: GlobalCache, + ) -> ResultUnion { + let Some(abs_path) = self + .fs_ref() + .abs_buf_checked(&[source_dir, import_path], bufs!(relative_abs_path)) + else { + return ResultUnion::NotFound; + }; + + if self.opts.external.abs_paths.count() > 0 + && self.opts.external.abs_paths.contains(abs_path) + { + // If the string literal in the source text is an absolute path and has + // been marked as an external module, mark it as *not* an absolute path. + // That way we preserve the literal text in the output and don't generate + // a relative path from the output directory to that path. + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "The path \"{}\" is marked as external by the user", + bstr::BStr::new(abs_path) + )); + } + + return ResultUnion::Success(Result { + path_pair: PathPair { + primary: Path::init( + self.fs_ref() + .dirname_store + .append_slice(abs_path) + .expect("oom"), + ), + secondary: None, + }, + flags: ResultFlags::IS_EXTERNAL, + ..Default::default() + }); + } + + // Check the "browser" map + if self.care_about_browser_field { + let dirname = bun_paths::dirname(abs_path).expect("unreachable"); + if let Ok(Some(import_dir_info_outer)) = self.dir_info_cached(dirname) { + if let Some(import_dir_info) = import_dir_info_outer.get_enclosing_browser_scope() { + let pkg = import_dir_info.package_json().unwrap(); + if let Some(remap) = self + .check_browser_map::<{ BrowserMapPathKind::AbsolutePath }>( + &import_dir_info, + abs_path, + ) + { + // Is the path disabled? + if remap.is_empty() { + let mut _path = Path::init( + self.fs_ref() + .dirname_store + .append_slice(abs_path) + .expect("unreachable"), + ); + _path.is_disabled = true; + return ResultUnion::Success(Result { + path_pair: PathPair { + primary: _path, + secondary: None, + }, + ..Default::default() + }); + } + + match self.resolve_without_remapping( + import_dir_info, + remap, + kind, + global_cache, + ) { + MatchResultUnion::Success(match_result) => { + let mut flags = ResultFlags::default(); + flags.set_is_external(match_result.is_external); + flags.set_is_external_and_rewrite_import_path( + match_result.is_external, + ); + return ResultUnion::Success(Result { + path_pair: match_result.path_pair, + diff_case: match_result.diff_case, + dirname_fd: match_result.dirname_fd, + package_json: Some(std::ptr::from_ref(pkg)), + jsx: self.opts.jsx.clone(), + module_type: match_result.module_type, + flags, + ..Default::default() + }); + } + _ => {} + } + } + } + } + } + + let prev_extension_order = self.extension_order; + // PORT NOTE: defer restore reshaped — restored before each return + if strings::path_contains_node_modules_folder(abs_path) { + self.extension_order = self.opts.extension_order.kind(kind, true); + } + let ret = if let Some(res) = self.load_as_file_or_directory(abs_path, kind) { + ResultUnion::Success(Result { + path_pair: res.path_pair, + diff_case: res.diff_case, + dirname_fd: res.dirname_fd, + package_json: res.package_json, + jsx: self.opts.jsx.clone(), + ..Default::default() + }) + } else { + ResultUnion::NotFound + }; + self.extension_order = prev_extension_order; + ret + } + + pub fn check_package_path( + &mut self, + source_dir: &[u8], + unremapped_import_path: &'static [u8], + kind: ast::ImportKind, + global_cache: GlobalCache, + ) -> ResultUnion { + let mut import_path = unremapped_import_path; + let mut source_dir_info: DirInfoRef = match self.dir_info_cached(source_dir) { + Err(_) => return ResultUnion::NotFound, + Ok(Some(d)) => d, + Ok(None) => 'dir: { + // It is possible to resolve with a source file that does not exist: + // A. Bundler plugin refers to a non-existing `resolveDir`. + // B. `createRequire()` is called with a path that does not exist. This was + // hit in Nuxt, specifically the `vite-node` dependency [1]. + // + // Normally it would make sense to always bail here, but in the case of + // resolving "hello" from "/project/nonexistent_dir/index.ts", resolution + // should still query "/project/node_modules" and "/node_modules" + // + // For case B in Node.js, they use `_resolveLookupPaths` in + // combination with `_nodeModulePaths` to collect a listing of + // all possible parent `node_modules` [2]. Bun has a much smarter + // approach that caches directory entries, but it (correctly) does + // not cache non-existing directories. To successfully resolve this, + // Bun finds the nearest existing directory, and uses that as the base + // for `node_modules` resolution. Since that directory entry knows how + // to resolve concrete node_modules, this iteration stops at the first + // existing directory, regardless of what it is. + // + // The resulting `source_dir_info` cannot resolve relative files. + // + // [1]: https://github.com/oven-sh/bun/issues/16705 + // [2]: https://github.com/nodejs/node/blob/e346323109b49fa6b9a4705f4e3816fc3a30c151/lib/internal/modules/cjs/loader.js#L1934 + if cfg!(debug_assertions) { + debug_assert!(is_package_path(import_path)); + } + let mut closest_dir = source_dir; + // Use std.fs.path.dirname to get `null` once the entire + // directory tree has been visited. `null` is theoretically + // impossible since the drive root should always exist. + while let Some(current) = bun_paths::dirname(closest_dir) { + match self.dir_info_cached(current) { + Err(_) => return ResultUnion::NotFound, + Ok(Some(dir)) => break 'dir dir, + Ok(None) => {} + } + closest_dir = current; + } + return ResultUnion::NotFound; + } + }; + + if self.care_about_browser_field { + // Support remapping one package path to another via the "browser" field + if let Some(browser_scope) = source_dir_info.get_enclosing_browser_scope() { + if let Some(package_json) = browser_scope.package_json() { + if let Some(remapped) = self + .check_browser_map::<{ BrowserMapPathKind::PackagePath }>( + &browser_scope, + import_path, + ) + { + if remapped.is_empty() { + // "browser": {"module": false} + // does the module exist in the filesystem? + match self.load_node_modules( + import_path, + kind, + source_dir_info, + global_cache, + false, + ) { + MatchResultUnion::Success(node_module) => { + let mut pair = node_module.path_pair; + pair.primary.is_disabled = true; + if let Some(sec) = pair.secondary.as_mut() { + sec.is_disabled = true; + } + return ResultUnion::Success(Result { + path_pair: pair, + dirname_fd: node_module.dirname_fd, + diff_case: node_module.diff_case, + package_json: Some(std::ptr::from_ref(package_json)), + jsx: self.opts.jsx.clone(), + ..Default::default() + }); + } + _ => { + // "browser": {"module": false} + // the module doesn't exist and it's disabled + // so we should just not try to load it + let mut primary = Path::init(import_path); + primary.is_disabled = true; + return ResultUnion::Success(Result { + path_pair: PathPair { + primary, + secondary: None, + }, + diff_case: None, + jsx: self.opts.jsx.clone(), + ..Default::default() + }); + } + } + } + + import_path = remapped; + source_dir_info = browser_scope; + } + } + } + } + + match self.resolve_without_remapping(source_dir_info, import_path, kind, global_cache) { + MatchResultUnion::Success(res) => { + let mut result = Result { + path_pair: PathPair { + primary: Path::empty(), + secondary: None, + }, + jsx: self.opts.jsx.clone(), + ..Default::default() + }; + result.path_pair = res.path_pair; + result.dirname_fd = res.dirname_fd; + result.file_fd = res.file_fd; + result.package_json = res.package_json; + result.diff_case = res.diff_case; + result.flags.set_is_from_node_modules( + result.flags.is_from_node_modules() || res.is_node_module, + ); + result.jsx = self.opts.jsx.clone(); + result.module_type = res.module_type; + result.flags.set_is_external(res.is_external); + // Potentially rewrite the import path if it's external that + // was remapped to a different path + result + .flags + .set_is_external_and_rewrite_import_path(result.flags.is_external()); + + if result.path_pair.primary.is_disabled && result.path_pair.secondary.is_none() { + return ResultUnion::Success(result); + } + + if res.package_json.is_some() && self.care_about_browser_field { + let base_dir_info = match res.dir_info { + Some(d) => d, + None => match self.read_dir_info(result.path_pair.primary.name.dir) { + Ok(Some(d)) => d, + _ => return ResultUnion::Success(result), + }, + }; + if let Some(browser_scope) = base_dir_info.get_enclosing_browser_scope() { + if let Some(remap) = self + .check_browser_map::<{ BrowserMapPathKind::AbsolutePath }>( + &browser_scope, + result.path_pair.primary.text(), + ) + { + if remap.is_empty() { + result.path_pair.primary.is_disabled = true; + result.path_pair.primary = + Fs::Path::init_with_namespace(remap, b"file"); + } else { + match self.resolve_without_remapping( + browser_scope, + remap, + kind, + global_cache, + ) { + MatchResultUnion::Success(remapped) => { + result.path_pair = remapped.path_pair; + result.dirname_fd = remapped.dirname_fd; + result.file_fd = remapped.file_fd; + result.package_json = remapped.package_json; + result.diff_case = remapped.diff_case; + result.module_type = remapped.module_type; + result.flags.set_is_external(remapped.is_external); + + // Potentially rewrite the import path if it's external that + // was remapped to a different path + result.flags.set_is_external_and_rewrite_import_path( + result.flags.is_external(), + ); + + result.flags.set_is_from_node_modules( + result.flags.is_from_node_modules() + || remapped.is_node_module, + ); + return ResultUnion::Success(result); + } + _ => {} + } + } + } + } + } + + ResultUnion::Success(result) + } + MatchResultUnion::Pending(p) => ResultUnion::Pending(p), + MatchResultUnion::Failure(p) => ResultUnion::Failure(p), + _ => ResultUnion::NotFound, + } + } + + // This is a fallback, hopefully not called often. It should be relatively quick because everything should be in the cache. + pub fn package_json_for_resolved_node_module( + &mut self, + result: &Result, + ) -> Option<*const PackageJSON> { + let mut dir_info = self + .dir_info_cached(result.path_pair.primary.name.dir) + .ok() + .flatten()?; + loop { + if let Some(pkg) = dir_info.package_json() { + // if it doesn't have a name, assume it's something just for adjusting the main fields (react-bootstrap does this) + // In that case, we really would like the top-level package that you download from NPM + // so we ignore any unnamed packages + return Some(std::ptr::from_ref(pkg)); + } + + dir_info = dir_info.get_parent()?; + } + } + + pub fn root_node_module_package_json(&mut self, result: &Result) -> Option> { + let path = result.path_const()?; + let mut absolute = path.text(); + // /foo/node_modules/@babel/standalone/index.js + // ^------------^ + let mut end = strings::last_index_of(absolute, NODE_MODULE_ROOT_STRING).or_else(|| { + // try non-symlinked version + if path.pretty().len() != absolute.len() { + absolute = path.pretty(); + return strings::last_index_of(absolute, NODE_MODULE_ROOT_STRING); + } + None + })?; + end += NODE_MODULE_ROOT_STRING.len(); + + let is_scoped_package = absolute[end] == b'@'; + end += strings::index_of_char(&absolute[end..], SEP)? as usize; + + // /foo/node_modules/@babel/standalone/index.js + // ^ + if is_scoped_package { + end += 1; + end += strings::index_of_char(&absolute[end..], SEP)? as usize; + } + + end += 1; + + // /foo/node_modules/@babel/standalone/index.js + // ^ + let slice = &absolute[0..end]; + + // Try to avoid the hash table lookup whenever possible + // That can cause filesystem lookups in parent directories and it requires a lock + if let Some(pkg) = result.package_json_ref() { + if slice == pkg.source.path.name.dir_with_trailing_slash() { + return Some(RootPathPair { + package_json: std::ptr::from_ref(pkg), + base_path: slice, + }); + } + } + + { + let dir_info = self.dir_info_cached(slice).ok().flatten()?; + Some(RootPathPair { + base_path: slice, + package_json: std::ptr::from_ref(dir_info.package_json()?), + }) + } + } + + /// Directory cache keys must follow the following rules. If the rules are broken, + /// then there will be conflicting cache entries, and trying to bust the cache may not work. + /// + /// When an incorrect cache key is used, this assertion will trip; ignoring it allows + /// very very subtle cache invalidation issues to happen, which will cause modules to + /// mysteriously fail to resolve. + /// + /// The rules for this changed in https://github.com/oven-sh/bun/pull/9144 after multiple + /// cache issues were found on Windows. These issues extended to other platforms because + /// we never checked if the cache key was following the rules. + /// + /// CACHE KEY RULES: + /// A cache key must use native slashes, and must NOT end with a trailing slash. + /// But drive roots MUST have a trailing slash ('/' and 'C:\') + /// UNC paths, even if the root, must not have the trailing slash. + /// + /// The helper function bun.strings.withoutTrailingSlashWindowsPath can be used + /// to remove the trailing slash from a path + pub fn assert_valid_cache_key(path: &[u8]) { + if cfg!(debug_assertions) { + if path.len() > 1 + && strings::char_is_any_slash(path[path.len() - 1]) + && !if cfg!(windows) { + path.len() == 3 && path[1] == b':' + } else { + path.len() == 1 + } + { + panic!( + "Internal Assertion Failure: Invalid cache key \"{}\"\nSee Resolver.assertValidCacheKey for details.", + bstr::BStr::new(path) + ); + } + } + } + + /// Bust the directory cache for the given path. + /// See `assertValidCacheKey` for requirements on the input + pub fn bust_dir_cache(&mut self, path: &[u8]) -> bool { + Self::assert_valid_cache_key(path); + let first_bust = self.fs_mut().fs.bust_entries_cache(path); + let second_bust = self.dir_cache_mut().remove(path); + bun_core::scoped_log!( + Resolver, + "Bust {} = {}, {}", + bstr::BStr::new(path), + first_bust, + second_bust + ); + first_bust || second_bust + } + + /// bust both the named file and a parent directory, because `./hello` can resolve + /// to `./hello.js` or `./hello/index.js` + pub fn bust_dir_cache_from_specifier( + &mut self, + import_source_file: &[u8], + specifier: &[u8], + ) -> bool { + if bun_paths::is_absolute(specifier) { + let dir = bun_paths::dirname_platform(specifier, bun_paths::Platform::AUTO); + let a = self.bust_dir_cache(dir); + let b = self.bust_dir_cache(specifier); + return a || b; + } + + if !(specifier.starts_with(b"./") || specifier.starts_with(b"../")) { + return false; + } + if !bun_paths::is_absolute(import_source_file) { + return false; + } + + let joined = bun_paths::join_abs( + bun_paths::dirname_platform(import_source_file, bun_paths::Platform::AUTO), + bun_paths::Platform::AUTO, + specifier, + ); + let dir = bun_paths::dirname_platform(joined, bun_paths::Platform::AUTO); + + let a = self.bust_dir_cache(dir); + let b = self.bust_dir_cache(joined); + a || b + } + + pub fn load_node_modules( + &mut self, + import_path: &[u8], + kind: ast::ImportKind, + // PORT NOTE: `DirInfoRef` (not `&mut`) — body re-enters `dir_cache` via + // `dir_info_cached()` which, in the self-reference branch, returns the + // SAME BSSMap slot. A `&mut` param carries an FnEntry protector under + // Stacked Borrows; the inner retag would pop it (aliased-&mut UB). + // Spec resolver.zig:1761 takes raw `*DirInfo`; the arena handle Derefs + // to `&DirInfo` per use so overlapping shared reads are sound. + _dir_info: DirInfoRef, + global_cache: GlobalCache, + forbid_imports: bool, + ) -> MatchResultUnion { + let mut dir_info: DirInfoRef = _dir_info; + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Searching for {} in \"node_modules\" directories starting from \"{}\"", + bstr::BStr::new(import_path), + bstr::BStr::new(dir_info.abs_path) + )); + debug.increase_indent(); + } + // PORT NOTE: Zig `defer { debug.decreaseIndent() }` — reshaped for borrowck; + // `decrease_indent()` is called explicitly at every return point below. + + // First, check path overrides from the nearest enclosing TypeScript "tsconfig.json" file + + if let Some(tsconfig) = dir_info.enclosing_tsconfig_json { + // Try path substitutions first + if tsconfig.paths.count() > 0 { + if let Some(res) = self.match_tsconfig_paths(tsconfig, import_path, kind) { + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return MatchResultUnion::Success(res); + } + } + + // Try looking up the path relative to the base URL + if tsconfig.has_base_url() { + let base: &[u8] = &tsconfig.base_url; + if let Some(abs) = self.fs_ref().abs_buf_checked( + &[base, import_path], + bufs!(load_as_file_or_directory_via_tsconfig_base_path), + ) { + if let Some(res) = self.load_as_file_or_directory(abs, kind) { + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return MatchResultUnion::Success(res); + } + } + } + } + + let mut is_self_reference = false; + + // Find the parent directory with the "package.json" file + let mut dir_info_package_json: Option = Some(dir_info); + while let Some(d) = dir_info_package_json { + if d.package_json.is_some() { + break; + } + dir_info_package_json = d.get_parent(); + } + + // Check for subpath imports: https://nodejs.org/api/packages.html#subpath-imports + if let Some(_dir_info_package_json) = dir_info_package_json { + let package_json = _dir_info_package_json.package_json().unwrap(); + + if import_path.starts_with(b"#") && !forbid_imports && package_json.imports.is_some() { + let r = self.load_package_imports( + import_path, + _dir_info_package_json, + kind, + global_cache, + ); + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return r; + } + + // https://nodejs.org/api/packages.html#packages_self_referencing_a_package_using_its_name + let package_name = crate::package_json::Package::parse_name(import_path); + if let Some(_package_name) = package_name { + if _package_name == package_json.name.as_ref() && package_json.exports.is_some() { + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "\"{}\" is a self-reference", + bstr::BStr::new(import_path) + )); + } + dir_info = _dir_info_package_json; + is_self_reference = true; + } + } + } + + let esm_ = crate::package_json::Package::parse(import_path, bufs!(esm_subpath)); + + let source_dir_info = dir_info; + let mut any_node_modules_folder = false; + let use_node_module_resolver = global_cache != GlobalCache::force; + + // Then check for the package in any enclosing "node_modules" directories + // or in the package root directory if it's a self-reference + while use_node_module_resolver { + // Skip directories that are themselves called "node_modules", since we + // don't ever want to search for "node_modules/node_modules" + 'node_modules: { + if !(dir_info.has_node_modules() || is_self_reference) { + break 'node_modules; + } + any_node_modules_folder = true; + let abs_path: &[u8] = if is_self_reference { + dir_info.abs_path + } else { + match self.fs_ref().abs_buf_checked( + &[dir_info.abs_path, b"node_modules", import_path], + bufs!(node_modules_check), + ) { + Some(p) => p, + None => break 'node_modules, + } + }; + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Checking for a package in the directory \"{}\"", + bstr::BStr::new(abs_path) + )); + } + + let prev_extension_order = self.extension_order; + // PORT NOTE: defer restore reshaped — restored at end of block + + if let Some(ref esm) = esm_ { + let abs_package_path: &[u8] = if is_self_reference { + dir_info.abs_path + } else { + let parts = [dir_info.abs_path, b"node_modules".as_slice(), esm.name]; + self.fs_ref() + .abs_buf(&parts, bufs!(esm_absolute_package_path)) + }; + + if let Ok(Some(pkg_dir_info)) = self.dir_info_cached(abs_package_path) { + self.extension_order = match kind { + ast::ImportKind::Url + | ast::ImportKind::AtConditional + | ast::ImportKind::At => options::ExtOrder::Css, + _ => self.opts.extension_order.kind(kind, true), + }; + + if let Some(package_json) = pkg_dir_info.package_json() { + if let Some(exports_map) = package_json.exports.as_ref() { + // The condition set is determined by the kind of import + let mut module_type = package_json.module_type; + // PORT NOTE: reshaped for borrowck — Zig held a single `ESModule` + // with a raw `*DebugLogs` across both `resolve` calls and the + // intervening `handle_esm_resolution`. In Rust, keeping the + // `ESModule` (which holds `&mut self.debug_logs`) alive across a + // `&mut self` call is aliased-&mut UB. Build a fresh short-lived + // `ESModule` per `resolve` call so its borrow ends before + // `self.handle_esm_resolution` re-borrows `self`. + let conditions = match kind { + ast::ImportKind::Require | ast::ImportKind::RequireResolve => { + self.opts.conditions.require.clone().expect("oom") + } + ast::ImportKind::At | ast::ImportKind::AtConditional => { + self.opts.conditions.style.clone().expect("oom") + } + _ => self.opts.conditions.import.clone().expect("oom"), + }; + + // Resolve against the path "/", then join it with the absolute + // directory path. This is done because ESM package resolution uses + // URLs while our path resolution uses file system paths. We don't + // want problems due to Windows paths, which are very unlike URL + // paths. We also want to avoid any "%" characters in the absolute + // directory path accidentally being interpreted as URL escapes. + { + // PERF(port): extra conditions clone vs Zig — profile if hot. + let esm_resolution = ESModule { + conditions: conditions.clone().expect("oom"), + debug_logs: self.debug_logs.as_mut(), + module_type: &mut module_type, + } + .resolve(b"/", esm.subpath, &exports_map.root); + // ESModule temporary dropped here; `self` is unborrowed. + + if let Some(result) = self.handle_esm_resolution( + esm_resolution, + abs_package_path, + kind, + package_json, + esm.subpath, + ) { + let mut result_copy = result; + result_copy.is_node_module = true; + result_copy.module_type = module_type; + self.extension_order = prev_extension_order; + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return MatchResultUnion::Success(result_copy); + } + } + + // Some popular packages forget to include the extension in their + // exports map, so we try again without the extension. + // + // This is useful for browser-like environments + // where you want a file extension in the URL + // pathname by convention. Vite does this. + // + // React is an example of a package that doesn't include file extensions. + // { + // "exports": { + // ".": "./index.js", + // "./jsx-runtime": "./jsx-runtime.js", + // } + // } + // + // We limit this behavior just to ".js" files. + let extname = bun_paths::extension(esm.subpath); + if extname == b".js" && esm.subpath.len() > 3 { + let esm_resolution = ESModule { + conditions, + debug_logs: self.debug_logs.as_mut(), + module_type: &mut module_type, + } + .resolve( + b"/", + &esm.subpath[0..esm.subpath.len() - 3], + &exports_map.root, + ); + if let Some(result) = self.handle_esm_resolution( + esm_resolution, + abs_package_path, + kind, + package_json, + esm.subpath, + ) { + let mut result_copy = result; + result_copy.is_node_module = true; + result_copy.module_type = module_type; + self.extension_order = prev_extension_order; + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return MatchResultUnion::Success(result_copy); + } + } + + // if they hid "package.json" from "exports", still allow importing it. + if esm.subpath == b"./package.json" { + self.extension_order = prev_extension_order; + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return MatchResultUnion::Success(MatchResult { + // PORT NOTE: PackageJSON.source.path is bun_paths::fs::Path<'static>; convert + // to the resolver's interned crate::fs::Path<'static> via its text. + path_pair: PathPair { + primary: Path::init(package_json.source.path.text), + secondary: None, + }, + dirname_fd: pkg_dir_info.get_file_descriptor(), + file_fd: FD::INVALID, + // Spec resolver.zig:1930 — `Path.isNodeModule()` checks + // `lastIndexOf(name.dir, SEP++"node_modules"++SEP)`, i.e. a + // separator-bounded directory component on `name.dir` (not a + // bare substring of the full text). `bun_paths::fs::Path<'static>` + // doesn't carry that method, so re-derive via the resolver's + // `Path` (already done one line up for `path_pair.primary`). + is_node_module: Path::init(package_json.source.path.text) + .is_node_module(), + package_json: Some(std::ptr::from_ref(package_json)), + dir_info: Some(dir_info), + ..Default::default() + }); + } + + self.extension_order = prev_extension_order; + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return MatchResultUnion::NotFound; + } + } + } + } + + if let Some(res) = self.load_as_file_or_directory(abs_path, kind) { + self.extension_order = prev_extension_order; + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return MatchResultUnion::Success(res); + } + self.extension_order = prev_extension_order; + } + + match dir_info.get_parent() { + Some(p) => dir_info = p, + None => break, + } + } + + // try resolve from `NODE_PATH` + // https://nodejs.org/api/modules.html#loading-from-the-global-folders + let node_path: &[u8] = self + .env_loader() + .and_then(|env| env.get(b"NODE_PATH")) + .unwrap_or(b""); + if !node_path.is_empty() { + let delim = if cfg!(windows) { b';' } else { b':' }; + for path in node_path.split(|&b| b == delim).filter(|s| !s.is_empty()) { + let Some(abs_path) = self + .fs_ref() + .abs_buf_checked(&[path, import_path], bufs!(node_modules_check)) + else { + continue; + }; + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Checking for a package in the NODE_PATH directory \"{}\"", + bstr::BStr::new(abs_path) + )); + } + if let Some(res) = self.load_as_file_or_directory(abs_path, kind) { + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return MatchResultUnion::Success(res); + } + } + } + + dir_info = source_dir_info; + + // this is the magic! + if global_cache.can_use(any_node_modules_folder) + && self.use_package_manager() + && esm_.is_some() + && strings::is_npm_package_name(esm_.as_ref().unwrap().name) + { + let esm = esm_.as_ref().unwrap().with_auto_version(); + 'load_module_from_cache: { + // If the source directory doesn't have a node_modules directory, we can + // check the global cache directory for a package.json file. + // + // PORT NOTE (Stacked Borrows): `get_package_manager` returns the + // `*mut dyn AutoInstaller` raw pointer; the body below re-borrows + // `self` for `enqueue_dependency_to_resolve` / `debug_logs` / + // `log()`. The PackageManager lives in a separate allocation, so + // derive a raw pointer once and re-borrow per use — disjoint + // from `self`'s storage. + let manager_ptr: *mut dyn AutoInstaller = self.get_package_manager(); + // SAFETY: re-borrowed narrowly per use; PackageManager outlives resolver. + macro_rules! manager { + () => { + unsafe { &mut *manager_ptr } + }; + } + let mut dependency_version = Dependency::Version::default(); + let mut dependency_behavior = Dependency::Behavior::PROD; + let mut string_buf: &[u8] = esm.version; + + // const initial_pending_tasks = manager.pending_tasks; + let mut resolved_package_id: Install::PackageID = 'brk: { + // check if the package.json in the source directory was already added to the lockfile + // and try to look up the dependency from there + if let Some(package_json) = dir_info.package_json_for_dependencies() { + let mut dependencies_list: &[Dependency::Dependency] = &[]; + let resolve_from_lockfile = + package_json.package_manager_package_id != Install::INVALID_PACKAGE_ID; + + if resolve_from_lockfile { + let dependencies = manager!().lockfile_package_dependencies( + package_json.package_manager_package_id, + ); + + // try to find this package name in the dependencies of the enclosing package + dependencies_list = + dependencies.get(manager!().lockfile_dependencies_buf()); + string_buf = manager!().lockfile_string_bytes(); + } else if esm_.as_ref().unwrap().version.is_empty() { + // If you don't specify a version, default to the one chosen in your package.json + dependencies_list = package_json.dependencies.map.values(); + string_buf = package_json.dependencies.source_buf; + } + + for (dependency_id, dependency) in dependencies_list.iter().enumerate() { + if !strings::eql_long(dependency.name.slice(string_buf), esm.name, true) + { + continue; + } + + dependency_version = dependency.version.clone(); + dependency_behavior = dependency.behavior; + + if resolve_from_lockfile { + let resolutions = manager!().lockfile_package_resolutions( + package_json.package_manager_package_id, + ); + + // found it! + break 'brk resolutions.get(manager!().lockfile_resolutions_buf()) + [dependency_id]; + } + + break; + } + } + + // If we get here, it means that the lockfile doesn't have this package at all. + // we know nothing + break 'brk Install::INVALID_PACKAGE_ID; + }; + + // Now, there are two possible states: + // 1) We have resolved the package ID, either from the + // lockfile globally OR from the particular package.json + // dependencies list + // + // 2) We parsed the Dependency.Version but there is no + // existing resolved package ID + + // If its an exact version, we can just immediately look it up in the global cache and resolve from there + // If the resolved package ID is _not_ invalid, we can just check + + // If this returns null, then it means we need to *resolve* the package + // Even after resolution, we might still need to download the package + // There are two steps here! Two steps! + let resolution: Resolution = 'brk: { + if resolved_package_id == Install::INVALID_PACKAGE_ID { + if dependency_version.tag == Dependency::version::Tag::Uninitialized { + let sliced_string = + Semver::SlicedString::init(esm.version, esm.version); + if !esm_.as_ref().unwrap().version.is_empty() + && dir_info.enclosing_package_json.is_some() + && global_cache.allow_version_specifier() + { + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return MatchResultUnion::Failure(bun_core::err!( + "VersionSpecifierNotAllowedHere" + )); + } + string_buf = esm.version; + dependency_version = match manager!().parse_dependency( + Semver::String::init(esm.name, esm.name), + None, + esm.version, + &sliced_string, + self.log(), + ) { + Some(v) => v, + None => break 'load_module_from_cache, + }; + } + + if let Some(id) = manager!().lockfile_resolve(esm.name, &dependency_version) + { + resolved_package_id = id; + } + } + + if resolved_package_id != Install::INVALID_PACKAGE_ID { + break 'brk manager!().lockfile_package_resolution(resolved_package_id); + } + + // unsupported or not found dependency, we might need to install it to the cache + match self.enqueue_dependency_to_resolve( + // Read the raw `NonNull` fields directly (NOT the + // `&'static`-yielding accessors) so mut-provenance from + // `intern_package_json` survives to the write inside + // (Zig: resolver.zig:2074). + dir_info + .package_json_for_dependencies + .or(dir_info.package_json), + &esm, + dependency_behavior, + &mut resolved_package_id, + dependency_version.clone(), + string_buf, + ) { + DependencyToResolve::Resolution(res) => break 'brk res, + DependencyToResolve::Pending(pending) => { + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return MatchResultUnion::Pending(pending); + } + DependencyToResolve::Failure(err) => { + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return MatchResultUnion::Failure(err); + } + // this means we looked it up in the registry and the package doesn't exist or the version doesn't exist + DependencyToResolve::NotFound => { + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return MatchResultUnion::NotFound; + } + } + }; + + let dir_path_for_resolution = match manager!().path_for_resolution( + resolved_package_id, + &resolution, + bufs!(path_in_global_disk_cache), + ) { + Ok(p) => p, + Err(err) => { + // if it's missing, we need to install it + if err == bun_core::err!("FileNotFound") { + match manager!().get_preinstall_state(resolved_package_id) { + Install::PreinstallState::Done => { + // PORT NOTE: `MatchResult.path_pair` is `Path<'static>`; + // intern `import_path` so the disabled-module record + // outlives this frame (Zig had no lifetime here). + let interned = Fs::file_system::DirnameStore::instance() + .append_slice(import_path) + .expect("unreachable"); + let mut path = Fs::Path::init(interned); + path.is_disabled = true; + // this might mean the package is disabled + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return MatchResultUnion::Success(MatchResult { + path_pair: PathPair { + primary: path, + secondary: None, + }, + ..Default::default() + }); + } + st @ (Install::PreinstallState::Extract + | Install::PreinstallState::Extracting) => { + if !global_cache.can_install() { + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return MatchResultUnion::NotFound; + } + let (cloned, string_buf) = esm.copy().expect("unreachable"); + + if st == Install::PreinstallState::Extract { + let dependency_id = manager!() + .lockfile_legacy_package_to_dependency_id( + resolved_package_id, + ) + .expect("unreachable"); + // The npm version + URL live inside `resolution.value`; + // the `AutoInstaller` impl decodes them itself. + if let Err(enqueue_download_err) = manager!() + .enqueue_package_for_download( + esm.name, + dependency_id, + resolved_package_id, + &resolution, + Install::TaskCallbackContext { root_request_id: 0 }, + None, + ) + { + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return MatchResultUnion::Failure(enqueue_download_err); + } + } + + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return MatchResultUnion::Pending(PendingResolution { + esm: cloned, + dependency: dependency_version, + resolution_id: resolved_package_id, + string_buf, + tag: PendingResolutionTag::Download, + ..Default::default() + }); + } + _ => {} + } + } + + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return MatchResultUnion::Failure(err); + } + }; + + match self.dir_info_for_resolution(dir_path_for_resolution, resolved_package_id) { + Ok(dir_info_to_use_) => { + if let Some(pkg_dir_info) = dir_info_to_use_ { + let abs_package_path = pkg_dir_info.abs_path; + let mut module_type = options::ModuleType::Unknown; + if let Some(package_json) = pkg_dir_info.package_json() { + if let Some(exports_map) = package_json.exports.as_ref() { + // The condition set is determined by the kind of import + // PORT NOTE: reshaped for borrowck — see identical note above. + let conditions = match kind { + ast::ImportKind::Require + | ast::ImportKind::RequireResolve => { + self.opts.conditions.require.clone().expect("oom") + } + _ => self.opts.conditions.import.clone().expect("oom"), + }; + + // Resolve against the path "/", then join it with the absolute + // directory path. This is done because ESM package resolution uses + // URLs while our path resolution uses file system paths. We don't + // want problems due to Windows paths, which are very unlike URL + // paths. We also want to avoid any "%" characters in the absolute + // directory path accidentally being interpreted as URL escapes. + { + // PERF(port): extra conditions clone vs Zig — profile if hot. + let esm_resolution = ESModule { + conditions: conditions.clone().expect("oom"), + debug_logs: self.debug_logs.as_mut(), + module_type: &mut module_type, + } + .resolve(b"/", esm.subpath, &exports_map.root); + + if let Some(result) = self.handle_esm_resolution( + esm_resolution, + abs_package_path, + kind, + package_json, + esm.subpath, + ) { + let mut result_copy = result; + result_copy.is_node_module = true; + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return MatchResultUnion::Success(result_copy); + } + } + + // Some popular packages forget to include the extension in their + // exports map, so we try again without the extension. + // (same comment as above) + // + // We limit this behavior just to ".js" files. + let extname = bun_paths::extension(esm.subpath); + if extname == b".js" && esm.subpath.len() > 3 { + let esm_resolution = ESModule { + conditions, + debug_logs: self.debug_logs.as_mut(), + module_type: &mut module_type, + } + .resolve( + b"/", + &esm.subpath[0..esm.subpath.len() - 3], + &exports_map.root, + ); + if let Some(result) = self.handle_esm_resolution( + esm_resolution, + abs_package_path, + kind, + package_json, + esm.subpath, + ) { + let mut result_copy = result; + result_copy.is_node_module = true; + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return MatchResultUnion::Success(result_copy); + } + } + + // if they hid "package.json" from "exports", still allow importing it. + if esm.subpath == b"./package.json" { + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return MatchResultUnion::Success(MatchResult { + path_pair: PathPair { + primary: Fs::Path::init( + package_json.source.path.text, + ), + secondary: None, + }, + dirname_fd: pkg_dir_info.get_file_descriptor(), + file_fd: FD::INVALID, + is_node_module: package_json + .source + .path + .is_node_module(), + package_json: Some(std::ptr::from_ref(package_json)), + dir_info: Some(dir_info), + ..Default::default() + }); + } + + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return MatchResultUnion::NotFound; + } + } + + let Some(abs_path) = self.fs_ref().abs_buf_checked( + &[pkg_dir_info.abs_path, esm.subpath], + bufs!(node_modules_check), + ) else { + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return MatchResultUnion::NotFound; + }; + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Checking for a package in the directory \"{}\"", + bstr::BStr::new(abs_path) + )); + } + + if let Some(mut res) = self.load_as_file_or_directory(abs_path, kind) { + res.is_node_module = true; + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return MatchResultUnion::Success(res); + } + } + } + Err(err) => { + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return MatchResultUnion::Failure(err); + } + } + } + } + + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + MatchResultUnion::NotFound + } + + fn dir_info_for_resolution( + &mut self, + dir_path_maybe_trail_slash: &[u8], + package_id: Install::PackageID, + ) -> core::result::Result, bun_core::Error> { + // TODO(port): narrow error set + debug_assert!(self.package_manager.is_some()); + + let dir_path = strings::without_trailing_slash_windows_path(dir_path_maybe_trail_slash); + + Self::assert_valid_cache_key(dir_path); + // Stacked Borrows: bind ONE `&mut HashMap` and route both the lookup and the slot + // projection through it so the returned `*mut DirInfo` shares a parent tag with the + // borrow it was derived from (a second `dir_cache_mut()` Unique retag of the + // whole `BSSMapInner` would otherwise pop it). + let dc = self.dir_cache_mut(); + let mut dir_cache_info_result = dc.get_or_put(dir_path)?; + if dir_cache_info_result.status == allocators::Status::Exists { + // we've already looked up this package before + return Ok(dc + .at_index(dir_cache_info_result.index) + .map(DirInfoRef::from_slot)); + } + // SAFETY: PORT (Stacked Borrows) — derive `rfs` from the raw `*mut FileSystem` + // field via `addr_of_mut!` so later `&mut *self.log()` / `&mut *self.dir_cache()` + // retags below don't pop its provenance. Re-borrow `&mut *rfs` per use. + let rfs: *mut Fs::file_system::RealFS = self.rfs_ptr(); + macro_rules! rfs { + () => { + unsafe { &mut *rfs } + }; + } + // resolver mutex held; `EntriesMap` methods are safe wrappers over the singleton. + let mut cached_dir_entry_result = rfs!().entries.get_or_put(dir_path)?; + + // PORT NOTE: always assigned by either the cached-hit arm or the + // `needs_iter` block below; null-init so rustc accepts the proof. + let mut dir_entries_option: *mut Fs::file_system::real_fs::EntriesOption = + core::ptr::null_mut(); + let mut needs_iter = true; + let mut in_place: Option<*mut Fs::file_system::DirEntry> = None; + let open_dir = match bun_sys::open_dir_for_iteration(FD::cwd(), dir_path) { + Ok(d) => d, + Err(err) => { + // TODO: handle this error better + let _ = self.log_mut().add_error_fmt( + None, + bun_ast::Loc::EMPTY, + format_args!("Unable to open directory: {}", bstr::BStr::new(err.name())), + ); + return Err(err.into()); + } + }; + + if let Some(cached_entry) = rfs!().entries.at_index(cached_dir_entry_result.index) { + if let Fs::file_system::real_fs::EntriesOption::Entries(entries) = cached_entry { + if entries.generation >= self.generation { + dir_entries_option = cached_entry; + needs_iter = false; + } else { + in_place = Some(std::ptr::from_mut(*entries)); + } + } + } + + if needs_iter { + // SAFETY: (block-wide) `in_place`/`dir_entries_ptr`/`dir_entries_option` point to slots + // in `rfs.entries` (BSSMap singleton) or a fresh leaked Box; both outlive this fn and + // are accessed under `rfs.entries_mutex` (see LIFETIMES.tsv). + let mut new_entry = Fs::file_system::DirEntry::init( + if let Some(existing) = in_place { + // SAFETY: see block-wide note above. + unsafe { &*existing }.dir + } else { + Fs::file_system::DirnameStore::instance() + .append_slice(dir_path) + .expect("unreachable") + }, + self.generation, + ); + + // Pre-size `data` so the per-entry inserts below skip the + // 1→2→4→…→N hashbrown rehash cascade from an empty table. 64 + // covers a typical node_modules package dir; larger dirs still + // rehash from there (cheap relative to starting at 0). + new_entry.data.reserve(64); + + let mut dir_iterator = bun_sys::iterate_dir(open_dir); + // Hoist the `FilenameStore` singleton resolve out of the per-entry loop + // (see `DirEntry::add_entry` doc-comment) and reuse the appender state. + let mut filename_store = FilenameStoreAppender::new(); + while let Ok(Some(_value)) = dir_iterator.next() { + new_entry + .add_entry_with_store( + // SAFETY: see block-wide note above. + in_place.map(|existing| unsafe { &mut (*existing).data }), + &_value, + &mut filename_store, + (), + ) + .expect("unreachable"); + } + if let Some(existing) = in_place { + // SAFETY: see block-wide note above. + // PORT NOTE: Zig `clearAndFree` — `StringHashMap` (std::HashMap newtype) + // has no separate `clear_and_free`; `clear()` drops all entries. + unsafe { &mut *existing }.data.clear(); + } + + if self.store_fd { + new_entry.fd = open_dir; + } + // PORT NOTE: see `dir_info_cached_maybe_log` — `DirEntry.data` holds a `NonNull`, + // so a zeroed slot is UB; box `new_entry` directly for the fresh case. + let dir_entries_ptr = match in_place { + Some(p) => { + // SAFETY: dir_entries_ptr is a live BSSMap slot (`in_place`). + unsafe { *p = new_entry }; + p + } + None => bun_core::heap::into_raw(Box::new(new_entry)), + }; + + // bun.fs.debug("readdir({f}, {s}) = {d}", ...) — TODO(port): scoped log + + dir_entries_option = rfs!() + .entries + // SAFETY: see block-wide note above. + .put( + &mut cached_dir_entry_result, + Fs::file_system::real_fs::EntriesOption::Entries(unsafe { + &mut *dir_entries_ptr + }), + ) + .expect("unreachable"); + } + + // We must initialize it as empty so that the result index is correct. + // This is important so that browser_scope has a valid index. + // PORT NOTE: erase the `&mut DirInfo` borrow to `*mut` immediately so + // `self.dir_cache` (and `*self`) are reborrowable for the call below. + let dir_info_ptr: *mut DirInfo::DirInfo = self + .dir_cache_mut() + .put(&mut dir_cache_info_result, DirInfo::DirInfo::default()) + .expect("unreachable"); + + // `dir_path` is a slice into the threadlocal `bufs(.path_in_global_disk_cache)` buffer, + // which gets overwritten on the next auto-install resolution. `dirInfoUncached` stores + // its `path` argument directly as `DirInfo.abs_path` in the permanent `dir_cache`, so + // pass the interned copy from `DirEntry.dir` (always backed by `DirnameStore`) instead. + // SAFETY: ARENA — `dir_entries_option` is a slot in `rfs.entries` (BSSMap) and + // outlives the resolver. Hoist the `&'static [u8] dir` read out so no `&EntriesOption` + // temporary is live when the raw `*mut` is passed below (avoids a needless Unique + // retag that would pop the shared tag mid-argument-list under Stacked Borrows). + let dir_entries_dir = unsafe { &*dir_entries_option }.entries().dir; + self.dir_info_uncached( + dir_info_ptr, + dir_entries_dir, + // already `*mut EntriesOption` — pass raw, no intermediate `&mut` retag + dir_entries_option, + dir_cache_info_result, + cached_dir_entry_result.index, + // Packages in the global disk cache are top-level, we shouldn't try + // to check for a parent package.json + None, + allocators::NOT_FOUND, + open_dir, + Some(package_id), + )?; + // SAFETY: `dir_info_ptr` is the BSSMap slot just filled by `dir_info_uncached`. + Ok(Some(unsafe { DirInfoRef::from_raw(dir_info_ptr) })) + } + + fn enqueue_dependency_to_resolve( + &mut self, + // PORT NOTE: Zig `package_json_: ?*PackageJSON` (mutable). Carried as + // `NonNull` end-to-end so the mut-provenance from `intern_package_json` + // survives to the `package_manager_package_id` write below — taking + // `*const` and casting back to `*mut` would be UB under Stacked Borrows. + package_json_: Option>, + esm: &crate::package_json::Package<'_>, + behavior: Dependency::Behavior, + input_package_id_: &mut Install::PackageID, + version: Dependency::Version, + version_buf: &[u8], + ) -> DependencyToResolve { + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Enqueueing pending dependency \"{}@{}\"", + bstr::BStr::new(esm.name), + bstr::BStr::new(esm.version) + )); + } + + let input_package_id = *input_package_id_; + // PORT NOTE: see `manager_ptr` note in `load_node_modules` — split the + // `&mut self` borrow by holding the PackageManager via raw pointer. + let pm_ptr: *mut dyn AutoInstaller = self.get_package_manager(); + // SAFETY: PackageManager lives in a separate allocation; disjoint from `self`. + macro_rules! pm { + () => { + unsafe { &mut *pm_ptr } + }; + } + // we should never be trying to resolve a dependency that is already resolved + debug_assert!(pm!().lockfile_resolve(esm.name, &version).is_none()); + + // Add the containing package to the lockfile + + let is_main = + pm!().lockfile_packages_len() == 0 && input_package_id == Install::INVALID_PACKAGE_ID; + if is_main { + if let Some(mut package_json) = package_json_ { + // SAFETY: BACKREF — `package_json` is an interned arena slot + // (see `intern_package_json`); `NonNull` carries mut-provenance + // from `NonNull::from(&mut **last)` and no other live borrow + // exists here. + let package_json: &mut PackageJSON = unsafe { package_json.as_mut() }; + // PORT NOTE: Zig called `Package.fromPackageJSON(lockfile, pm, + // log, package_json, features)` then `setHasInstallScript` then + // `lockfile.appendPackage`. The `Package` type is bun_install- + // internal; the `AutoInstaller` impl performs all three steps. + let id = match pm!().lockfile_append_from_package_json( + package_json, + Install::Features { + dev_dependencies: true, + is_main: true, + dependencies: true, + optional_dependencies: true, + ..Default::default() + }, + ) { + Ok(id) => id, + Err(err) => return DependencyToResolve::Failure(err), + }; + package_json.package_manager_package_id = id; + } else { + // we're resolving an unknown package + // the unknown package is the root package + if let Err(err) = pm!().lockfile_append_root_stub() { + return DependencyToResolve::Failure(err); + } + } + } + + if self.opts.prefer_offline_install { + if let Some(package_id) = pm!().resolve_from_disk_cache(esm.name, &version) { + *input_package_id_ = package_id; + return DependencyToResolve::Resolution( + pm!().lockfile_package_resolution(package_id), + ); + } + } + + if input_package_id == Install::INVALID_PACKAGE_ID || input_package_id == 0 { + // All packages are enqueued to the root + // because we download all the npm package dependencies + match pm!().enqueue_dependency_to_root(esm.name, &version, version_buf, behavior) { + Install::EnqueueResult::Resolution { + package_id, + resolution, + } => { + *input_package_id_ = package_id; + return DependencyToResolve::Resolution(resolution); + } + Install::EnqueueResult::Pending(id) => { + let (cloned, string_buf) = esm.copy().expect("unreachable"); + + return DependencyToResolve::Pending(PendingResolution { + esm: cloned, + dependency: version, + root_dependency_id: id, + string_buf, + tag: PendingResolutionTag::Resolve, + ..Default::default() + }); + } + Install::EnqueueResult::NotFound => { + return DependencyToResolve::NotFound; + } + Install::EnqueueResult::Failure(err) => { + return DependencyToResolve::Failure(err); + } + } + } + + // PORT NOTE: 1:1 with Zig — `resolver.zig` ends this function with + // `bun.unreachablePanic("TODO: implement enqueueDependencyToResolve for + // non-root packages", .{})`. The non-root path is genuinely unimplemented + // in the Zig source; this is not a porting stub. + unreachable!("TODO: implement enqueueDependencyToResolve for non-root packages") + } + + fn handle_esm_resolution( + &mut self, + esm_resolution_: crate::package_json::Resolution, + abs_package_path: &[u8], + kind: ast::ImportKind, + package_json: &PackageJSON, + package_subpath: &[u8], + ) -> Option { + let mut esm_resolution = esm_resolution_; + use crate::package_json::Status; + if !((matches!( + esm_resolution.status, + Status::Inexact | Status::Exact | Status::ExactEndsWithStar + )) && !esm_resolution.path.is_empty() + && esm_resolution.path[0] == SEP) + { + return None; + } + + let abs_esm_path: &[u8] = match self.fs_ref().abs_buf_checked( + &[ + abs_package_path, + strings::without_leading_path_separator(&esm_resolution.path), + ], + bufs!(esm_absolute_package_path_joined), + ) { + Some(p) => p, + None => { + esm_resolution.status = Status::ModuleNotFound; + return None; + } + }; + + match esm_resolution.status { + Status::Exact | Status::ExactEndsWithStar => { + let resolved_dir_info = match self + .dir_info_cached(bun_paths::dirname(abs_esm_path).unwrap()) + .ok() + .flatten() + { + Some(d) => d, + None => { + esm_resolution.status = Status::ModuleNotFound; + return None; + } + }; + let entries = match resolved_dir_info.get_entries_ref(self.generation) { + Some(e) => e, + None => { + esm_resolution.status = Status::ModuleNotFound; + return None; + } + }; + let extension_order: options::ExtOrder = + if kind == ast::ImportKind::At || kind == ast::ImportKind::AtConditional { + self.extension_order + } else { + self.opts + .extension_order + .kind(kind, resolved_dir_info.is_inside_node_modules()) + }; + + let base = bun_paths::basename(abs_esm_path); + let entry_query = match entries.get(base) { + Some(q) => q, + None => { + let ends_with_star = esm_resolution.status == Status::ExactEndsWithStar; + esm_resolution.status = Status::ModuleNotFound; + + // Try to have a friendly error message if people forget the extension + if ends_with_star { + let buf = bufs!(load_as_file); + buf[..base.len()].copy_from_slice(base); + for ext in self.opts.ext_order_slice(extension_order).iter() { + let ext: &[u8] = ext; + let file_name = &mut buf[0..base.len() + ext.len()]; + file_name[base.len()..].copy_from_slice(ext); + if entries.get(&file_name[..]).is_some() { + if let Some(debug) = self.debug_logs.as_mut() { + let parts = [package_json.name.as_ref(), package_subpath]; + debug.add_note_fmt(format_args!( + "The import {} is missing the extension {}", + bstr::BStr::new(ResolvePath::join( + &parts, + bun_paths::Platform::AUTO + )), + bstr::BStr::new(ext) + )); + } + esm_resolution.status = Status::ModuleNotFoundMissingExtension; + let _ = ext; // PORT NOTE: Zig stored `missing_suffix = ext` here; unused after `return null`. + break; + } + } + } + return None; + } + }; + + if entry_query.entry().kind(self.rfs_ptr(), self.store_fd) + == Fs::file_system::EntryKind::Dir + { + let ends_with_star = esm_resolution.status == Status::ExactEndsWithStar; + esm_resolution.status = Status::UnsupportedDirectoryImport; + + // Try to have a friendly error message if people forget the "/index.js" suffix + if ends_with_star { + if let Ok(Some(dir_info_ref)) = self.dir_info_cached(abs_esm_path) { + if let Some(dir_entries) = dir_info_ref.get_entries_ref(self.generation) + { + let index = b"index"; + let buf = bufs!(load_as_file); + buf[..index.len()].copy_from_slice(index); + for ext in self.opts.ext_order_slice(extension_order).iter() { + let ext: &[u8] = ext; + let file_name = &mut buf[0..index.len() + ext.len()]; + file_name[index.len()..].copy_from_slice(ext); + let index_query = dir_entries.get(&file_name[..]); + if let Some(iq) = index_query { + if iq.entry().kind(self.rfs_ptr(), self.store_fd) + == Fs::file_system::EntryKind::File + { + if let Some(debug) = self.debug_logs.as_mut() { + let mut ms = + Vec::with_capacity(1 + file_name.len()); + ms.push(b'/'); + ms.extend_from_slice(&file_name[..]); + let parts = + [package_json.name.as_ref(), package_subpath]; + debug.add_note_fmt(format_args!( + "The import {} is missing the suffix {}", + bstr::BStr::new(ResolvePath::join( + &parts, + bun_paths::Platform::AUTO + )), + bstr::BStr::new(&ms) + )); + } + break; + } + } + } + } + } + } + + return None; + } + + let absolute_out_path: &[u8] = { + if entry_query.entry().abs_path.is_empty() { + // SAFETY: EntryStore-owned slot; resolver mutex held. RHS fully + // evaluated before LHS `&mut Entry` is materialized. + unsafe { &mut *entry_query.entry }.abs_path = PathString::init( + self.fs_ref() + .dirname_store + .append_slice(abs_esm_path) + .expect("unreachable"), + ); + } + entry_query.entry().abs_path.slice() + }; + let module_type = if let Some(pkg) = resolved_dir_info.package_json() { + pkg.module_type + } else { + options::ModuleType::Unknown + }; + + Some(MatchResult { + path_pair: PathPair { + primary: Path::init_with_namespace(absolute_out_path, b"file"), + secondary: None, + }, + dirname_fd: entries.fd, + file_fd: entry_query.entry().cache().fd, + dir_info: Some(resolved_dir_info), + diff_case: entry_query.diff_case, + is_node_module: true, + package_json: Some( + resolved_dir_info + .package_json() + .map(|p| std::ptr::from_ref(p)) + .unwrap_or(std::ptr::from_ref(package_json)), + ), + module_type, + ..Default::default() + }) + } + Status::Inexact => { + // If this was resolved against an expansion key ending in a "/" + // instead of a "*", we need to try CommonJS-style implicit + // extension and/or directory detection. + if let Some(res) = self.load_as_file_or_directory(abs_esm_path, kind) { + let mut res_copy = res; + res_copy.is_node_module = true; + res_copy.package_json = res_copy + .package_json + .or(Some(std::ptr::from_ref(package_json))); + return Some(res_copy); + } + esm_resolution.status = Status::ModuleNotFound; + None + } + _ => unreachable!(), + } + } + + pub fn resolve_without_remapping( + &mut self, + // PORT NOTE: `DirInfoRef` (not `&mut`) — forwards into `load_node_modules` + // which re-enters `dir_cache` and may re-derive the same DirInfo slot. + // Spec resolver.zig:2584 takes raw `*DirInfo`. + source_dir_info: DirInfoRef, + import_path: &[u8], + kind: ast::ImportKind, + global_cache: GlobalCache, + ) -> MatchResultUnion { + if is_package_path(import_path) { + self.load_node_modules(import_path, kind, source_dir_info, global_cache, false) + } else { + let Some(resolved) = self.fs_ref().abs_buf_checked( + &[source_dir_info.abs_path, import_path], + bufs!(resolve_without_remapping), + ) else { + return MatchResultUnion::NotFound; + }; + if let Some(result) = self.load_as_file_or_directory(resolved, kind) { + return MatchResultUnion::Success(result); + } + MatchResultUnion::NotFound + } + } + + pub fn parse_tsconfig( + &mut self, + file: &[u8], + dirname_fd: FD, + ) -> core::result::Result>, bun_core::Error> { + // Since tsconfig.json is cached permanently, in our DirEntries cache + // we must use the global allocator + let mut entry = self.caches.fs.read_file_with_allocator( + // SAFETY: process-global `FileSystem` singleton (see `fs()` PORT NOTE); narrow `&mut` + // for this call only — `self.caches` is a field of `self` (disjoint allocation). + unsafe { &mut *self.fs() }, + file, + dirname_fd, + false, + None, + None, + )?; + // PORT NOTE: reshaped for borrowck — `mem::take` the contents (leaving + // `Contents::Empty` behind) so `entry` stays whole for the close-guard. + let entry_contents = core::mem::take(&mut entry.contents); + let _close_guard = scopeguard::guard(entry, |mut e| { + let _ = e.close_fd(); + }); + + // The file name needs to be persistent because it can have errors + // and if those errors need to print the filename + // then it will be undefined memory if we parse another tsconfig.json later + let key_path = self + .fs_ref() + .dirname_store + .append_slice(file) + .expect("unreachable"); + + // `use_shared_buffer = false` above, so `entry_contents` is + // `Contents::Owned`/`Empty`. Zig reads with `bun.default_allocator` and + // never frees (tsconfig is interned into the permanent DirInfo cache). + // PORTING.md §Forbidden bars `mem::forget`/`from_raw_parts` to mint + // `&'static`; route through the process-lifetime arena instead. + // TODO(port): once `bun_ast::Source.contents` becomes `Cow<'static,[u8]>` + // / `Box<[u8]>`, the arena indirection here can be dropped. + let contents_static: &'static [u8] = intern_tsconfig_contents(entry_contents); + + let source = bun_ast::Source::init_path_string(key_path, contents_static); + let file_dir = source.path.source_dir(); + + // SAFETY: BACKREF — `self.log` (see `log()` PORT NOTE); disjoint from `self.caches`, + // narrow `&mut` for this call only. + let mut result = + match TSConfigJSON::parse(unsafe { &mut *self.log() }, &source, &mut self.caches.json)? + { + Some(r) => r, + None => return Ok(None), + }; + + if result.has_base_url() { + // this might leak + if !bun_paths::is_absolute(&result.base_url) { + // PORT NOTE: Zig interns into `dirname_store` and stores the + // arena slice; Rust `base_url: Box<[u8]>` owns its bytes, so + // copy `abs_buf`'s thread-local result directly instead of + // double-copying through the arena. + let abs = self + .fs_ref() + .abs_buf(&[file_dir, &result.base_url[..]], bufs!(tsconfig_base_url)); + result.base_url = Box::from(abs); + } + } + + if result.paths.count() > 0 + && (result.base_url_for_paths.is_empty() + || !bun_paths::is_absolute(&result.base_url_for_paths)) + { + // this might leak + let abs = self + .fs_ref() + .abs_buf(&[file_dir, &result.base_url[..]], bufs!(tsconfig_base_url)); + result.base_url_for_paths = Box::from(abs); + } + + // PORT NOTE: Zig `TSConfigJSON.parse` returns `*TSConfigJSON` (already + // heap). Return the `Box` so the caller (`dir_info_uncached`) takes + // ownership — intermediate configs in an extends-chain are dropped via + // `heap::take`, the final one is interned into the DirInfo cache. + Ok(Some(result)) + } + + pub fn bin_dirs(&self) -> &[&'static [u8]] { + if !BIN_FOLDERS_LOADED.load(core::sync::atomic::Ordering::Acquire) { + return &[]; + } + // SAFETY: BIN_FOLDERS protected by BIN_FOLDERS_LOCK at write sites; + // `BIN_FOLDERS_LOADED` (acquire) guarantees init. + unsafe { (*BIN_FOLDERS.get()).assume_init_ref().const_slice() } + } + + pub fn parse_package_json( + &mut self, + file: &[u8], + dirname_fd: FD, + package_id: Option, + ) -> core::result::Result>, bun_core::Error> { + use crate::package_json::{IncludeDependencies, IncludeScripts}; + // PORT NOTE: Zig threaded both as comptime params; `IncludeDependencies` is a + // const generic on `PackageJSON::parse`, `IncludeScripts` is runtime (it only + // gates one branch). + let include_scripts = if self.care_about_scripts { + IncludeScripts::IncludeScripts + } else { + IncludeScripts::IgnoreScripts + }; + let pkg = if ALLOW_DEPENDENCIES { + PackageJSON::parse::<{ IncludeDependencies::Local }>( + self, + file, + dirname_fd, + package_id, + include_scripts, + ) + } else { + PackageJSON::parse::<{ IncludeDependencies::None }>( + self, + file, + dirname_fd, + package_id, + include_scripts, + ) + }; + let Some(pkg) = pkg else { return Ok(None) }; + + // PORT NOTE: Zig `PackageJSON.new` = `bun.TrivialNew` (heap-allocate, + // never freed — DirInfo cache holds `&'static` refs). PORTING.md + // §Forbidden bars `Box::leak`; intern into the process-lifetime arena + // owned alongside the DirInfo singleton instead. + Ok(Some(intern_package_json(pkg))) + } + + fn dir_info_cached( + &mut self, + path: &[u8], + ) -> core::result::Result, bun_core::Error> { + self.dir_info_cached_maybe_log(true, path) + } + + pub fn read_dir_info( + &mut self, + path: &[u8], + ) -> core::result::Result, bun_core::Error> { + self.dir_info_cached_maybe_log(false, path) + } + + /// Like `readDirInfo`, but returns `null` instead of throwing an error. + pub fn read_dir_info_ignore_error(&mut self, path: &[u8]) -> Option { + self.dir_info_cached_maybe_log(false, path).ok().flatten() + } + + // PORT NOTE: Zig's `dirInfoCachedMaybeLog` takes `comptime enable_logging` + // and `comptime follow_symlinks`. `follow_symlinks` is `true` at every call + // site, so it's dropped here; `enable_logging` is a plain runtime parameter + // (it gates one cold error-formatting branch) so this large dir-walk function + // monomorphizes to a single copy instead of two faulted in at startup. + fn dir_info_cached_maybe_log( + &mut self, + enable_logging: bool, + raw_input_path: &[u8], + ) -> core::result::Result, bun_core::Error> { + // TODO(port): narrow error set + // `self.mutex` is `&'static Mutex` (Copy) — bind it first so the guard + // doesn't keep `self` borrowed across the body. + let _unlock = self.mutex.lock_guard(); + let mut input_path = raw_input_path; + + if is_dot_slash(input_path) || input_path == b"." { + input_path = self.fs_ref().top_level_dir; + } + + // A path longer than MAX_PATH_BYTES cannot name a real directory. + // Bailing here also prevents overflowing `dir_info_uncached_path` + // below when called with user-controlled absolute import paths. + if input_path.len() > MAX_PATH_BYTES { + return Ok(None); + } + + #[cfg(windows)] + { + let win32_normalized_dir_info_cache_buf = bufs!(win32_normalized_dir_info_cache); + input_path = self + .fs_ref() + .normalize_buf(win32_normalized_dir_info_cache_buf, input_path); + // kind of a patch on the fact normalizeBuf isn't 100% perfect what we want + if (input_path.len() == 2 && input_path[1] == b':') + || (input_path.len() == 3 && input_path[1] == b':' && input_path[2] == b'.') + { + debug_assert!(input_path.as_ptr() == win32_normalized_dir_info_cache_buf.as_ptr()); + win32_normalized_dir_info_cache_buf[2] = b'\\'; + input_path = &win32_normalized_dir_info_cache_buf[..3]; + } + + // Filter out \\hello\, a UNC server path but without a share. + // When there isn't a share name, such path is not considered to exist. + if input_path.starts_with(b"\\\\") { + let first_slash = strings::index_of_char(&input_path[2..], b'\\') + .ok_or(()) + .ok(); + if first_slash.is_none() { + return Ok(None); + } + let first_slash = first_slash.unwrap(); + if strings::index_of_char(&input_path[2 + first_slash as usize..], b'\\').is_none() + { + return Ok(None); + } + } + } + + ::bun_core::assertf!( + bun_paths::is_absolute(input_path), + "cannot resolve DirInfo for non-absolute path: {}", + bstr::BStr::new(input_path) + ); + + let path_without_trailing_slash = strings::without_trailing_slash_windows_path(input_path); + Self::assert_valid_cache_key(path_without_trailing_slash); + let top_result = self + .dir_cache_mut() + .get_or_put(path_without_trailing_slash)?; + if top_result.status != allocators::Status::Unknown { + return Ok(self + .dir_cache_mut() + .at_index(top_result.index) + .map(DirInfoRef::from_slot)); + } + + let dir_info_uncached_path_buf = bufs!(dir_info_uncached_path); + + let mut i: i32 = 1; + let input_path_len = input_path.len(); + dir_info_uncached_path_buf[..input_path_len].copy_from_slice(input_path); + // The slice spans one byte past the copied path so the NUL-splice/restore at + // `input_path_len` (queue index 0, processed last in the open-dir loop below) + // writes through `path`'s own provenance. `input_path_len + 1 ≤ MAX_PATH_BYTES + 1` + // (checked above) and `PathBuffer` always carries the +1 sentinel slot, so the + // safe slice is in-bounds and the threadlocal buffer outlives this fn. + let path: &mut [u8] = &mut dir_info_uncached_path_buf[..input_path_len + 1]; + + bufs!(dir_entry_paths_to_resolve)[0].write(DirEntryResolveQueueItem { + result: top_result, + unsafe_path: bun_ptr::RawSlice::new(&path[..input_path_len]), + safe_path: bun_ptr::RawSlice::EMPTY, + fd: FD::INVALID, + }); + let mut top = Dirname::dirname(&path[..input_path_len]); + + let mut top_parent = allocators::Result { + index: allocators::NOT_FOUND, + hash: 0, + status: allocators::Status::NotFound, + }; + #[cfg(windows)] + let root_path = strings::without_trailing_slash_windows_path( + ResolvePath::windows_filesystem_root(path), + ); + #[cfg(not(windows))] + // we cannot just use "/" + // we will write to the buffer past the ptr len so it must be a non-const buffer + let root_path = &path[0..1]; + Self::assert_valid_cache_key(root_path); + + // PORT NOTE: hold RealFS as a raw `*mut` so the entries-mutex/close-dirs + // scopeguards can capture it by Copy without keeping a `self.rfs_ptr()` + // borrow live across the loop body (which calls `&mut self` methods). + // SAFETY: ARENA — `self.fs` points at the process-global FileSystem singleton. + // Derive provenance from the raw `*mut FileSystem` field directly so later + // `unsafe { &mut *self.fs() }` calls (e.g. `dirname_store.append_*`) cannot pop `rfs`'s tag + // under Stacked Borrows (PORTING.md §Forbidden: aliased-&mut). + let rfs: *mut Fs::file_system::RealFS = self.rfs_ptr(); + macro_rules! rfs { + () => { + unsafe { &mut *rfs } + }; + } + + // SAFETY: `rfs` points at process-global storage; outlives this guard. + let _entries_unlock = rfs!().entries_mutex.lock_guard(); + + while top.len() > root_path.len() { + debug_assert!(top.as_ptr() == root_path.as_ptr()); + let result = self.dir_cache_mut().get_or_put(top)?; + + if result.status != allocators::Status::Unknown { + top_parent = result; + break; + } + // Path has more uncached components than our fixed queue can hold. + // This only happens for user-controlled absolute import paths with + // hundreds of short components — no real directory is this deep. + if usize::try_from(i).expect("int cast") >= bufs!(dir_entry_paths_to_resolve).len() { + return Ok(None); + } + bufs!(dir_entry_paths_to_resolve)[usize::try_from(i).expect("int cast")].write( + DirEntryResolveQueueItem { + unsafe_path: bun_ptr::RawSlice::new(top), + result, + safe_path: bun_ptr::RawSlice::EMPTY, + fd: FD::INVALID, + }, + ); + + if let Some(top_entry) = rfs!().entries.get(top) { + match top_entry { + Fs::file_system::real_fs::EntriesOption::Entries(entries) => { + // SAFETY: slot was written immediately above. + let slot = unsafe { + bufs!(dir_entry_paths_to_resolve)[usize::try_from(i).expect("int cast")] + .assume_init_mut() + }; + slot.safe_path = bun_ptr::RawSlice::new(entries.dir); + slot.fd = entries.fd; + } + Fs::file_system::real_fs::EntriesOption::Err(err) => { + debuglog!( + "Failed to load DirEntry {} {} - {}", + bstr::BStr::new(top), + bstr::BStr::new(err.original_err.name()), + bstr::BStr::new(err.canonical_error.name()) + ); + break; + } + } + } + i += 1; + top = Dirname::dirname(top); + } + + if top == root_path { + let result = self.dir_cache_mut().get_or_put(root_path)?; + if result.status != allocators::Status::Unknown { + top_parent = result; + } else { + bufs!(dir_entry_paths_to_resolve)[usize::try_from(i).expect("int cast")].write( + DirEntryResolveQueueItem { + unsafe_path: bun_ptr::RawSlice::new(root_path), + result, + safe_path: bun_ptr::RawSlice::EMPTY, + fd: FD::INVALID, + }, + ); + if let Some(top_entry) = rfs!().entries.get(top) { + match top_entry { + Fs::file_system::real_fs::EntriesOption::Entries(entries) => { + // SAFETY: slot was written immediately above. + let slot = unsafe { + bufs!(dir_entry_paths_to_resolve) + [usize::try_from(i).expect("int cast")] + .assume_init_mut() + }; + slot.safe_path = bun_ptr::RawSlice::new(entries.dir); + slot.fd = entries.fd; + } + Fs::file_system::real_fs::EntriesOption::Err(err) => { + debuglog!( + "Failed to load DirEntry {} {} - {}", + bstr::BStr::new(top), + bstr::BStr::new(err.original_err.name()), + bstr::BStr::new(err.canonical_error.name()) + ); + return Err(err.canonical_error); + } + } + } + + i += 1; + } + } + + let mut queue_slice_len = usize::try_from(i).expect("int cast"); + if cfg!(debug_assertions) { + debug_assert!(queue_slice_len > 0); + } + let open_dir_count = core::cell::Cell::new(0usize); + + // When this function halts, any item not processed means it's not found. + // PORT NOTE: capture only what the cleanup needs by-value (store_fd) / by-Cell + // (open_dir_count) so the guard doesn't pin `&mut self` across the loop + // body. `need_to_close_files()` is evaluated AT DROP TIME (matching + // Zig's `defer`), not snapshotted up-front — the loop body calls + // `Fs.FileSystem.setMaxFd()` which can flip `needToCloseFiles()` + // mid-walk. Reach the RealFS via the `&'static` singleton accessor + // instead of capturing a raw `*mut RealFS` (the read is `&self`-only). + let close_dirs_store_fd = self.store_fd; + scopeguard::defer! { + let n = open_dir_count.get(); + if n > 0 && (!close_dirs_store_fd || Fs::FileSystem::get().fs.need_to_close_files()) { + let open_dirs = &bufs!(open_dirs)[0..n]; + for open_dir in open_dirs { + open_dir.close(); + } + } + } + + // We want to walk in a straight line from the topmost directory to the desired directory + // For each directory we visit, we get the entries, but not traverse into child directories + // (unless those child directories are in the queue) + // We go top-down instead of bottom-up to increase odds of reusing previously open file handles + // "/home/jarred/Code/node_modules/react/cjs/react.development.js" + // ^ + // If we start there, we will traverse all of /home/jarred, including e.g. /home/jarred/Downloads + // which is completely irrelevant. + + // After much experimentation... + // - fts_open is not the fastest way to read directories. fts actually just uses readdir!! + // - remember + let mut _safe_path: Option<&'static [u8]> = None; + + // Start at the top. + while queue_slice_len > 0 { + // SAFETY: every slot in `0..queue_slice_len` was `.write()`-initialised above. + let mut queue_top = + unsafe { bufs!(dir_entry_paths_to_resolve)[queue_slice_len - 1].assume_init_ref() } + .clone(); + // `unsafe_path` was set to a slice of the threadlocal + // `dir_info_uncached_path` buffer earlier in this fn; valid for the + // remainder of the fn body. `safe_path` is either empty or a + // dirname_store-backed `&'static [u8]`. Copy the `RawSlice` handles + // out so the re-borrows below don't hold `queue_top` borrowed. + let (qt_unsafe_path, qt_safe_path) = (queue_top.unsafe_path, queue_top.safe_path); + let queue_top_unsafe_path: &[u8] = qt_unsafe_path.slice(); + let queue_top_safe_path: &[u8] = qt_safe_path.slice(); + // defer top_parent = queue_top.result — done at end of loop body + queue_slice_len -= 1; + + let open_dir: FD = if queue_top.fd.is_valid() { + queue_top.fd + } else { + 'open_dir: { + // This saves us N copies of .toPosixPath + // which was likely the perf gain from resolving directories relative to the parent directory, anyway. + // `queue_top_unsafe_path.len()` is ≤ `input_path_len` < `path.len()` for + // every queue item, so this indexes in-bounds (the +1 sentinel slot for + // queue index 0 — see the `path` construction above). + let nul_at = queue_top_unsafe_path.len(); + let prev_char = path[nul_at]; + path[nul_at] = 0; + let sentinel = bun_core::ZStr::from_buf(path, nul_at); + + #[cfg(unix)] + let open_req: core::result::Result = { + // TODO(port): std.fs.openDirAbsoluteZ — using bun_sys equivalent + bun_sys::open_dir_absolute_z( + sentinel, + bun_sys::OpenDirOptions { + no_follow: false, + iterate: true, + }, + ) + .map_err(Into::into) + }; + #[cfg(windows)] + let open_req: core::result::Result = { + bun_sys::open_dir_at_windows_a( + FD::INVALID, + sentinel.as_bytes(), + bun_sys::WindowsOpenDirOptions { + iterable: true, + no_follow: false, + read_only: true, + ..Default::default() + }, + ) + .map_err(Into::into) + }; + + // bun.fs.debug("open({s})", .{sentinel}) — TODO(port): scoped log + // Restore the byte we NUL-terminated above (Zig: `defer path[len] = prev_char`). + // No early-return path exists between the write and here, so a guard is unnecessary. + path[nul_at] = prev_char; + + match open_req { + Ok(fd) => break 'open_dir fd, + Err(err) => { + // Ignore "ENOTDIR" here so that calling "ReadDirectory" on a file behaves + // as if there is nothing there at all instead of causing an error due to + // the directory actually being a file. This is a workaround for situations + // where people try to import from a path containing a file as a parent + // directory. The "pnpm" package manager generates a faulty "NODE_PATH" + // list which contains such paths and treating them as missing means we just + // ignore them during path resolution. + if err == bun_core::err!("ENOTDIR") + || err == bun_core::err!("IsDir") + || err == bun_core::err!("NotDir") + { + return Ok(None); + } + let cached_dir_entry_result = rfs!() + .entries + .get_or_put(queue_top_unsafe_path) + .expect("unreachable"); + // If we don't properly cache not found, then we repeatedly attempt to open the same directories, + // which causes a perf trace that looks like this stupidity; + // + // openat(dfd: CWD, filename: "node_modules/react", flags: RDONLY|DIRECTORY) = -1 ENOENT (No such file or directory) + // ... + self.dir_cache_mut().mark_not_found(queue_top.result); + rfs!().entries.mark_not_found(cached_dir_entry_result); + if !(err == bun_core::err!("ENOENT") + || err == bun_core::err!("FileNotFound")) + { + if enable_logging { + let pretty = queue_top_unsafe_path; + let _ = self.log_mut().add_error_fmt( + None, + bun_ast::Loc::default(), + format_args!( + "Cannot read directory \"{}\": {}", + bstr::BStr::new(pretty), + bstr::BStr::new(err.name()) + ), + ); + } + } + + return Ok(None); + } + } + } + }; + + if !queue_top.fd.is_valid() { + Fs::FileSystem::set_max_fd(open_dir.native()); + // these objects mostly just wrap the file descriptor, so it's fine to keep it. + bufs!(open_dirs)[open_dir_count.get()] = open_dir; + open_dir_count.set(open_dir_count.get() + 1); + } + + let dir_path: &'static [u8] = if !queue_top_safe_path.is_empty() { + // SAFETY: non-empty `safe_path` is always a dirname_store-backed + // `&'static [u8]` (set from `entries.dir` above); widen the + // `RawSlice`-tied borrow back to its true `'static` lifetime. + unsafe { bun_ptr::detach_lifetime(queue_top_safe_path) } + } else { + // ensure trailing slash + if _safe_path.is_none() { + // Now that we've opened the topmost directory successfully, it's reasonable to store the slice. + // `path` spans `input_path_len + 1` for the NUL-splice above; the + // logical input is `path[..input_path_len]` (Zig resolver.zig:2750). + let input = &path[..input_path_len]; + if input[input.len() - 1] != SEP { + let parts: [&[u8]; 2] = [input, SEP_STR.as_bytes()]; + _safe_path = Some(self.fs_ref().dirname_store.append_parts(&parts)?); + } else { + _safe_path = Some(self.fs_ref().dirname_store.append_slice(input)?); + } + } + + let safe_path = _safe_path.unwrap(); + + // Spec resolver.zig:2965 calls `std.mem.indexOf` (returns 0 for an + // empty needle), not `bun.strings.indexOf` (returns null for an + // empty needle). On Windows `queue_top_unsafe_path` is empty when + // `windows_filesystem_root` cannot classify the input — e.g. + // `import(":://x")` is "absolute" per std but has no drive root, + // so `root_path` is `path[0..0]`. Match the spec so the resolver + // caches a not-found instead of panicking. + let dir_path_i = if queue_top_unsafe_path.is_empty() { + 0 + } else { + strings::index_of(safe_path, queue_top_unsafe_path).expect("unreachable") + }; + let mut end = dir_path_i + queue_top_unsafe_path.len(); + + // Directories must always end in a trailing slash or else various bugs can occur. + // This covers "what happens when the trailing" + end += usize::from( + safe_path.len() > end + && end > 0 + && safe_path[end - 1] != SEP + && safe_path[end] == SEP, + ); + &safe_path[dir_path_i..end] + }; + + let mut cached_dir_entry_result = + rfs!().entries.get_or_put(dir_path).expect("unreachable"); + + let mut dir_entries_option: *mut Fs::file_system::real_fs::EntriesOption = + core::ptr::null_mut(); + let mut needs_iter = true; + let mut in_place: Option<*mut Fs::file_system::DirEntry> = None; + + if let Some(cached_entry) = rfs!().entries.at_index(cached_dir_entry_result.index) { + if let Fs::file_system::real_fs::EntriesOption::Entries(entries) = cached_entry { + if entries.generation >= self.generation { + dir_entries_option = cached_entry; + needs_iter = false; + } else { + in_place = Some(std::ptr::from_mut(*entries)); + } + } + } + + if needs_iter { + // SAFETY: (block-wide) `in_place`/`dir_entries_ptr`/`dir_entries_option` point to + // slots in `rfs.entries` (BSSMap singleton) or a fresh leaked Box; both outlive this + // fn and are accessed under `rfs.entries_mutex` (see LIFETIMES.tsv). + let mut new_entry = Fs::file_system::DirEntry::init( + if let Some(existing) = in_place { + // SAFETY: see block-wide note above. + unsafe { &*existing }.dir + } else { + Fs::file_system::DirnameStore::instance() + .append_slice(dir_path) + .expect("unreachable") + }, + self.generation, + ); + + // Pre-size `data` so the per-entry inserts below skip the + // 1→2→4→…→N hashbrown rehash cascade from an empty table. 64 + // covers a typical node_modules package dir; larger dirs + // still rehash from there (cheap relative to starting at 0). + new_entry.data.reserve(64); + + let mut dir_iterator = bun_sys::iterate_dir(open_dir); + // PORT NOTE: Zig `while (dir_iterator.next().unwrap()) |entry|` — + // `.unwrap()` was on the inner `Maybe(?Entry)`; the Rust `WrappedIterator::next` + // is already flattened to `Result>`, so the `.unwrap()` + // moved to `?`-style break-on-error. + // Hoist the `FilenameStore` singleton resolve out of the per-entry loop + // (see `DirEntry::add_entry` doc-comment) and reuse the appender state. + let mut filename_store = FilenameStoreAppender::new(); + loop { + let _value = match dir_iterator.next() { + Ok(Some(v)) => v, + Ok(None) => break, + Err(_) => break, + }; + new_entry + .add_entry_with_store( + // SAFETY: see block-wide note above. + in_place.map(|existing| unsafe { &mut (*existing).data }), + &_value, + &mut filename_store, + (), + ) + .expect("unreachable"); + } + if let Some(existing) = in_place { + // SAFETY: see block-wide note above. + // PORT NOTE: Zig `clear_and_free`; bun_collections::StringHashMap exposes `clear`. + unsafe { &mut *existing }.data.clear(); + } + new_entry.fd = if self.store_fd { open_dir } else { FD::INVALID }; + // PORT NOTE: Zig `entries_ptr = in_place orelse allocator.create(DirEntry)` then + // `entries_ptr.* = new_entry` (no drop glue). `DirEntry.data` is a `HashMap` + // (`NonNull` inside), so a zeroed slot is UB and `*ptr = new_entry` would drop it. + // Box `new_entry` directly for the fresh case; assign-into only for `in_place`. + let dir_entries_ptr = match in_place { + Some(p) => { + // SAFETY: dir_entries_ptr is a live BSSMap slot (`in_place`). + unsafe { *p = new_entry }; + p + } + None => bun_core::heap::into_raw(Box::new(new_entry)), + }; + dir_entries_option = rfs!() + .entries + // SAFETY: see block-wide note above. + .put( + &mut cached_dir_entry_result, + Fs::file_system::real_fs::EntriesOption::Entries(unsafe { + &mut *dir_entries_ptr + }), + )?; + // bun.fs.debug("readdir({f}, {s}) = {d}", ...) — TODO(port): scoped log + } + + // We must initialize it as empty so that the result index is correct. + // This is important so that browser_scope has a valid index. + // PORT NOTE: erase the `&mut DirInfo` borrow to `*mut` immediately so + // `self.dir_cache` (and `*self`) are reborrowable for the call below. + // SAFETY: ARENA — `dir_cache()` singleton (see PORT NOTE). Stacked Borrows: bind + // ONE `&mut HashMap` and derive BOTH slot pointers from it so they share a parent + // tag — a second `&mut *self.dir_cache()` Unique retag of the whole `BSSMapInner` + // (whose `backing_buf` is inline) would pop `dir_info_ptr`'s tag before + // `dir_info_uncached` writes through it. Spec resolver.zig:3022/3030 routes both + // through the single raw `r.dir_cache: *HashMap` with no intermediate retag. + // NOTE: erasing `&mut V` to `*mut V` does NOT, by itself, survive a sibling Unique + // retag of the parent allocation; the shared `dc` parent is what keeps both live. + let dc = self.dir_cache_mut(); + let dir_info_ptr: *mut DirInfo::DirInfo = + dc.put(&mut queue_top.result, DirInfo::DirInfo::default())?; + let parent_dir_ptr = dc.at_index(top_parent.index).map(DirInfoRef::from_slot); + + self.dir_info_uncached( + dir_info_ptr, + dir_path, + // SAFETY: ARENA — `dir_entries_option` is a slot in `rfs.entries` (BSSMap) and outlives the resolver. + dir_entries_option, + queue_top.result, + cached_dir_entry_result.index, + parent_dir_ptr, + top_parent.index, + open_dir, + None, + )?; + + top_parent = queue_top.result; + + if queue_slice_len == 0 { + // SAFETY: `dir_info_ptr` is the BSSMap slot just filled by `dir_info_uncached`. + return Ok(Some(unsafe { DirInfoRef::from_raw(dir_info_ptr) })); + + // Is the directory we're searching for actually a file? + } else if queue_slice_len == 1 { + // const next_in_queue = queue_slice[0]; + // const next_basename = std.fs.path.basename(next_in_queue.unsafe_path); + // if (dir_info_ptr.getEntries(r.generation)) |entries| { + // if (entries.get(next_basename) != null) { + // return null; + // } + // } + } + } + + unreachable!() + } + + // This closely follows the behavior of "tryLoadModuleUsingPaths()" in the + // official TypeScript compiler + pub fn match_tsconfig_paths( + &mut self, + tsconfig: &TSConfigJSON, + path: &[u8], + kind: ast::ImportKind, + ) -> Option { + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Matching \"{}\" against \"paths\" in \"{}\"", + bstr::BStr::new(path), + bstr::BStr::new(&tsconfig.abs_path) + )); + } + + let mut abs_base_url: &[u8] = &tsconfig.base_url_for_paths; + + // The explicit base URL should take precedence over the implicit base URL + // if present. This matters when a tsconfig.json file overrides "baseUrl" + // from another extended tsconfig.json file but doesn't override "paths". + if tsconfig.has_base_url() { + abs_base_url = &tsconfig.base_url; + } + + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Using \"{}\" as \"baseURL\"", + bstr::BStr::new(abs_base_url) + )); + } + + // Check for exact matches first + { + // PORT NOTE: ArrayHashMap has no `&self` (key,value) iterator; zip the + // parallel `keys()`/`values()` slices (insertion order). + for (key, value) in tsconfig + .paths + .keys() + .iter() + .zip(tsconfig.paths.values().iter()) + { + if strings::eql_long(key, path, true) { + for original_path in value.iter() { + let mut absolute_original_path: &[u8] = original_path; + + if !bun_paths::is_absolute(absolute_original_path) { + let parts: [&[u8]; 2] = [abs_base_url, original_path.as_ref()]; + absolute_original_path = + self.fs_ref().abs_buf(&parts, bufs!(tsconfig_path_abs)); + } + + if let Some(res) = + self.load_as_file_or_directory(absolute_original_path, kind) + { + return Some(res); + } + } + } + } + } + + struct TSConfigMatch<'b> { + prefix: &'b [u8], + suffix: &'b [u8], + original_paths: &'b [Box<[u8]>], + } + + let mut longest_match: Option = None; + let mut longest_match_prefix_length: i32 = -1; + let mut longest_match_suffix_length: i32 = -1; + + for (key, original_paths) in tsconfig + .paths + .keys() + .iter() + .zip(tsconfig.paths.values().iter()) + { + if let Some(star) = strings::index_of_char(key, b'*') { + let star = star as usize; + let prefix: &[u8] = if star == 0 { b"" } else { &key[0..star] }; + let suffix: &[u8] = if star == key.len() - 1 { + b"" + } else { + &key[star + 1..] + }; + + // Find the match with the longest prefix. If two matches have the same + // prefix length, pick the one with the longest suffix. This second edge + // case isn't handled by the TypeScript compiler, but we handle it + // because we want the output to always be deterministic + let plen = i32::try_from(prefix.len()).expect("int cast"); + let slen = i32::try_from(suffix.len()).expect("int cast"); + if path.starts_with(prefix) + && path.ends_with(suffix) + && (plen > longest_match_prefix_length + || (plen == longest_match_prefix_length + && slen > longest_match_suffix_length)) + { + longest_match_prefix_length = plen; + longest_match_suffix_length = slen; + longest_match = Some(TSConfigMatch { + prefix, + suffix, + original_paths, + }); + } + } + } + + // If there is at least one match, only consider the one with the longest + // prefix. This matches the behavior of the TypeScript compiler. + if longest_match_prefix_length != -1 { + let longest_match = longest_match.unwrap(); + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Found a fuzzy match for \"{}*{}\" in \"paths\"", + bstr::BStr::new(longest_match.prefix), + bstr::BStr::new(longest_match.suffix) + )); + } + + for original_path in longest_match.original_paths.iter() { + // Swap out the "*" in the original path for whatever the "*" matched + let matched_text = + &path[longest_match.prefix.len()..path.len() - longest_match.suffix.len()]; + + let total_length: Option = strings::index_of_char(original_path, b'*'); + let prefix_end = total_length + .map(|v| v as usize) + .unwrap_or(original_path.len()); + let prefix_parts: [&[u8]; 2] = [abs_base_url, &original_path[0..prefix_end]]; + + // Concatenate the matched text with the suffix from the wildcard path + let matched_text_with_suffix = bufs!(tsconfig_match_full_buf3); + let mut matched_text_with_suffix_len: usize = 0; + if total_length.is_some() { + let suffix = strings::trim_left(&original_path[prefix_end..], b"*"); + matched_text_with_suffix_len = matched_text.len() + suffix.len(); + if matched_text_with_suffix_len > matched_text_with_suffix.len() { + continue; + } + ::bun_core::concat_into(matched_text_with_suffix, &[matched_text, suffix]); + } + + // 1. Normalize the base path + // so that "/Users/foo/project/", "../components/*" => "/Users/foo/components/"" + let Some(prefix) = self + .fs_ref() + .abs_buf_checked(&prefix_parts, bufs!(tsconfig_match_full_buf2)) + else { + continue; + }; + + // 2. Join the new base path with the matched result + // so that "/Users/foo/components/", "/foo/bar" => /Users/foo/components/foo/bar + let parts: [&[u8]; 3] = [ + prefix, + if matched_text_with_suffix_len > 0 { + strings::trim_left( + &matched_text_with_suffix[0..matched_text_with_suffix_len], + b"/", + ) + } else { + b"" + }, + strings::trim_left(longest_match.suffix, b"/"), + ]; + let Some(absolute_original_path) = self + .fs_ref() + .abs_buf_checked(&parts, bufs!(tsconfig_match_full_buf)) + else { + continue; + }; + + if let Some(res) = self.load_as_file_or_directory(absolute_original_path, kind) { + return Some(res); + } + } + } + + None + } + + pub fn load_package_imports( + &mut self, + import_path: &[u8], + // PORT NOTE: `DirInfoRef` (not `&mut`) — `handle_esm_resolution` re-enters + // `dir_cache` via `dir_info_cached(dirname(abs_esm_path))`; for any + // imports-map entry resolving to `./` that dirname equals + // `dir_info.abs_path`, re-deriving `&mut` to the SAME slot while a + // `&mut` param's FnEntry protector is live is aliased-&mut UB. + // Spec resolver.zig:3182 takes raw `*DirInfo`. + dir_info: DirInfoRef, + kind: ast::ImportKind, + global_cache: GlobalCache, + ) -> MatchResultUnion { + let package_json = dir_info.package_json().unwrap(); + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Looking for {} in \"imports\" map in {}", + bstr::BStr::new(import_path), + bstr::BStr::new(package_json.source.path.text) + )); + debug.increase_indent(); + // defer debug.decreaseIndent() — TODO(port): missing matching decrease in Zig too + } + let imports_map = package_json.imports.as_ref().unwrap(); + + if import_path.len() == 1 || import_path.starts_with(b"#/") { + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "The path \"{}\" must not equal \"#\" and must not start with \"#/\"", + bstr::BStr::new(import_path) + )); + } + return MatchResultUnion::NotFound; + } + let mut module_type = options::ModuleType::Unknown; + + // PORT NOTE: reshaped for borrowck — Zig kept a raw `*DebugLogs` inside + // `ESModule` across the subsequent `&mut self` calls. In Rust that is + // aliased-&mut UB, so the `ESModule` is constructed as a temporary whose + // borrow of `self.debug_logs` ends as soon as `resolve_imports` returns. + let esm_resolution = ESModule { + conditions: match kind { + ast::ImportKind::Require | ast::ImportKind::RequireResolve => { + self.opts.conditions.require.clone().expect("oom") + } + _ => self.opts.conditions.import.clone().expect("oom"), + }, + debug_logs: self.debug_logs.as_mut(), + module_type: &mut module_type, + } + .resolve_imports(import_path, &imports_map.root); + let _ = module_type; + + if esm_resolution.status == crate::package_json::Status::PackageResolve { + // https://github.com/oven-sh/bun/issues/4972 + // Resolve a subpath import to a Bun or Node.js builtin + // + // Code example: + // + // import { readFileSync } from '#fs'; + // + // package.json: + // + // "imports": { + // "#fs": "node:fs" + // } + // + if self.opts.mark_builtins_as_external || self.opts.target.is_bun() { + if let Some(alias) = HardcodedAlias::get( + &esm_resolution.path, + self.opts.target, + HardcodedAliasCfg::default(), + ) { + return MatchResultUnion::Success(MatchResult { + path_pair: PathPair { + primary: Fs::Path::init(alias.path.as_bytes()), + secondary: None, + }, + is_external: true, + ..Default::default() + }); + } + } + + return self.load_node_modules( + &esm_resolution.path, + kind, + dir_info, + global_cache, + true, + ); + } + + if let Some(result) = self.handle_esm_resolution( + esm_resolution, + package_json.source.path.name.dir, + kind, + package_json, + b"", + ) { + return MatchResultUnion::Success(result); + } + + MatchResultUnion::NotFound + } + + pub fn check_browser_map( + &mut self, + dir_info: &DirInfo::DirInfo, + input_path_: &[u8], + ) -> Option<&'static [u8]> { + let package_json = dir_info.package_json()?; + let browser_map = &package_json.browser_map; + + if browser_map.count() == 0 { + return None; + } + + let mut input_path = input_path_; + + if KIND == BrowserMapPathKind::AbsolutePath { + let abs_path = dir_info.abs_path; + // Turn absolute paths into paths relative to the "browser" map location + if !input_path.starts_with(abs_path) { + return None; + } + + input_path = &input_path[abs_path.len()..]; + } + + if input_path.is_empty() + || (input_path.len() == 1 && (input_path[0] == b'.' || input_path[0] == SEP)) + { + // No bundler supports remapping ".", so we don't either + return None; + } + + // Normalize the path so we can compare against it without getting confused by "./" + let cleaned = self + .fs_ref() + .normalize_buf(bufs!(check_browser_map), input_path); + + if cleaned.len() == 1 && cleaned[0] == b'.' { + // No bundler supports remapping ".", so we don't either + return None; + } + + let mut checker = BrowserMapPath { + remapped: b"", + cleaned, + input_path, + extension_order: self.opts.ext_order_slice(self.extension_order), + map: &package_json.browser_map, + }; + + if checker.check_path(input_path) { + return Some(checker.remapped); + } + + // First try the import path as a package path + if is_package_path(checker.input_path) { + let abs_to_rel = bufs!(abs_to_rel); + match KIND { + BrowserMapPathKind::AbsolutePath => { + abs_to_rel[0..2].copy_from_slice(b"./"); + abs_to_rel[2..2 + checker.input_path.len()].copy_from_slice(checker.input_path); + if checker.check_path(&abs_to_rel[0..checker.input_path.len() + 2]) { + return Some(checker.remapped); + } + } + BrowserMapPathKind::PackagePath => { + // Browserify allows a browser map entry of "./pkg" to override a package + // path of "require('pkg')". This is weird, and arguably a bug. But we + // replicate this bug for compatibility. However, Browserify only allows + // this within the same package. It does not allow such an entry in a + // parent package to override this in a child package. So this behavior + // is disallowed if there is a "node_modules" folder in between the child + // package and the parent package. + let is_in_same_package = match dir_info.get_parent() { + Some(parent) => !parent.is_node_modules(), + None => true, + }; + + if is_in_same_package { + abs_to_rel[0..2].copy_from_slice(b"./"); + abs_to_rel[2..2 + checker.input_path.len()] + .copy_from_slice(checker.input_path); + + if checker.check_path(&abs_to_rel[0..checker.input_path.len() + 2]) { + return Some(checker.remapped); + } + } + } + } + } + + None + } + + pub fn load_from_main_field( + &mut self, + path: &[u8], + // PORT NOTE: `DirInfoRef` (not `&mut`) — `get_enclosing_browser_scope()` + // may return `dir_info` itself (resolver.zig:4161 self-browser-scope), + // which would alias a live `&mut`. Spec uses raw `*DirInfo`. + dir_info: DirInfoRef, + _field_rel_path: &[u8], + field: &[u8], + extension_order: options::ExtOrder, + ) -> Option { + let mut field_rel_path = _field_rel_path; + // Is this a directory? + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Found main field \"{}\" with path \"{}\"", + bstr::BStr::new(field), + bstr::BStr::new(field_rel_path) + )); + debug.increase_indent(); + } + + // defer { debug.decreaseIndent() } — handled at returns + macro_rules! dec_ret { + ($e:expr) => {{ + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return $e; + }}; + } + + if self.care_about_browser_field { + // Potentially remap using the "browser" field + if let Some(browser_scope) = dir_info.get_enclosing_browser_scope() { + if let Some(browser_json) = browser_scope.package_json() { + if let Some(remap) = self + .check_browser_map::<{ BrowserMapPathKind::AbsolutePath }>( + &browser_scope, + field_rel_path, + ) + { + // Is the path disabled? + if remap.is_empty() { + let paths = [path, field_rel_path]; + let new_path = self.fs_ref().abs_alloc(&paths).expect("unreachable"); + let mut _path = Path::init(new_path); + _path.is_disabled = true; + dec_ret!(Some(MatchResult { + path_pair: PathPair { + primary: _path, + secondary: None + }, + package_json: Some(std::ptr::from_ref(browser_json)), + ..Default::default() + })); + } + + field_rel_path = remap; + } + } + } + } + let _paths = [path, field_rel_path]; + let field_abs_path = self.fs_ref().abs_buf(&_paths, bufs!(field_abs_path)); + + // Is this a file? + if let Some(result) = self.load_as_file(field_abs_path, extension_order) { + if let Some(package_json) = dir_info.package_json() { + dec_ret!(Some(MatchResult { + path_pair: PathPair { + primary: Fs::Path::init(result.path), + secondary: None + }, + package_json: Some(std::ptr::from_ref(package_json)), + dirname_fd: result.dirname_fd, + ..Default::default() + })); + } + + dec_ret!(Some(MatchResult { + path_pair: PathPair { + primary: Fs::Path::init(result.path), + secondary: None + }, + dirname_fd: result.dirname_fd, + diff_case: result.diff_case, + ..Default::default() + })); + } + + // Is it a directory with an index? + let Some(field_dir_info) = self.dir_info_cached(field_abs_path).ok().flatten() else { + dec_ret!(None); + }; + + let r = self.load_as_index_with_browser_remapping( + field_dir_info, + field_abs_path, + extension_order, + ); + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + r + } + + // nodeModulePathsForJS / Resolver__propForRequireMainPaths: see src/jsc/resolver_jsc.zig + // (no Zig callers; exported to C++ only) + + // PORT NOTE: `dir_info` is a `DirInfoRef` (matching spec `*DirInfo`) so + // `load_index_with_extension` may re-borrow without aliasing the caller's `&mut`. + pub fn load_as_index( + &mut self, + dir_info: DirInfoRef, + extension_order: options::ExtOrder, + ) -> Option { + // Try the "index" file with extensions + // PORT NOTE: index by `0..len` so each iteration takes a fresh short + // borrow of `self.opts` that ends before `&mut self` is taken by + // `load_index_with_extension` (matches `extra_cjs_extensions` loop below). + let n = self.opts.ext_order_slice(extension_order).len(); + for i in 0..n { + // BACKREF: `RawSlice` detaches the `&self.opts` borrow so the loop + // body can take `&mut self`. Backing `Box<[u8]>` is owned by + // `self.opts` and never mutated while the resolver runs. + let ext = bun_ptr::RawSlice::new(&*self.opts.ext_order_slice(extension_order)[i]); + if let Some(result) = self.load_index_with_extension(dir_info, &ext) { + return Some(result); + } + } + // PORT NOTE: index by `0..len` so each iteration takes a fresh short + // borrow of `self.opts` that ends before `&mut self` is taken by + // `load_index_with_extension` (avoids the forbidden lifetime-extension cast). + let n = self.opts.extra_cjs_extensions.len(); + for i in 0..n { + // BACKREF: see `RawSlice` note above — backing `Box<[u8]>` in + // `extra_cjs_extensions` is heap-stable for the resolver's life. + let ext = bun_ptr::RawSlice::new(&*self.opts.extra_cjs_extensions[i]); + if let Some(result) = self.load_index_with_extension(dir_info, &ext) { + return Some(result); + } + } + + None + } + + fn load_index_with_extension( + &mut self, + dir_info: DirInfoRef, + ext: &[u8], + ) -> Option { + // SAFETY: PORT (Stacked Borrows) — derive `rfs` from the raw `*mut FileSystem` + // field so the `&mut *self.fs()` calls below (`abs_buf`/`dirname_store.append_slice`) + // don't pop its provenance. Re-borrow `&mut *rfs` at the single use site. + let rfs: *mut Fs::file_system::RealFS = self.rfs_ptr(); + + let ext_buf = bufs!(extension_path); + + let base = &mut ext_buf[0..b"index".len() + ext.len()]; + base[0..b"index".len()].copy_from_slice(b"index"); + base[b"index".len()..].copy_from_slice(ext); + + if let Some(entries) = dir_info.get_entries_ref(self.generation) { + if let Some(lookup) = entries.get(&base[..]) { + if lookup.entry().kind(rfs, self.store_fd) == Fs::file_system::EntryKind::File { + let out_buf: &[u8] = { + if lookup.entry().abs_path.is_empty() { + let parts = [dir_info.abs_path, &base[..]]; + let out_buf_ = self.fs_ref().abs_buf(&parts, bufs!(index)); + // SAFETY: EntryStore-owned slot; resolver mutex held. RHS fully + // evaluated before LHS `&mut Entry` is materialized. + unsafe { &mut *lookup.entry }.abs_path = PathString::init( + self.fs_ref() + .dirname_store + .append_slice(out_buf_) + .expect("unreachable"), + ); + } + lookup.entry().abs_path.slice() + }; + + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Found file: \"{}\"", + bstr::BStr::new(out_buf) + )); + } + + if let Some(package_json) = dir_info.package_json() { + return Some(MatchResult { + path_pair: PathPair { + primary: Path::init(out_buf), + secondary: None, + }, + diff_case: lookup.diff_case, + package_json: Some(std::ptr::from_ref(package_json)), + dirname_fd: dir_info.get_file_descriptor(), + ..Default::default() + }); + } + + return Some(MatchResult { + path_pair: PathPair { + primary: Path::init(out_buf), + secondary: None, + }, + diff_case: lookup.diff_case, + dirname_fd: dir_info.get_file_descriptor(), + ..Default::default() + }); + } + } + } + + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Failed to find file: \"{}/{}\"", + bstr::BStr::new(dir_info.abs_path), + bstr::BStr::new(&base[..]) + )); + } + + None + } + + pub fn load_as_index_with_browser_remapping( + &mut self, + // PORT NOTE: `DirInfoRef` (not `&mut`) — `get_enclosing_browser_scope()` + // may return `dir_info` itself (resolver.zig:4161 self-browser-scope), + // which would alias a live `&mut`. Spec uses raw `*DirInfo`. + dir_info: DirInfoRef, + path_: &[u8], + extension_order: options::ExtOrder, + ) -> Option { + // In order for our path handling logic to be correct, it must end with a trailing slash. + let mut path = path_; + // Hoisted to fn-body scope so the immutable reborrow taken below can outlive + // the `if` block without lifetime erasure; the field is not touched again in + // this fn (only `remap_path` is, via a separate `bufs!` raw-ptr projection). + let path_buf = bufs!(remap_path_trailing_slash); + if !strings::ends_with_char(path_, SEP) { + path_buf[..path.len()].copy_from_slice(path); + path_buf[path.len()] = SEP; + path_buf[path.len() + 1] = 0; + path = &path_buf[..path.len() + 1]; + } + + if self.care_about_browser_field { + if let Some(browser_scope) = dir_info.get_enclosing_browser_scope() { + const FIELD_REL_PATH: &[u8] = b"index"; + + if let Some(browser_json) = browser_scope.package_json() { + if let Some(remap) = self + .check_browser_map::<{ BrowserMapPathKind::AbsolutePath }>( + &browser_scope, + FIELD_REL_PATH, + ) + { + // Is the path disabled? + if remap.is_empty() { + let paths = [path, FIELD_REL_PATH]; + let new_path = self.fs_ref().abs_buf(&paths, bufs!(remap_path)); + let mut _path = Path::init(new_path); + _path.is_disabled = true; + return Some(MatchResult { + path_pair: PathPair { + primary: _path, + secondary: None, + }, + package_json: Some(std::ptr::from_ref(browser_json)), + ..Default::default() + }); + } + + let new_paths = [path, remap]; + let remapped_abs = self.fs_ref().abs_buf(&new_paths, bufs!(remap_path)); + + // Is this a file + if let Some(file_result) = self.load_as_file(remapped_abs, extension_order) + { + return Some(MatchResult { + dirname_fd: file_result.dirname_fd, + path_pair: PathPair { + primary: Path::init(file_result.path), + secondary: None, + }, + diff_case: file_result.diff_case, + ..Default::default() + }); + } + + // Is it a directory with an index? + if let Ok(Some(new_dir)) = self.dir_info_cached(remapped_abs) { + if let Some(absolute) = self.load_as_index(new_dir, extension_order) { + return Some(absolute); + } + } + + return None; + } + } + } + } + + self.load_as_index(dir_info, extension_order) + } + + pub fn load_as_file_or_directory( + &mut self, + path: &[u8], + kind: ast::ImportKind, + ) -> Option { + let extension_order = self.extension_order; + + // Is this a file? + if let Some(file) = self.load_as_file(path, extension_order) { + // Determine the package folder by looking at the last node_modules/ folder in the path + let nm_seg = const_format::concatcp!("node_modules", SEP_STR).as_bytes(); + if let Some(last_node_modules_folder) = strings::last_index_of(file.path, nm_seg) { + let node_modules_folder_offset = last_node_modules_folder + nm_seg.len(); + // Determine the package name by looking at the next separator + if let Some(package_name_length) = + strings::index_of_char(&file.path[node_modules_folder_offset..], SEP) + { + if let Ok(Some(package_dir_info)) = self.dir_info_cached( + &file.path[0..node_modules_folder_offset + package_name_length as usize], + ) { + if let Some(package_json) = package_dir_info.package_json() { + return Some(MatchResult { + path_pair: PathPair { + primary: Path::init(file.path), + secondary: None, + }, + diff_case: file.diff_case, + dirname_fd: file.dirname_fd, + package_json: Some(std::ptr::from_ref(package_json)), + file_fd: file.file_fd, + ..Default::default() + }); + } + } + } + } + + if cfg!(debug_assertions) { + debug_assert!(bun_paths::is_absolute(file.path)); + } + + return Some(MatchResult { + path_pair: PathPair { + primary: Path::init(file.path), + secondary: None, + }, + diff_case: file.diff_case, + dirname_fd: file.dirname_fd, + file_fd: file.file_fd, + ..Default::default() + }); + } + + // Is this a directory? + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Attempting to load \"{}\" as a directory", + bstr::BStr::new(path) + )); + debug.increase_indent(); + } + // defer if (r.debug_logs) |*debug| debug.decreaseIndent(); + macro_rules! dec_ret { + ($e:expr) => {{ + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return $e; + }}; + } + + // PORT NOTE: `DirInfoRef` (matching spec resolver.zig:3674 raw `*DirInfo`). + // The callees fetch `get_enclosing_browser_scope()` which can resolve + // back to this same BSSMap slot — holding a `&mut` here would alias. + let dir_info: DirInfoRef = match self.dir_info_cached(path) { + Ok(Some(d)) => d, + Ok(None) => dec_ret!(None), + Err(err) => { + #[cfg(debug_assertions)] + Output::pretty_errorln(&format_args!( + "err: {} reading {}", + bstr::BStr::new(err.name()), + bstr::BStr::new(path) + )); + dec_ret!(None); + } + }; + let mut package_json: Option<*const PackageJSON> = None; + + // Try using the main field(s) from "package.json" + if let Some(pkg_json) = dir_info.package_json() { + package_json = Some(std::ptr::from_ref(pkg_json)); + if pkg_json.main_fields.count() > 0 { + let main_field_values = &pkg_json.main_fields; + // BACKREF: `RawSlice` detaches the `&self.opts.main_fields` + // borrow so the loop body can take `&mut self`. Backing + // `Box<[Box<[u8]>]>` heap buffer is owned by `self.opts` and + // never mutated during resolve. + let main_field_keys = bun_ptr::RawSlice::>::new(&self.opts.main_fields); + let mf_ext_order = options::ExtOrder::MainField; + // Spec resolver.zig compares the *pointer* of `opts.main_fields` + // against the per-target default to detect "user did not pass + // --main-fields"; the bundler now projects that as an explicit + // bool because the owned `Box<[Box<[u8]>]>` can never alias a + // static. + let auto_main = self.opts.main_fields_is_default; + + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Searching for main fields in \"{}\"", + bstr::BStr::new(pkg_json.source.path.text) + )); + } + + for key in main_field_keys.iter() { + let key: &[u8] = key; + let field_rel_path = match main_field_values.get(key) { + Some(v) => v, + None => { + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Did not find main field \"{}\"", + bstr::BStr::new(key) + )); + } + continue; + } + }; + + let mut _result = match self.load_from_main_field( + path, + dir_info, + field_rel_path, + key, + if key == b"main" { + mf_ext_order + } else { + extension_order + }, + ) { + Some(r) => r, + None => continue, + }; + + // If the user did not manually configure a "main" field order, then + // use a special per-module automatic algorithm to decide whether to + // use "module" or "main" based on whether the package is imported + // using "import" or "require". + if auto_main && key == b"module" { + let mut absolute_result: Option = None; + + if let Some(main_rel_path) = main_field_values.get(b"main".as_slice()) { + if !main_rel_path.is_empty() { + absolute_result = self.load_from_main_field( + path, + dir_info, + main_rel_path, + b"main", + mf_ext_order, + ); + } + } else { + // Some packages have a "module" field without a "main" field but + // still have an implicit "index.js" file. In that case, treat that + // as the value for "main". + absolute_result = self.load_as_index_with_browser_remapping( + dir_info, + path, + mf_ext_order, + ); + } + + if let Some(auto_main_result) = absolute_result { + // If both the "main" and "module" fields exist, use "main" if the + // path is for "require" and "module" if the path is for "import". + // If we're using "module", return enough information to be able to + // fall back to "main" later if something ended up using "require()" + // with this same path. The goal of this code is to avoid having + // both the "module" file and the "main" file in the bundle at the + // same time. + // + // Additionally, if this is for the runtime, use the "main" field. + // If it doesn't exist, the "module" field will be used. + if self.prefer_module_field && kind != ast::ImportKind::Require { + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Resolved to \"{}\" using the \"module\" field in \"{}\"", + bstr::BStr::new(auto_main_result.path_pair.primary.text()), + bstr::BStr::new(pkg_json.source.path.text) + )); + debug.add_note_fmt(format_args!( + "The fallback path in case of \"require\" is {}", + bstr::BStr::new(auto_main_result.path_pair.primary.text()) + )); + } + + dec_ret!(Some(MatchResult { + path_pair: PathPair { + primary: _result.path_pair.primary, + secondary: Some(auto_main_result.path_pair.primary), + }, + diff_case: _result.diff_case, + dirname_fd: _result.dirname_fd, + package_json, + file_fd: auto_main_result.file_fd, + ..Default::default() + })); + } else { + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Resolved to \"{}\" using the \"{}\" field in \"{}\"", + bstr::BStr::new(auto_main_result.path_pair.primary.text()), + bstr::BStr::new(key), + bstr::BStr::new(pkg_json.source.path.text) + )); + } + let mut _auto_main_result = auto_main_result; + _auto_main_result.package_json = package_json; + dec_ret!(Some(_auto_main_result)); + } + } + } + + _result.package_json = _result.package_json.or(package_json); + dec_ret!(Some(_result)); + } + } + } + + // Look for an "index" file with known extensions + if let Some(res) = + self.load_as_index_with_browser_remapping(dir_info, path, extension_order) + { + let mut res_copy = res; + res_copy.package_json = res_copy.package_json.or(package_json); + dec_ret!(Some(res_copy)); + } + + dec_ret!(None); + } + + pub fn load_as_file( + &mut self, + path: &[u8], + extension_order: options::ExtOrder, + ) -> Option { + // SAFETY: PORT — RealFS is the global singleton (fs.zig); Zig held a raw + // pointer here (resolver.zig:3784). Derive provenance from the raw + // `*mut FileSystem` field so intervening `unsafe { &mut *self.fs() }` calls in + // `load_extension` / `dirname_store.append_slice` don't invalidate `rfs` + // under Stacked Borrows. We re-borrow `&mut *rfs` at each use site. + let rfs: *mut Fs::file_system::RealFS = self.rfs_ptr(); + #[allow(unused_macros)] + macro_rules! rfs { + () => { + unsafe { &mut *rfs } + }; + } + + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Attempting to load \"{}\" as a file", + bstr::BStr::new(path) + )); + debug.increase_indent(); + } + macro_rules! dec_ret { + ($e:expr) => {{ + if let Some(d) = self.debug_logs.as_mut() { + d.decrease_indent(); + } + return $e; + }}; + } + + let dir_path = strings::without_trailing_slash_windows_path(Dirname::dirname(path)); + + // PORT — `dir_entry` is a slot in the BSSMap singleton (ARENA, see + // LIFETIMES.tsv); wrap in `BackRef` so later `&mut self` calls + // (debug_logs / load_extension / dirname_store) don't trip borrowck + // while each read goes through safe `BackRef: Deref` (pointee outlives + // holder by ARENA invariant). + let dir_entry: bun_ptr::BackRef = + match unsafe { &mut *rfs }.read_directory( + dir_path, + None, + self.generation, + self.store_fd, + ) { + Ok(e) => bun_ptr::BackRef::new_mut(e), + Err(_) => dec_ret!(None), + }; + + if let Fs::file_system::real_fs::EntriesOption::Err(err) = dir_entry.get() { + match err.original_err { + e if e == bun_core::err!("ENOENT") + || e == bun_core::err!("FileNotFound") + || e == bun_core::err!("ENOTDIR") + || e == bun_core::err!("NotDir") => {} + _ => { + let _ = self.log_mut().add_error_fmt( + None, + bun_ast::Loc::EMPTY, + format_args!( + "Cannot read directory \"{}\": {}", + bstr::BStr::new(dir_path), + bstr::BStr::new(err.original_err.name()) + ), + ); + } + } + dec_ret!(None); + } + + // ARENA-backed `DirEntry` (see `dir_entry` note above) — `BackRef` so each + // `entries!()` is a fresh safe shared borrow instead of an open-coded raw deref. + let entries = bun_ptr::BackRef::new(dir_entry.entries()); + macro_rules! entries { + () => { + entries.get() + }; + } + + let base = bun_paths::basename(path); + + // Try the plain path without any extensions + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Checking for file \"{}\" ", + bstr::BStr::new(base) + )); + } + + if let Some(query) = entries!().get(base) { + if query.entry().kind(rfs, self.store_fd) == Fs::file_system::EntryKind::File { + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!("Found file \"{}\" ", bstr::BStr::new(base))); + } + + let abs_path: &'static [u8] = { + if query.entry().abs_path.is_empty() { + let abs_path_parts = [query.entry().dir, query.entry().base()]; + let joined = self.fs_ref().abs_buf(&abs_path_parts, bufs!(load_as_file)); + // SAFETY: EntryStore-owned slot; resolver mutex held. RHS fully + // evaluated before LHS `&mut Entry` is materialized. + unsafe { &mut *query.entry }.abs_path = PathString::init( + self.fs_ref() + .dirname_store + .append_slice(joined) + .expect("unreachable"), + ); + } + crate::path_string_static(&query.entry().abs_path) + }; + + dec_ret!(Some(LoadResult { + path: abs_path, + diff_case: query.diff_case, + dirname_fd: entries!().fd, + file_fd: query.entry().cache().fd, + dir_info: None, + })); + } + } + + // Try the path with extensions + bufs!(load_as_file)[..path.len()].copy_from_slice(path); + // PORT NOTE: index by `0..len` so each iteration takes a fresh short + // borrow of `self.opts` that ends before `&mut self` is taken by + // `load_extension` (matches `extra_cjs_extensions` loop below). + let n = self.opts.ext_order_slice(extension_order).len(); + for i in 0..n { + // BACKREF: `RawSlice` detaches the `&self.opts` borrow so the loop + // body can take `&mut self`. Backing `Box<[u8]>` is owned by + // `self.opts` and never mutated while the resolver runs. + let ext = bun_ptr::RawSlice::new(&*self.opts.ext_order_slice(extension_order)[i]); + if let Some(result) = self.load_extension(base, path, &ext, entries!()) { + dec_ret!(Some(result)); + } + } + + // PORT NOTE: index by `0..len` so each iteration takes a fresh short + // borrow of `self.opts` that ends before `&mut self` is taken by + // `load_extension` (avoids the forbidden lifetime-extension cast). + let n = self.opts.extra_cjs_extensions.len(); + for i in 0..n { + // BACKREF: see `RawSlice` note above — backing `Box<[u8]>` in + // `extra_cjs_extensions` is heap-stable for the resolver's life. + let ext = bun_ptr::RawSlice::new(&*self.opts.extra_cjs_extensions[i]); + if let Some(result) = self.load_extension(base, path, &ext, entries!()) { + dec_ret!(Some(result)); + } + } + + // TypeScript-specific behavior: if the extension is ".js" or ".jsx", try + // replacing it with ".ts" or ".tsx". At the time of writing this specific + // behavior comes from the function "loadModuleFromFile()" in the file + // "moduleNameThisResolver.ts" in the TypeScript compiler source code. It + // contains this comment: + // + // If that didn't work, try stripping a ".js" or ".jsx" extension and + // replacing it with a TypeScript one; e.g. "./foo.js" can be matched + // by "./foo.ts" or "./foo.d.ts" + // + // We don't care about ".d.ts" files because we can't do anything with + // those, so we ignore that part of the behavior. + // + // See the discussion here for more historical context: + // https://github.com/microsoft/TypeScript/issues/4595 + if let Some(last_dot) = strings::last_index_of_char(base, b'.') { + let ext = &base[last_dot..base.len()]; + // PORT NOTE: spec resolver.zig:3890-3891 — Zig `and` binds tighter than `or`, so the + // node_modules gate only applies to the `.mjs` arm. Mirror that precedence exactly. + if ext == b".js" + || ext == b".jsx" + || (ext == b".mjs" + && (!FeatureFlags::DISABLE_AUTO_JS_TO_TS_IN_NODE_MODULES + || !strings::path_contains_node_modules_folder(path))) + { + let segment = &base[0..last_dot]; + let tail = &mut bufs!(load_as_file)[path.len() - base.len()..]; + tail[..segment.len()].copy_from_slice(segment); + + let exts: &[&[u8]] = if ext == b".mjs" { + &[b".mts"] + } else { + &[b".ts", b".tsx", b".mts"] + }; + + for ext_to_replace in exts { + let buffer = &mut tail[0..segment.len() + ext_to_replace.len()]; + buffer[segment.len()..].copy_from_slice(ext_to_replace); + + if let Some(query) = entries!().get(&buffer[..]) { + if query.entry().kind(rfs, self.store_fd) + == Fs::file_system::EntryKind::File + { + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Rewrote to \"{}\" ", + bstr::BStr::new(&buffer[..]) + )); + } + + dec_ret!(Some(LoadResult { + path: { + if query.entry().abs_path.is_empty() { + // SAFETY: `dir` is `&'static [u8]` (DirnameStore-interned), + // copied out so no `&Entry` borrow survives into the + // `&mut Entry` write below. + let entry_dir = query.entry().dir; + let new_abs = if !entry_dir.is_empty() + && entry_dir[entry_dir.len() - 1] == SEP + { + let parts: [&[u8]; 2] = [entry_dir, &buffer[..]]; + PathString::init( + self.fs_ref() + .filename_store + .append_parts(&parts) + .expect("unreachable"), + ) + // the trailing path CAN be missing here + } else { + let parts: [&[u8]; 3] = + [entry_dir, SEP_STR.as_bytes(), &buffer[..]]; + PathString::init( + self.fs_ref() + .filename_store + .append_parts(&parts) + .expect("unreachable"), + ) + }; + // SAFETY: EntryStore-owned slot; resolver mutex held. RHS + // fully evaluated above — sole `&mut Entry` for this write. + unsafe { &mut *query.entry }.abs_path = new_abs; + } + crate::path_string_static(&query.entry().abs_path) + }, + diff_case: query.diff_case, + dirname_fd: entries!().fd, + file_fd: query.entry().cache().fd, + dir_info: None, + })); + } + } + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Failed to rewrite \"{}\" ", + bstr::BStr::new(base) + )); + } + } + } + } + + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Failed to find \"{}\" ", + bstr::BStr::new(path) + )); + } + + if FeatureFlags::WATCH_DIRECTORIES { + // For existent directories which don't find a match + // Start watching it automatically, + if let Some(watcher) = self.watcher.as_ref() { + watcher.watch(entries!().dir, entries!().fd); + } + } + dec_ret!(None); + } + + fn load_extension( + &mut self, + base: &[u8], + path: &[u8], + ext: &[u8], + entries: &Fs::file_system::DirEntry, + ) -> Option { + // SAFETY: PORT — see load_as_file; derive `rfs` from the raw `*mut FileSystem` + // field so `unsafe { &mut *self.fs() }` calls below (`filename_store.append_parts`) don't pop + // its provenance under Stacked Borrows. + let rfs: *mut Fs::file_system::RealFS = self.rfs_ptr(); + // BACKREF — `entries` is a slot in the BSSMap-backed `DirEntry` arena + // (see `load_as_file`); detach the borrowck lifetime via `BackRef` so the + // `&mut self` calls below (debug_logs / fs_ref) don't conflict, while + // each read stays a safe `BackRef: Deref`. + let entries = bun_ptr::BackRef::new(entries); + let buffer = &mut bufs!(load_as_file)[0..path.len() + ext.len()]; + buffer[path.len()..].copy_from_slice(ext); + let file_name = &buffer[path.len() - base.len()..buffer.len()]; + + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Checking for file \"{}\" ", + bstr::BStr::new(&buffer[..]) + )); + } + + if let Some(query) = entries.get().get(file_name) { + if query.entry().kind(rfs, self.store_fd) == Fs::file_system::EntryKind::File { + if let Some(debug) = self.debug_logs.as_mut() { + debug.add_note_fmt(format_args!( + "Found file \"{}\" ", + bstr::BStr::new(&buffer[..]) + )); + } + + // now that we've found it, we allocate it. + return Some(LoadResult { + path: { + // SAFETY: EntryStore-owned slot; resolver mutex held. RHS is fully + // evaluated (shared reads) before the LHS `&mut Entry` is + // materialized for the write — no overlapping unique borrow. + unsafe { &mut *query.entry }.abs_path = if query.entry().abs_path.is_empty() + { + PathString::init( + self.fs_ref() + .dirname_store + .append_slice(&buffer[..]) + .expect("unreachable"), + ) + } else { + query.entry().abs_path + }; + crate::path_string_static(&query.entry().abs_path) + }, + diff_case: query.diff_case, + dirname_fd: entries.fd, + file_fd: query.entry().cache().fd, + dir_info: None, + }); + } + } + + None + } + + fn dir_info_uncached( + &mut self, + info: *mut DirInfo::DirInfo, + path: &'static [u8], + _entries: *mut Fs::file_system::real_fs::EntriesOption, + _result: allocators::Result, + dir_entry_index: allocators::IndexType, + parent: Option, + parent_index: allocators::IndexType, + fd: FD, + package_id: Option, + ) -> core::result::Result<(), bun_core::Error> { + let result = _result; + + // SAFETY: PORT — RealFS / DirEntry are global ARENA singletons (BSSMap-backed); + // Zig held raw pointers here (resolver.zig:4004 `rfs: *Fs.FileSystem.RealFS`). + // Derive `rfs_ptr` from the raw `*mut FileSystem` field so later `unsafe { &mut *self.fs() }` calls + // (`abs_buf` / `dirname_store.append_slice` in the parent-symlink block) cannot + // invalidate it under Stacked Borrows. Re-borrow at EACH use site so no `&mut` + // outlives a `unsafe { &mut *self.fs() }` / `get_entries()` / `parse_package_json()` call. + // TODO(port): split RealFS borrow once entries iteration is interior-mutability-backed. + let rfs_ptr: *mut Fs::file_system::RealFS = self.rfs_ptr(); + let entries_ptr: *mut Fs::file_system::DirEntry = unsafe { &mut *_entries }.entries_mut(); + // PORT NOTE: re-borrow per use; see SAFETY note above. + macro_rules! rfs { + () => { + unsafe { &mut *rfs_ptr } + }; + } + macro_rules! entries { + () => { + unsafe { &mut *entries_ptr } + }; + } + + if cfg!(debug_assertions) { + // `path` is stored in the permanent `dir_cache` as `DirInfo.abs_path`. It must not + // point into a reused threadlocal scratch buffer, or a later resolution will + // corrupt cached entries. Callers must intern it (e.g. via `DirnameStore`) first. + ::bun_core::assertf!( + !allocators::is_slice_in_buffer(path, &bufs!(path_in_global_disk_cache)[..]), + "DirInfo.abs_path must not point into the threadlocal path_in_global_disk_cache buffer (got \"{}\")", + bstr::BStr::new(path) + ); + } + + // SAFETY: info is a slot in the BSSMap-backed dir_cache + let info = unsafe { &mut *info }; + *info = DirInfo::DirInfo { + abs_path: path, + // .abs_real_path = path, + parent: parent_index, + entries: dir_entry_index, + ..Default::default() + }; + + // A "node_modules" directory isn't allowed to directly contain another "node_modules" directory + let mut base = bun_paths::basename(path); + + // base must + if base.len() > 1 && base[base.len() - 1] == SEP { + base = &base[0..base.len() - 1]; + } + + info.flags + .set_present(DirInfo::Flag::IsNodeModules, base == b"node_modules"); + + // if (entries != null) { + if !info.is_node_modules() { + if let Some(entry) = entries!().get_comptime_query(b"node_modules") { + info.flags.set_present( + DirInfo::Flag::HasNodeModules, + entry.entry().kind(rfs!(), self.store_fd) == Fs::file_system::EntryKind::Dir, + ); + } + } + + if self.care_about_bin_folder { + 'append_bin_dir: { + if info.has_node_modules() { + if entries!().has_comptime_query(b"node_modules") { + // SAFETY: BIN_FOLDERS guarded by BIN_FOLDERS_LOCK below + if !BIN_FOLDERS_LOADED.load(core::sync::atomic::Ordering::Acquire) { + // SAFETY: callers hold RESOLVER_MUTEX; first init. + unsafe { (*BIN_FOLDERS.get()).write(BinFolderArray::default()) }; + BIN_FOLDERS_LOADED.store(true, core::sync::atomic::Ordering::Release); + } + + // TODO(port): std.fs.Dir.openDirZ → bun_sys + let Ok(file) = bun_sys::open_dir_z( + fd, + bun_paths::path_literal!(b"node_modules/.bin"), + Default::default(), + ) else { + break 'append_bin_dir; + }; + let _close = bun_sys::CloseOnDrop::new(file); + let Ok(bin_path) = file.get_fd_path(bufs!(node_bin_path)) else { + break 'append_bin_dir; + }; + let _unlock = BIN_FOLDERS_LOCK.lock_guard(); + + // SAFETY: BIN_FOLDERS guarded by BIN_FOLDERS_LOCK acquired above. + unsafe { + for existing_folder in + (*BIN_FOLDERS.get()).assume_init_ref().const_slice() + { + if *existing_folder == bin_path { + break 'append_bin_dir; + } + } + + let Ok(stored) = self.fs_ref().dirname_store.append_slice(bin_path) + else { + break 'append_bin_dir; + }; + let _ = (*BIN_FOLDERS.get()).assume_init_mut().append(stored); + } + } + } + + if info.is_node_modules() { + if let Some(q) = entries!().get_comptime_query(b".bin") { + if q.entry().kind(rfs!(), self.store_fd) == Fs::file_system::EntryKind::Dir + { + // SAFETY: BIN_FOLDERS_LOADED is single-thread init-once; protected by RESOLVER_MUTEX held by callers. + if !BIN_FOLDERS_LOADED.load(core::sync::atomic::Ordering::Acquire) { + // SAFETY: callers hold RESOLVER_MUTEX; first init. + unsafe { (*BIN_FOLDERS.get()).write(BinFolderArray::default()) }; + BIN_FOLDERS_LOADED + .store(true, core::sync::atomic::Ordering::Release); + } + + let Ok(file) = bun_sys::open_dir_z(fd, b".bin\0", Default::default()) + else { + break 'append_bin_dir; + }; + let _close = bun_sys::CloseOnDrop::new(file); + let Ok(bin_path) = bun_sys::get_fd_path(file, bufs!(node_bin_path)) + else { + break 'append_bin_dir; + }; + let _unlock = BIN_FOLDERS_LOCK.lock_guard(); + + // SAFETY: BIN_FOLDERS guarded by BIN_FOLDERS_LOCK acquired above. + unsafe { + for existing_folder in + (*BIN_FOLDERS.get()).assume_init_ref().const_slice() + { + if *existing_folder == bin_path { + break 'append_bin_dir; + } + } + + let Ok(stored) = self.fs_ref().dirname_store.append_slice(bin_path) + else { + break 'append_bin_dir; + }; + let _ = (*BIN_FOLDERS.get()).assume_init_mut().append(stored); + } + } + } + } + } + } + // } + + if let Some(parent_) = parent { + // Propagate the browser scope into child directories + info.enclosing_browser_scope = parent_.enclosing_browser_scope; + info.package_json_for_browser_field = parent_.package_json_for_browser_field; + info.enclosing_tsconfig_json = parent_.enclosing_tsconfig_json; + + if let Some(parent_package_json) = parent_.package_json() { + // https://github.com/oven-sh/bun/issues/229 + if !parent_package_json.name.is_empty() || self.care_about_bin_folder { + info.enclosing_package_json = Some(parent_package_json); + } + + if parent_package_json.dependencies.map.count() > 0 + || parent_package_json.package_manager_package_id != Install::INVALID_PACKAGE_ID + { + // PORT NOTE: store the raw `NonNull` field (not the + // `&'static` accessor result) so mut-provenance flows + // through to `enqueue_dependency_to_resolve`. + info.package_json_for_dependencies = parent_.package_json; + } + } + + info.enclosing_package_json = info + .enclosing_package_json + .or(parent_.enclosing_package_json); + info.package_json_for_dependencies = info + .package_json_for_dependencies + .or(parent_.package_json_for_dependencies); + + // Make sure "absRealPath" is the real path of the directory (resolving any symlinks) + if !self.opts.preserve_symlinks { + if let Some(parent_entries) = parent_.get_entries_ref(self.generation) { + if let Some(lookup) = parent_entries.get(base) { + // `entries_ptr` is a slot in the BSSMap-backed entries singleton — + // route the read-only `.fd` access through the existing + // `entries!()` re-borrow macro instead of a raw-ptr deref. + let entries_fd = entries!().fd; + if entries_fd.is_valid() + && !lookup.entry().cache().fd.is_valid() + && self.store_fd + { + lookup.entry().set_cache_fd(entries_fd); + } + // SAFETY: EntryStore-owned slot; `entries_mutex` held — read-only borrow, + // dies (NLL) before any later `&mut` to this slot. + let entry = lookup.entry(); + + let mut symlink = entry.symlink(rfs!(), self.store_fd); + if !symlink.is_empty() { + if let Some(logs) = self.debug_logs.as_mut() { + let mut buf = Vec::new(); + write!( + &mut buf, + "Resolved symlink \"{}\" to \"{}\"", + bstr::BStr::new(path), + bstr::BStr::new(symlink) + ) + .ok(); + logs.add_note(buf); + } + info.abs_real_path = symlink; + } else if !parent_.abs_real_path.is_empty() { + // this might leak a little i'm not sure + let parts = [parent_.abs_real_path, base]; + // PORT NOTE: split into two statements so the two `&mut FileSystem` + // borrows from `unsafe { &mut *self.fs() }` don't overlap (Stacked Borrows). + let joined = self + .fs_ref() + .abs_buf(&parts, bufs!(dir_info_uncached_filename)); + symlink = self + .fs_ref() + .dirname_store + .append_slice(joined) + .expect("unreachable"); + + if let Some(logs) = self.debug_logs.as_mut() { + let mut buf = Vec::new(); + write!( + &mut buf, + "Resolved symlink \"{}\" to \"{}\"", + bstr::BStr::new(path), + bstr::BStr::new(symlink) + ) + .ok(); + logs.add_note(buf); + } + lookup.entry().set_cache_symlink(PathString::init(symlink)); + info.abs_real_path = symlink; + } + } + } + } + + if parent_.is_node_modules() || parent_.is_inside_node_modules() { + info.flags + .set_present(DirInfo::Flag::InsideNodeModules, true); + } + } + + // Record if this directory has a package.json file + if self.opts.load_package_json { + if let Some(lookup) = entries!().get_comptime_query(b"package.json") { + // SAFETY: EntryStore-owned slot; `entries_mutex` held — read-only borrow, + // dies (NLL) before any later `&mut` to this slot. + let entry = lookup.entry(); + if entry.kind(rfs!(), self.store_fd) == Fs::file_system::EntryKind::File { + info.package_json = if self.use_package_manager() + && !info.has_node_modules() + && !info.is_node_modules() + { + self.parse_package_json::( + path, + if FeatureFlags::STORE_FILE_DESCRIPTORS { + fd + } else { + FD::INVALID + }, + package_id, + ) + .ok() + .flatten() + } else { + self.parse_package_json::( + path, + if FeatureFlags::STORE_FILE_DESCRIPTORS { + fd + } else { + FD::INVALID + }, + None, + ) + .ok() + .flatten() + }; + + if let Some(pkg) = info.package_json() { + if pkg.browser_map.count() > 0 { + info.enclosing_browser_scope = result.index; + info.package_json_for_browser_field = Some(pkg); + } + + if !pkg.name.is_empty() || self.care_about_bin_folder { + info.enclosing_package_json = Some(pkg); + } + + if pkg.dependencies.map.count() > 0 + || pkg.package_manager_package_id != Install::INVALID_PACKAGE_ID + { + // PORT NOTE: store the raw `NonNull` field (not the + // `&'static` accessor result) so mut-provenance flows + // through to `enqueue_dependency_to_resolve`. + info.package_json_for_dependencies = info.package_json; + } + + if let Some(logs) = self.debug_logs.as_mut() { + logs.add_note_fmt(format_args!( + "Resolved package.json in \"{}\"", + bstr::BStr::new(path) + )); + } + } + } + } + } + + // Record if this directory has a tsconfig.json or jsconfig.json file + if self.opts.load_tsconfig_json { + let mut tsconfig_path: Option<&[u8]> = None; + if self.opts.tsconfig_override.is_none() { + if let Some(lookup) = entries!().get_comptime_query(b"tsconfig.json") { + // SAFETY: EntryStore-owned slot; `entries_mutex` held — read-only borrow, + // dies (NLL) before any later `&mut` to this slot. + let entry = lookup.entry(); + if entry.kind(rfs!(), self.store_fd) == Fs::file_system::EntryKind::File { + let parts = [path, b"tsconfig.json".as_slice()]; + tsconfig_path = Some( + self.fs_ref() + .abs_buf(&parts, bufs!(dir_info_uncached_filename)), + ); + } + } + if tsconfig_path.is_none() { + if let Some(lookup) = entries!().get_comptime_query(b"jsconfig.json") { + // SAFETY: EntryStore-owned slot; `entries_mutex` held — read-only borrow, + // dies (NLL) before any later `&mut` to this slot. + let entry = lookup.entry(); + if entry.kind(rfs!(), self.store_fd) == Fs::file_system::EntryKind::File { + let parts = [path, b"jsconfig.json".as_slice()]; + tsconfig_path = Some( + self.fs_ref() + .abs_buf(&parts, bufs!(dir_info_uncached_filename)), + ); + } + } + } + } else if parent.is_none() { + // PORT NOTE: re-borrow as 'static so the `&self.opts` borrow ends before + // `self.parse_tsconfig(&mut self, ...)`. `tsconfig_override` is owned by + // BundleOptions (lives for the resolver's lifetime). + tsconfig_path = self + .opts + .tsconfig_override + .as_deref() + .map(|s| unsafe { &*std::ptr::from_ref::<[u8]>(s) }); + } + + if let Some(tsconfigpath) = tsconfig_path { + let parsed_tsconfig: Option<*mut TSConfigJSON> = match self.parse_tsconfig( + tsconfigpath, + if FeatureFlags::STORE_FILE_DESCRIPTORS { + fd + } else { + FD::ZERO + }, + ) { + Ok(v) => v.map(bun_core::heap::into_raw), + Err(err) => { + let pretty = tsconfigpath; + if err == bun_core::err!("ENOENT") || err == bun_core::err!("FileNotFound") + { + let _ = self.log_mut().add_error_fmt( + None, + bun_ast::Loc::EMPTY, + format_args!( + "Cannot find tsconfig file {}", + bun_core::fmt::quote(pretty) + ), + ); + } else if err != bun_core::err!("ParseErrorAlreadyLogged") + && err != bun_core::err!("IsDir") + && err != bun_core::err!("EISDIR") + { + let _ = self.log_mut().add_error_fmt( + None, + bun_ast::Loc::EMPTY, + format_args!( + "Cannot read file {}: {}", + bun_core::fmt::quote(pretty), + bstr::BStr::new(err.name()) + ), + ); + } + None + } + }; + // PORT NOTE: spec resolver.zig:4207 assigns info.tsconfig_json here (a raw + // ?*TSConfigJSON), then frees that allocation in the merge loop below before + // reassigning. With Rust references (Option<&'static TSConfigJSON>, dir_info.rs) + // that briefly-dangling state is UB. Defer the assignment to after the merge — + // it is always overwritten when parsed_tsconfig.is_some(), and DirInfo defaults + // tsconfig_json to None otherwise. + if let Some(tsconfig_json) = parsed_tsconfig { + let mut parent_configs: BoundedArray<*mut TSConfigJSON, 64> = + BoundedArray::default(); + parent_configs.append(tsconfig_json)?; + // `current`/`parent_config_ptr`/`merged_config` are heap TSConfigJSON + // allocations from `parse_tsconfig` (heap::alloc); uniquely owned by + // this extends-chain walk and freed via heap::take below. Hold as + // `BackRef` (pointee outlives holder) so the loop body reads via safe + // `Deref` instead of three open-coded raw-ptr derefs. + let mut current = bun_ptr::BackRef::from( + core::ptr::NonNull::new(tsconfig_json).expect("heap alloc"), + ); + while !current.extends.is_empty() { + let ts_dir_name = Dirname::dirname(¤t.abs_path); + let abs_path = ResolvePath::join_abs_string_buf( + ts_dir_name, + bufs!(tsconfig_path_abs), + &[ts_dir_name, ¤t.extends], + bun_paths::Platform::AUTO, + ); + let parent_config_maybe: Option<*mut TSConfigJSON> = + match self.parse_tsconfig(abs_path, FD::INVALID) { + Ok(v) => v.map(bun_core::heap::into_raw), + Err(err) => { + let _ = self.log_mut().add_debug_fmt( + None, + bun_ast::Loc::EMPTY, + format_args!( + "{} loading tsconfig.json extends {}", + bstr::BStr::new(err.name()), + bun_core::fmt::quote(abs_path) + ), + ); + break; + } + }; + if let Some(parent_config) = parent_config_maybe { + parent_configs.append(parent_config)?; + current = bun_ptr::BackRef::from( + core::ptr::NonNull::new(parent_config).expect("heap alloc"), + ); + } else { + break; + } + } + + let mut merged_config = parent_configs.pop().unwrap(); + // starting from the base config (end of the list) + // successively apply the inheritable attributes to the next config + while let Some(parent_config_ptr) = parent_configs.pop() { + // SAFETY: see loop-wide note above. + let parent_config = unsafe { &mut *parent_config_ptr }; + // SAFETY: see loop-wide note above. + let mc = unsafe { &mut *merged_config }; + mc.emit_decorator_metadata = + mc.emit_decorator_metadata || parent_config.emit_decorator_metadata; + if !parent_config.base_url.is_empty() { + mc.base_url = core::mem::take(&mut parent_config.base_url); + } + mc.jsx = parent_config.merge_jsx(mc.jsx.clone()); + mc.jsx_flags.insert_all(parent_config.jsx_flags); + + if let Some(value) = parent_config.preserve_imports_not_used_as_values { + mc.preserve_imports_not_used_as_values = Some(value); + } + + // TypeScript replaces paths across extends (child overrides parent + // entirely), so when a more-specific config defines paths, replace + // rather than merge. base_url_for_paths is set whenever the paths + // key is present in the JSON (even if empty), so it discriminates + // "not defined" from "defined as {}" — the latter clears inherited + // paths per TypeScript semantics. + if !parent_config.base_url_for_paths.is_empty() { + // The previous merged_config.paths is being replaced; free its + // backing storage before overwriting so the PathsMap from the + // deeper config doesn't leak. Each value is a []string slice + // that was separately heap-allocated in TSConfigJSON.parse() + // (tsconfig_json.zig), so free those before the map itself. + // (In Rust, dropping the map frees values automatically.) + mc.paths = core::mem::take(&mut parent_config.paths); + mc.base_url_for_paths = + core::mem::take(&mut parent_config.base_url_for_paths); + } else { + // paths were not moved to merged_config, so they're still owned + // by parent_config. base_url_for_paths.len == 0 implies the map + // is empty (it's only set when the `paths` key is present in the + // JSON), so this is a no-op but documents the ownership. + // (Drop handles parent_config.paths.) + } + // Every scalar/reference we need has been copied into merged_config + // (strings live in dirname_store or default_allocator and outlive the + // struct). The heap-allocated TSConfigJSON itself is no longer needed; + // without this, every intermediate config in an extends chain leaks on + // each dirInfoUncached() call, which is especially bad under HMR where + // bustDirCache triggers a re-parse of the whole chain on every reload. + // SAFETY: parent_config_ptr came from TSConfigJSON::new (heap::alloc) + TSConfigJSON::destroy(unsafe { bun_core::heap::take(parent_config_ptr) }); + } + // `merged_config` is a leaked Box (heap::alloc) interned into DirInfo; outlives the resolver. + info.tsconfig_json = Some( + core::ptr::NonNull::new(merged_config).expect("heap::alloc is non-null"), + ); + } + info.enclosing_tsconfig_json = info.tsconfig_json(); + } + } + + Ok(()) + } +} + +impl<'a> Resolver<'a> { + /// Port of `pub fn deinit(r: *ThisResolver)` (resolver.zig:601-604). + /// + /// PORT NOTE: NOT `impl Drop` — the bundler builds a `Resolver` per worker + /// thread (see `for_worker`), and all instances share the same `dir_cache` + /// singleton. A `Drop` impl would fire once per worker going out of scope, + /// resetting the SHARED cache (freeing PackageJSON/TSConfigJSON, closing cached + /// fds) while other live Resolvers still hold pointers into it. Spec calls + /// `deinit` explicitly exactly once at shutdown; mirror that. + pub fn deinit(&mut self) { + // Caller is the sole remaining owner at shutdown; no other Resolver alias is live. + for di in self.dir_cache_mut().values_mut() { + // Zig: `di.deinit()` — releases owned PackageJSON / TSConfigJSON resources + // in-place (side effects beyond memory: those Drops close cached fds / + // deref intrusive refcounts). Ported as `DirInfo::reset`. + di.reset(); + } + // dir_cache is &'static — do not deinit the singleton here + // TODO(port): Zig calls dir_cache.deinit() but it's a global BSSMap; revisit ownership + } +} + +// ─── nested helper types ─────────────────────────────────────────────────── + +enum DependencyToResolve { + NotFound, + Pending(PendingResolution), + Failure(bun_core::Error), + Resolution(Resolution), +} + +#[derive(Clone, Copy, PartialEq, Eq, core::marker::ConstParamTy)] +pub enum BrowserMapPathKind { + PackagePath, + AbsolutePath, +} + +pub struct BrowserMapPath<'b> { + pub remapped: &'static [u8], + pub cleaned: &'b [u8], + pub input_path: &'b [u8], + pub extension_order: &'b [Box<[u8]>], + pub map: &'b BrowserMap, +} + +impl<'b> BrowserMapPath<'b> { + pub fn check_path(&mut self, path_to_check: &[u8]) -> bool { + let map = self.map; + + let cleaned = self.cleaned; + // Check for equality + if let Some(result) = map.get(path_to_check) { + // SAFETY: ARENA — `BrowserMap` values are `Box<[u8]>` owned by a `'static` + // PackageJSON (allocated in `parse_package_json`, never freed — DirInfo + // cache is process-global); the `'b` borrow on `map` artificially shortens + // what is process-lifetime storage. `Interned` is the canonical proof type. + self.remapped = unsafe { bun_ptr::Interned::assume(result) }.as_bytes(); + // SAFETY: TODO(port): lifetime — extending borrow of caller-owned slice; consumed before checker is dropped. + self.input_path = unsafe { &*std::ptr::from_ref::<[u8]>(path_to_check) }; + return true; + } + + let ext_buf = bufs!(extension_path); + + if cleaned.len() <= ext_buf.len() { + ext_buf[..cleaned.len()].copy_from_slice(cleaned); + + // If that failed, try adding implicit extensions + for ext in self.extension_order.iter() { + let ext: &[u8] = ext; + if cleaned.len() + ext.len() > ext_buf.len() { + continue; + } + ext_buf[cleaned.len()..cleaned.len() + ext.len()].copy_from_slice(ext); + let new_path = &ext_buf[0..cleaned.len() + ext.len()]; + // if let Some(debug) = r.debug_logs.as_mut() { + // debug.add_note_fmt(format_args!("Checking for \"{}\" ", bstr::BStr::new(new_path))); + // } + if let Some(_remapped) = map.get(new_path) { + // SAFETY: ARENA — see `result` note above. + self.remapped = unsafe { bun_ptr::Interned::assume(_remapped) }.as_bytes(); + // SAFETY: TODO(port): lifetime — `new_path` borrows the threadlocal `extension_path` buf; consumed before next overwrite. + self.cleaned = unsafe { &*std::ptr::from_ref::<[u8]>(new_path) }; + // SAFETY: same as above. + self.input_path = unsafe { &*std::ptr::from_ref::<[u8]>(new_path) }; + return true; + } + } + } + + // If that failed, try assuming this is a directory and looking for an "index" file + + let index_path: &[u8] = { + let trimmed = strings::trim_right(path_to_check, &[SEP]); + let parts = [ + trimmed, + const_format::concatcp!(SEP_STR, "index").as_bytes(), + ]; + ResolvePath::join_string_buf( + bufs!(tsconfig_base_url), + &parts, + bun_paths::Platform::AUTO, + ) + }; + + if let Some(_remapped) = map.get(index_path) { + // SAFETY: ARENA — see `result` note above. + self.remapped = unsafe { bun_ptr::Interned::assume(_remapped) }.as_bytes(); + // SAFETY: TODO(port): lifetime — `index_path` borrows the threadlocal `extension_path` buf; consumed before next overwrite. + self.input_path = unsafe { &*std::ptr::from_ref::<[u8]>(index_path) }; + return true; + } + + if index_path.len() <= ext_buf.len() { + ext_buf[..index_path.len()].copy_from_slice(index_path); + + for ext in self.extension_order.iter() { + let ext: &[u8] = ext; + if index_path.len() + ext.len() > ext_buf.len() { + continue; + } + ext_buf[index_path.len()..index_path.len() + ext.len()].copy_from_slice(ext); + let new_path = &ext_buf[0..index_path.len() + ext.len()]; + // if let Some(debug) = r.debug_logs.as_mut() { + // debug.add_note_fmt(format_args!("Checking for \"{}\" ", bstr::BStr::new(new_path))); + // } + if let Some(_remapped) = map.get(new_path) { + // SAFETY: ARENA — see `result` note above. + self.remapped = unsafe { bun_ptr::Interned::assume(_remapped) }.as_bytes(); + // SAFETY: TODO(port): lifetime — `new_path` borrows the threadlocal `extension_path` buf; consumed before next overwrite. + self.cleaned = unsafe { &*std::ptr::from_ref::<[u8]>(new_path) }; + // SAFETY: same as above. + self.input_path = unsafe { &*std::ptr::from_ref::<[u8]>(new_path) }; + return true; + } + } + } + + false + } +} + +#[inline] +fn is_dot_slash(path: &[u8]) -> bool { + #[cfg(not(windows))] + { + path == b"./" + } + #[cfg(windows)] + { + path.len() == 2 && path[0] == b'.' && strings::char_is_any_slash(path[1]) + } +} + +// ModuleTypeMap = bun.ComptimeStringMap(options.ModuleType, .{...}) +// +// PERF(port): was `phf::Map<&[u8], ModuleType>`. With only 4 keys — all +// length 4 — the phf hash + index probe is strictly more work than a single +// length gate followed by 4-byte compares (which LLVM lowers to one u32 +// load + compare per arm once `len == 4` is established). Mirrors the +// length-gated dispatch used in `clap::find_param`. +#[inline] +fn module_type_from_ext(ext: &[u8]) -> Option { + if ext.len() != 4 { + return None; + } + match ext { + b".mjs" | b".mts" => Some(options::ModuleType::Esm), + b".cjs" | b".cts" => Some(options::ModuleType::Cjs), + _ => None, + } +} + +const NODE_MODULE_ROOT_STRING: &[u8] = + const_format::concatcp!(SEP_STR, "node_modules", SEP_STR).as_bytes(); + +// `dev` scope (Output.scoped(.Resolver, .visible)) — same scope name as `debuglog` but visible. +// Folded into the same `Resolver` declared scope; TODO(port): restore the visible/hidden +// scope distinction. + +pub struct Dirname; + +impl Dirname { + /// NOT `std.fs.path.dirname`. Resolver-specific upward-traversal dirname + /// (resolver.zig:4297): returns trailing-sep-INCLUSIVE slice, never `None`, + /// `is_sep_any` on all platforms. Do NOT replace with `bun_core::dirname`. + pub fn dirname(path: &[u8]) -> &[u8] { + if path.is_empty() { + return SEP_STR.as_bytes(); + } + + let root: &[u8] = { + #[cfg(windows)] + { + let root = ResolvePath::windows_filesystem_root(path); + // Preserve the trailing slash for UNC paths. + // Going from `\\server\share\folder` should end up + // at `\\server\share\`, not `\\server\share` + if root.len() >= 5 && path.len() > root.len() { + &path[0..root.len() + 1] + } else { + root + } + } + #[cfg(not(windows))] + { + b"/" + } + }; + + let mut end_index: usize = path.len() - 1; + while bun_paths::is_sep_any(path[end_index]) { + if end_index == 0 { + return root; + } + end_index -= 1; + } + + while !bun_paths::is_sep_any(path[end_index]) { + if end_index == 0 { + return root; + } + end_index -= 1; + } + + if end_index == 0 && bun_paths::is_sep_any(path[0]) { + return &path[0..1]; + } + + if end_index == 0 { + return root; + } + + &path[0..end_index + 1] + } +} + +pub struct RootPathPair<'b> { + pub base_path: &'b [u8], + pub package_json: *const PackageJSON, +} + +// ported from: src/resolver/resolver.zig diff --git a/src/resolver/result.rs b/src/resolver/result.rs new file mode 100644 index 00000000000..03567c229b7 --- /dev/null +++ b/src/resolver/result.rs @@ -0,0 +1,578 @@ +//! Resolver output and bookkeeping types: `Result`, `MatchResult`, `LoadResult`, +//! `PathPair`, `PendingResolution`, `DebugLogs`, and friends. These are the +//! value types the [`crate::Resolver`] state machine produces and threads +//! through `resolve_without_remapping` / `load_as_file_or_directory`. + +use core::ptr::NonNull; +use std::io::Write as _; + +use ::bun_ast::import_record as ast; +use ::bun_install_types::resolver_hooks as Install; +use bun_ast::Msg; +use bun_collections::MultiArrayList; +use bun_core::MutableString; +use bun_paths::SEP_STR; +use bun_paths::strings; +use bun_sys::Fd as FD; + +use crate::dir_info::DirInfoRef; +use crate::fs as Fs; +use crate::package_json::PackageJSON; +use crate::resolver::Dependency; +use crate::{allocators, options}; + +// PORT NOTE: `Path` in the body is the `'static`-interned variant (paths borrow +// DirnameStore/FilenameStore). Alias here so the bare-`Path` use sites resolve +// without a per-site lifetime annotation. +type Path = crate::fs::Path<'static>; + +pub struct SideEffectsData { + pub source: Option>, // TODO(port): lifetime — never instantiated + pub range: bun_ast::Range, + + // If true, "sideEffects" was an array. If false, "sideEffects" was false. + pub is_side_effects_array_in_json: bool, +} + +pub struct PathPair { + pub primary: Path, + pub secondary: Option, +} + +impl Default for PathPair { + fn default() -> Self { + Self { + primary: Path::empty(), + secondary: None, + } + } +} + +pub struct PathPairIter<'a> { + index: u8, // u2 in Zig + ctx: &'a mut PathPair, +} + +impl<'a> PathPairIter<'a> { + pub fn next(&mut self) -> Option<&mut Path> { + if let Some(path_) = self.next_() { + // SAFETY: reshaped for borrowck — recurse via raw ptr to avoid double &mut. + let p: *mut Path = path_; + unsafe { + if (*p).is_disabled { + return self.next(); + } + return Some(&mut *p); + } + } + None + } + + fn next_(&mut self) -> Option<&mut Path> { + let ind = self.index; + self.index = self.index.saturating_add(1); + + match ind { + 0 => Some(&mut self.ctx.primary), + 1 => self.ctx.secondary.as_mut(), + _ => None, + } + } +} + +impl PathPair { + pub fn iter(&mut self) -> PathPairIter<'_> { + PathPairIter { + ctx: self, + index: 0, + } + } +} + +// Re-export of `bun_ast::SideEffects`. +// Spec: options.zig:884 `Loader.sideEffects()` returns `bun.resolver.SideEffects` +// — the SAME type stored in `Result.primary_side_effects_data`. Re-export so +// `result.primary_side_effects_data = loader.side_effects()` type-checks. +use bun_ast::SideEffects; + +pub struct Result { + pub path_pair: PathPair, + + pub jsx: options::jsx::Pragma, + + pub package_json: Option<*const PackageJSON>, + + pub diff_case: Option>, + + // If present, any ES6 imports to this file can be considered to have no side + // effects. This means they should be removed if unused. + pub primary_side_effects_data: SideEffects, + + // This is the "type" field from "package.json" + pub module_type: options::ModuleType, + + pub debug_meta: Option, + + pub dirname_fd: FD, + pub file_fd: FD, + pub import_kind: ast::ImportKind, + + /// Pack boolean flags to reduce padding overhead. + /// Previously 6 separate bool fields caused ~42+ bytes of padding waste. + pub flags: ResultFlags, +} + +impl Default for Result { + fn default() -> Self { + Self { + path_pair: PathPair::default(), + jsx: options::jsx::Pragma::default(), + package_json: None, + diff_case: None, + primary_side_effects_data: SideEffects::HasSideEffects, + module_type: options::ModuleType::Unknown, + debug_meta: None, + dirname_fd: FD::INVALID, + file_fd: FD::INVALID, + import_kind: ast::ImportKind::Stmt, // Zig: undefined + flags: ResultFlags::default(), + } + } +} + +bitflags::bitflags! { + #[derive(Default, Clone, Copy)] + pub struct ResultFlags: u8 { + const IS_EXTERNAL = 1 << 0; + const IS_EXTERNAL_AND_REWRITE_IMPORT_PATH = 1 << 1; + const IS_STANDALONE_MODULE = 1 << 2; + // This is true when the package was loaded from within the node_modules directory. + const IS_FROM_NODE_MODULES = 1 << 3; + // If true, unused imports are retained in TypeScript code. This matches the + // behavior of the "importsNotUsedAsValues" field in "tsconfig.json" when the + // value is not "remove". + const PRESERVE_UNUSED_IMPORTS_TS = 1 << 4; + const EMIT_DECORATOR_METADATA = 1 << 5; + const EXPERIMENTAL_DECORATORS = 1 << 6; + // _padding: u1 + } +} + +// Convenience accessors mirroring the Zig packed-struct field syntax. +impl ResultFlags { + #[inline] + pub fn is_external(&self) -> bool { + self.contains(Self::IS_EXTERNAL) + } + #[inline] + pub fn set_is_external(&mut self, v: bool) { + self.set(Self::IS_EXTERNAL, v) + } + #[inline] + pub fn is_external_and_rewrite_import_path(&self) -> bool { + self.contains(Self::IS_EXTERNAL_AND_REWRITE_IMPORT_PATH) + } + #[inline] + pub fn set_is_external_and_rewrite_import_path(&mut self, v: bool) { + self.set(Self::IS_EXTERNAL_AND_REWRITE_IMPORT_PATH, v) + } + #[inline] + pub fn is_standalone_module(&self) -> bool { + self.contains(Self::IS_STANDALONE_MODULE) + } + #[inline] + pub fn is_from_node_modules(&self) -> bool { + self.contains(Self::IS_FROM_NODE_MODULES) + } + #[inline] + pub fn set_is_from_node_modules(&mut self, v: bool) { + self.set(Self::IS_FROM_NODE_MODULES, v) + } + #[inline] + pub fn emit_decorator_metadata(&self) -> bool { + self.contains(Self::EMIT_DECORATOR_METADATA) + } + #[inline] + pub fn set_emit_decorator_metadata(&mut self, v: bool) { + self.set(Self::EMIT_DECORATOR_METADATA, v) + } + #[inline] + pub fn experimental_decorators(&self) -> bool { + self.contains(Self::EXPERIMENTAL_DECORATORS) + } + #[inline] + pub fn set_experimental_decorators(&mut self, v: bool) { + self.set(Self::EXPERIMENTAL_DECORATORS, v) + } +} + +pub enum ResultUnion { + Success(Result), + Failure(bun_core::Error), + Pending(PendingResolution), + NotFound, +} + +impl Result { + /// Read-only view of `package_json`. The field stores `Option<*const _>` + /// (rather than `Option<&'static _>`) so [`Default`] / zeroed-init stays + /// bit-valid; callers that only read go through here. Single deref site + /// for the ARENA-backed pointer — same invariant as + /// [`dir_info::DirInfo::package_json`]. + #[inline] + pub fn package_json_ref(&self) -> Option<&'static PackageJSON> { + Self::deref_package_json(self.package_json) + } + + /// Field-value form of [`package_json_ref`] for sites where `self` is + /// already mutably borrowed (e.g. while iterating `path_pair`). Takes the + /// `Copy` field directly so the borrow checker only sees a field read. + #[inline] + pub fn deref_package_json(ptr: Option<*const PackageJSON>) -> Option<&'static PackageJSON> { + // SAFETY: ARENA — every `*const PackageJSON` stored in + // `Result::package_json` is interned in the resolver's process-lifetime + // PackageJSON cache (or a `'static` fallback-module literal); never + // freed while a `Result` is live (see LIFETIMES.tsv). No + // `&mut PackageJSON` is ever materialized concurrently with a read. + ptr.map(|p| unsafe { &*p }) + } + + pub fn path(&mut self) -> Option<&mut Path> { + if !self.path_pair.primary.is_disabled { + return Some(&mut self.path_pair.primary); + } + + if let Some(second) = self.path_pair.secondary.as_mut() { + if !second.is_disabled { + return Some(second); + } + } + + None + } + + pub fn path_const(&self) -> Option<&Path> { + if !self.path_pair.primary.is_disabled { + return Some(&self.path_pair.primary); + } + + if let Some(second) = self.path_pair.secondary.as_ref() { + if !second.is_disabled { + return Some(second); + } + } + + None + } + + // remember: non-node_modules can have package.json + // checking package.json may not be relevant + pub fn is_likely_node_module(&self) -> bool { + let Some(path_) = self.path_const() else { + return false; + }; + self.flags.is_from_node_modules() + || strings::index_of(path_.text(), b"/node_modules/").is_some() + } + + // Most NPM modules are CommonJS + // If unspecified, assume CommonJS. + // If internal app code, assume ESM. + pub fn should_assume_common_js(&self, kind: ast::ImportKind) -> bool { + match self.module_type { + options::ModuleType::Esm => false, + options::ModuleType::Cjs => true, + _ => { + if kind == ast::ImportKind::Require || kind == ast::ImportKind::RequireResolve { + return true; + } + + // If we rely just on isPackagePath, we mess up tsconfig.json baseUrl paths. + self.is_likely_node_module() + } + } + } + + pub fn hash(&self, _: &[u8], _: options::Loader) -> u32 { + let module = self.path_pair.primary.text(); + // SEP_STR ++ "node_modules" ++ SEP_STR + let node_module_root = const_format::concatcp!(SEP_STR, "node_modules", SEP_STR).as_bytes(); + if let Some(end_) = strings::last_index_of(module, node_module_root) { + let end: usize = end_ + node_module_root.len(); + return bun_wyhash::hash(&module[end..]) as u32; + } + + bun_wyhash::hash(self.path_pair.primary.text()) as u32 + } +} + +pub struct DebugMeta { + pub notes: Vec, + pub suggestion_text: &'static [u8], + pub suggestion_message: &'static [u8], + pub suggestion_range: SuggestionRange, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum SuggestionRange { + Full, + End, +} + +impl DebugMeta { + pub fn init() -> DebugMeta { + DebugMeta { + notes: Vec::new(), + suggestion_text: b"", + suggestion_message: b"", + suggestion_range: SuggestionRange::Full, + } + } + + pub fn log_error_msg( + &mut self, + log: &mut bun_ast::Log, + source: Option<&bun_ast::Source>, + r: bun_ast::Range, + args: core::fmt::Arguments<'_>, + ) -> core::result::Result<(), bun_core::Error> { + // TODO(port): narrow error set + if source.is_some() && !self.suggestion_message.is_empty() { + let suggestion_range = if self.suggestion_range == SuggestionRange::End { + bun_ast::Range { + loc: bun_ast::Loc { + start: r.end_i() as i32 - 1, + }, + ..Default::default() + } + } else { + r + }; + let data = bun_ast::range_data(source, suggestion_range, self.suggestion_message); + // PORT NOTE: Zig spec writes `data.location.?.suggestion = m.suggestion_text` + // here, but `logger.Location` (logger.zig:73) has no `suggestion` field — + // `logErrorMsg` is uncalled in the Zig source so the field access is never + // type-checked under lazy compilation. Mirror the effective behavior (no-op). + let _ = &self.suggestion_text; + self.notes.push(data); + } + + let mut msg_text = Vec::new(); + write!(&mut msg_text, "{}", args).ok(); + log.add_msg(Msg { + kind: bun_ast::Kind::Err, + data: bun_ast::range_data(source, r, msg_text), + notes: core::mem::take(&mut self.notes).into_boxed_slice(), + ..Default::default() + }); + Ok(()) + } +} + +pub struct DirEntryResolveQueueItem { + pub result: allocators::Result, + // PORT NOTE: `RawSlice` (not `&'static [u8]`) — these point into the + // threadlocal `dir_info_uncached_path` buffer and are consumed before + // `dir_info_cached_maybe_log` returns. `RawSlice` is `repr(transparent)` + // over `*const [u8]` so the bit-level zero-init invariant for `Bufs` is + // unchanged (the array slot is `MaybeUninit`-wrapped), and read sites use + // safe `.slice()` instead of an open-coded raw-ptr deref. + pub unsafe_path: bun_ptr::RawSlice, + pub safe_path: bun_ptr::RawSlice, + pub fd: FD, +} + +impl Default for DirEntryResolveQueueItem { + fn default() -> Self { + Self { + result: allocators::Result { + hash: 0, + index: allocators::NOT_FOUND, + status: allocators::Status::Unknown, + }, + unsafe_path: bun_ptr::RawSlice::EMPTY, + safe_path: bun_ptr::RawSlice::EMPTY, + fd: FD::INVALID, + } + } +} + +// `bun_alloc::Result` doesn't derive Clone (yet); all its fields are Copy, so +// hand-roll Clone here for the queue-item move at `dir_info_cached`. +impl Clone for DirEntryResolveQueueItem { + fn clone(&self) -> Self { + Self { + result: allocators::Result { + hash: self.result.hash, + index: self.result.index, + status: self.result.status, + }, + unsafe_path: self.unsafe_path, + safe_path: self.safe_path, + fd: self.fd, + } + } +} + +pub struct DebugLogs { + pub what: Vec, + pub indent: MutableString, + pub notes: Vec, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum FlushMode { + Fail, + Success, +} + +impl DebugLogs { + pub fn init() -> core::result::Result { + let mutable = MutableString::init(0)?; + Ok(DebugLogs { + what: Vec::new(), + indent: mutable, + notes: Vec::new(), + }) + } + + // deinit → Drop (only frees `notes`; `indent` deinit was commented out in Zig) + + #[cold] + pub fn increase_indent(&mut self) { + self.indent.append(b" ").expect("unreachable"); + } + + #[cold] + pub fn decrease_indent(&mut self) { + let new_len = self.indent.list.len() - 1; + self.indent.list.truncate(new_len); + } + + #[cold] + pub fn add_note(&mut self, text: Vec) { + let len = self.indent.len(); + let final_text = if len > 0 { + let mut __text = Vec::with_capacity(text.len() + len); + __text.extend_from_slice(self.indent.list.as_slice()); + __text.extend_from_slice(&text); + // d.notes.allocator.free(_text) — drop(text) is implicit + __text + } else { + text + }; + + self.notes + .push(bun_ast::range_data(None, bun_ast::Range::NONE, final_text)); + } + + #[cold] + pub fn add_note_fmt(&mut self, args: core::fmt::Arguments<'_>) { + let mut buf = Vec::new(); + write!(&mut buf, "{}", args).expect("unreachable"); + self.add_note(buf); + } +} + +pub struct MatchResult { + pub path_pair: PathPair, + pub dirname_fd: FD, + pub file_fd: FD, + pub is_node_module: bool, + pub package_json: Option<*const PackageJSON>, + pub diff_case: Option>, + pub dir_info: Option, + pub module_type: options::ModuleType, + pub is_external: bool, +} + +impl Default for MatchResult { + fn default() -> Self { + Self { + path_pair: PathPair::default(), + dirname_fd: FD::INVALID, + file_fd: FD::INVALID, + is_node_module: false, + package_json: None, + diff_case: None, + dir_info: None, + module_type: options::ModuleType::Unknown, + is_external: false, + } + } +} + +pub enum MatchResultUnion { + NotFound, + Success(MatchResult), + Pending(PendingResolution), + Failure(bun_core::Error), +} + +pub struct PendingResolution { + pub esm: crate::package_json::PackageExternal, + pub dependency: Dependency::Version, + pub resolution_id: Install::PackageID, + pub root_dependency_id: Install::DependencyID, + pub import_record_id: u32, + pub string_buf: Vec, + pub tag: PendingResolutionTag, +} + +impl Default for PendingResolution { + fn default() -> Self { + Self { + esm: Default::default(), + dependency: Default::default(), + resolution_id: Install::INVALID_PACKAGE_ID, + root_dependency_id: Install::INVALID_PACKAGE_ID, + import_record_id: u32::MAX, + string_buf: Vec::new(), + tag: PendingResolutionTag::Download, + } + } +} + +pub type PendingResolutionList = MultiArrayList; + +impl PendingResolution { + // PORT NOTE: deinitListItems → Drop on MultiArrayList + // (Zig body only freed `dependency` + `string_buf` per item; both are owned fields with Drop.) + + // deinit → Drop (frees dependency + string_buf; both have Drop) + + pub fn init( + esm: crate::package_json::Package<'_>, + dependency: Dependency::Version, + resolution_id: Install::PackageID, + ) -> core::result::Result { + // PORT NOTE: Zig body called `try esm.copy(allocator)` and left `string_buf` + // / `tag` defaulted; that fn was never compiled (Zig lazy-analyzes unreferenced + // fns). `Package::copy` is the count→allocate→clone Builder dance the live + // call sites open-code, so thread the freshly-allocated buffer into + // `string_buf` here so `Drop` frees what backs the cloned `esm` strings. + let (esm, string_buf) = esm.copy()?; + Ok(PendingResolution { + esm, + dependency, + resolution_id, + string_buf, + ..PendingResolution::default() + }) + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum PendingResolutionTag { + Download, + Resolve, + Done, +} + +pub struct LoadResult { + pub path: &'static [u8], // TODO(port): lifetime — interned in dirname_store + pub diff_case: Option>, + pub dirname_fd: FD, + pub file_fd: FD, + pub dir_info: Option, +} diff --git a/src/resolver/standalone_module_graph.rs b/src/resolver/standalone_module_graph.rs new file mode 100644 index 00000000000..d166e221737 --- /dev/null +++ b/src/resolver/standalone_module_graph.rs @@ -0,0 +1,30 @@ +//! `StandaloneModuleGraph` — the resolver-side trait abstraction over +//! `bun_standalone_graph::Graph` (which depends on `bun_bundler`). Defining +//! the trait here lets the resolver hold a `dyn` object without depending on +//! the higher-tier crate that implements it. + +/// Resolver's view of a compiled-standalone-binary module graph. The concrete +/// `bun_standalone_graph::Graph` (which depends on `bun_bundler`) implements +/// this; the resolver holds a trait object so it stays below both in the dep +/// graph. The path-prefix predicate lives in +/// `bun_options_types::standalone_path` (MOVE_DOWN) and is callable without a +/// graph instance. +pub trait StandaloneModuleGraph: Send + Sync { + /// Look up `name` (already known to be under the standalone virtual root) + /// and return the embedded file's canonical name slice if present. + fn find_assume_standalone_path(&self, name: &[u8]) -> Option<&[u8]>; + /// Look up `name` (any path — checks the standalone virtual-root prefix + /// first) and return the embedded file's canonical name slice if present. + /// Spec `StandaloneModuleGraph.find`. + fn find(&self, name: &[u8]) -> Option<&[u8]>; + /// `StandaloneModuleGraph.base_public_path_with_default_suffix` — the + /// virtual-root prefix used for embedded modules (e.g. `/$bunfs/root/`). + /// Baked-in `'static` constant; surfaced here so low-tier callers + /// (worker entry-point resolution) don't need the concrete graph type. + fn base_public_path_with_default_suffix(&self) -> &'static [u8]; + /// `StandaloneModuleGraph.compile_exec_argv` — the `--compile-exec-argv` + /// string baked into a `bun build --compile` binary. Exposed via the trait + /// so `process.execArgv` (lower-tier `bun_jsc` callers holding only the + /// trait object) can read it without downcasting to the concrete graph. + fn compile_exec_argv(&self) -> &[u8]; +} diff --git a/src/router/lib.rs b/src/router/lib.rs index 187540ffe8f..2cbd5bd3062 100644 --- a/src/router/lib.rs +++ b/src/router/lib.rs @@ -784,7 +784,7 @@ impl<'a> RouteLoader<'a> { index_id = Some(i); } - // PERF(port): was appendAssumeCapacity — profile in Phase B + // PERF(port): was appendAssumeCapacity — profile if hot // SAFETY: `Route::parse` interned every PathString field via // `DirnameStore::append{,_lower_case}` (process-lifetime arena). let (filepath, match_name, public_path) = unsafe { @@ -1142,8 +1142,8 @@ impl Route { // PORT NOTE: DirnameStore::append returns `&'static [u8]` (process- // lifetime arena), so rebinding here drops the borrow on - // `route_file_buf` and removes the need for the Phase-A - // lifetime transmutes that were below. + // `route_file_buf` and avoids needing lifetime transmutes + // below. let dirname_store = FileSystem::instance().dirname_store(); let public_path: &'static [u8] = dirname_store.append(public_path).expect("unreachable"); @@ -1353,7 +1353,7 @@ pub mod Sorter { Ordering::Equal => match a.kind { // static + dynamic are sorted alphabetically pattern::Tag::Static | pattern::Tag::Dynamic => { - // PERF(port): was @call(bun.callmod_inline, ...) — profile in Phase B + // PERF(port): was @call(bun.callmod_inline, ...) — profile if hot sort_by_name_string(a_name, b_name) } // catch all and optional catch all must appear below dynamic @@ -1382,9 +1382,8 @@ pub mod Sorter { } } -// TODO(port): `impl Route { pub use Sorter }` is not valid Rust; Phase B should make -// `Sorter` an inherent module on `Route` via a wrapper type or move callers to `crate::Sorter`. -// B-1: callers use `crate::Sorter` directly. +// PORT NOTE: Zig nested `Sorter` under `Route`; Rust has no `impl Route { pub use Sorter }` +// equivalent, so callers use `crate::Sorter` directly. struct RouteBufs { route_file_buf: PathBuffer, @@ -1509,8 +1508,9 @@ impl<'a> Match<'a> { // ────────────────────────────────────────────────────────────────────────── // Traits introduced to replace Zig's `comptime T: type` duck-typing -// (Resolver, Server, RequestContext). Phase B should colocate these with -// the canonical types in bun_resolver / bun_runtime. +// (Resolver, Server, RequestContext). +// TODO(refactor): colocate these with the canonical types in +// bun_resolver / bun_runtime. // ────────────────────────────────────────────────────────────────────────── pub trait ResolverLike { @@ -2171,7 +2171,11 @@ mod tests { }; // var resolver = Resolver.init1(default_allocator, &logger, &FileSystem.instance, opts); - let mut resolver = TestResolver(bun_resolver::Resolver::init1(&mut log, fs, opts)); + let mut resolver = TestResolver(bun_resolver::Resolver::init1( + core::ptr::NonNull::from(&mut log), + fs, + opts, + )); // const root_dir = (try resolver.readDirInfo(pages_dir)).?; let root_dir = resolver @@ -2232,7 +2236,11 @@ mod tests { ..Default::default() }; - let mut resolver = TestResolver(bun_resolver::Resolver::init1(&mut log, fs, opts)); + let mut resolver = TestResolver(bun_resolver::Resolver::init1( + core::ptr::NonNull::from(&mut log), + fs, + opts, + )); // const root_dir = (try resolver.readDirInfo(pages_dir)).?; let root_dir = resolver diff --git a/src/runtime/api/filesystem_router.rs b/src/runtime/api/filesystem_router.rs index a4afe390d37..a8185d3bd78 100644 --- a/src/runtime/api/filesystem_router.rs +++ b/src/runtime/api/filesystem_router.rs @@ -96,9 +96,8 @@ bun_jsc::codegen_cached_accessors!("FileSystemRouter"; routes); // R-2 (host-fn re-entrancy): every JS-exposed method takes `&self`; per-field // interior mutability via `JsCell` for the two fields that `reload`/`match` -// mutate. The codegen shim still emits `this: &mut FileSystemRouter` until -// Phase 1 lands — `&mut T` reborrows to `&T` so the impls compile against -// either. +// mutate. The codegen shim may still emit `this: &mut FileSystemRouter` — +// `&mut T` reborrows to `&T` so the impls compile against either. #[bun_jsc::JsClass] pub struct FileSystemRouter { // BACKREF — interned `RefString`s live in the VM cache and outlive this @@ -108,7 +107,7 @@ pub struct FileSystemRouter { // PORT NOTE: Router<'a> only borrows the global FileSystem singleton — `'static` is faithful. pub router: JsCell>, // PERF(port): was arena bulk-free — Router borrows slices from this arena across calls; - // kept as boxed arena per LIFETIMES.tsv (OWNED). Phase B: confirm bumpalo vs ArenaAllocator. + // kept as boxed arena per LIFETIMES.tsv (OWNED). TODO(port): confirm bumpalo vs ArenaAllocator. pub arena: JsCell>, // PORT NOTE: dropped `std.mem.Allocator param` field — it was always `arena.arena()`. pub asset_prefix: Option>, @@ -243,7 +242,7 @@ impl FileSystemRouter { let vm_ptr = VirtualMachine::get_mut_ptr(); Resolver::scoped_log( core::ptr::addr_of_mut!((*vm_ptr).transpiler.resolver), - &raw mut log, + core::ptr::NonNull::from(&mut log), ) }; @@ -462,7 +461,7 @@ impl FileSystemRouter { let _restore_log = unsafe { Resolver::scoped_log( core::ptr::addr_of_mut!((*vm_ptr).transpiler.resolver), - &raw mut log, + core::ptr::NonNull::from(&mut log), ) }; diff --git a/src/runtime/bake/production.rs b/src/runtime/bake/production.rs index 07fa3c8421c..7105577d413 100644 --- a/src/runtime/bake/production.rs +++ b/src/runtime/bake/production.rs @@ -136,7 +136,7 @@ pub fn build_command(ctx: Context) -> Result<(), bun_core::Error> { let b = &mut vm.transpiler; // TODO(port): preload/argv are `Vec>` on both sides; clone since // ctx outlives vm but Zig assigned slices directly (no ownership transfer). - // Phase B may change VM fields to borrow from ctx. + // Could change VM fields to borrow from ctx. vm.preload = ctx.preloads.clone(); vm.argv = ctx.passthrough.clone(); vm.arena = NonNull::new(&raw mut arena); @@ -146,8 +146,7 @@ pub fn build_command(ctx: Context) -> Result<(), bun_core::Error> { // `Option>`, so no lifetime-extension cast is needed. let install_ptr = ctx.install.as_deref().map(NonNull::from); b.options.install = install_ptr; - b.resolver.opts.install = - install_ptr.map_or(core::ptr::null(), |p| p.as_ptr() as *const ()); + b.resolver.opts.install = install_ptr; b.resolver.opts.global_cache = ctx.debug.global_cache; b.resolver.opts.prefer_offline_install = ctx .debug diff --git a/src/runtime/cli/repl_command.rs b/src/runtime/cli/repl_command.rs index fefeef9b11e..38fb951ca6f 100644 --- a/src/runtime/cli/repl_command.rs +++ b/src/runtime/cli/repl_command.rs @@ -66,7 +66,7 @@ impl ReplCommand { // TODO(port): arena is threaded into VirtualMachine (vm.arena / vm.allocator). Non-AST // crate would normally drop MimallocArena, but VM init protocol requires it. Note // `bun_alloc::Arena` is bumpalo-backed and NOT semantically `bun.allocators.MimallocArena` - // (mi_heap wrapper) — Phase B should either have bun_jsc::VirtualMachine own its arena + // (mi_heap wrapper) — TODO(refactor): either have bun_jsc::VirtualMachine own its arena // internally (drop the param) or expose a distinct `bun_alloc::MimallocArena` type. let arena = Arena::new(); @@ -109,10 +109,7 @@ impl ReplCommand { // lifetime-extension cast is needed. let install_ptr = ctx.install.as_deref().map(core::ptr::NonNull::from); b.options.install = install_ptr; - // resolver's `BundleOptions.install` is the FORWARD_DECL `*const ()` - // (breaks the bun_install dep cycle) — erase the type. - b.resolver.opts.install = - install_ptr.map_or(core::ptr::null(), |p| p.as_ptr() as *const ()); + b.resolver.opts.install = install_ptr; b.resolver.opts.global_cache = ctx.debug.global_cache; b.resolver.opts.prefer_offline_install = ctx .debug @@ -167,7 +164,7 @@ impl ReplCommand { // TODO(port): @constCast(&arena) — vm.arena stores a *mut Arena pointing at runner.arena; // lifetime is the holdAPILock scope (globalExit() never returns so the frame never unwinds). // Assigned AFTER moving `arena` into `runner` — assigning from the pre-move local would - // dangle. Model as raw ptr until VM arena ownership is decided in Phase B. + // dangle. Model as raw ptr until VM arena ownership is decided. unsafe { (*vm).arena = NonNull::new(&raw mut runner.arena) }; // PORT NOTE: jsc.OpaqueWrap(ReplRunner, ReplRunner.start) — comptime fn-ptr wrapper that diff --git a/src/runtime/cli/run_command.rs b/src/runtime/cli/run_command.rs index 03a0613055e..421bac19be6 100644 --- a/src/runtime/cli/run_command.rs +++ b/src/runtime/cli/run_command.rs @@ -104,8 +104,8 @@ impl RunCommand { pub fn print_help(package_json: Option<&PackageJSON>) { // PORT NOTE: templates are passed as *string literals* so the // `pretty_fmt!` proc-macro rewrites the `` color markup at compile - // time. Routing them through a `const &str` + `{}` (as the original - // Phase-A draft did) prints the raw ``/`` tags verbatim. + // time. Routing them through a `const &str` + `{}` prints the raw + // ``/`` tags verbatim. pretty!("Usage: bun run [flags] \\\n\n"); pretty!("Flags:"); bun_clap::simple_help(crate::cli::arguments::RUN_PARAMS); @@ -552,7 +552,10 @@ Full documentation is available at https://bun.com/docs/cli/run /// not share `.text` pages with the hot `bun run