Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/ast/import_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,18 @@ bitflags::bitflags! {
/// `IS_UNUSED` for its own reasons (an unused TypeScript import, a macro
/// import), and those records must never be resolved.
const IS_BARREL_DEFERRED = 1 << 18;

/// Scan phase: `path` is the resolved module's path, not a specifier. See `set_module_path`.
const PATH_IS_MODULE = 1 << 22;
}
}

impl<'a> ImportRecord<'a> {
/// Store the resolved module's path. The bundler's path map pass links only such records.
#[inline]
pub fn set_module_path(&mut self, path: Path<'a>) {
self.path = path;
self.flags.insert(Flags::PATH_IS_MODULE);
}
}

Expand Down
26 changes: 8 additions & 18 deletions src/bundler/PathToSourceIndexMap.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,8 @@
use bun_ast::{ImportRecord, ImportRecordFlags};
use bun_collections::StringHashMap;

use crate::IndexStringMap::IndexInt;

/// Abstracts over the two structurally-identical `Path` ports (`bun_paths::fs::Path`
/// and `bun_resolver::fs::Path`) so the bundler can key the map with either while
/// the crates converge. Both expose `.text: &[u8]`, which is all we need.
pub trait PathLike {
fn path_text(&self) -> &[u8];
}

// `bun_resolver::fs::Path` is now a re-export of `bun_paths::fs::Path` (D090),
// so a single impl covers both.
impl PathLike for bun_paths::fs::Path<'_> {
#[inline]
fn path_text(&self) -> &[u8] {
self.text
}
}

/// The lifetime of the keys are not owned by this map.
///
/// We assume it's arena allocated.
Expand All @@ -33,8 +18,13 @@ pub type Map = StringHashMap<IndexInt>;
pub(crate) type GetOrPutResult<'a> = bun_collections::string_hash_map::GetOrPutResult<'a, IndexInt>;

impl PathToSourceIndexMap {
pub(crate) fn get_path(&self, path: &impl PathLike) -> Option<IndexInt> {
self.get(path.path_text())
/// Without `PATH_IS_MODULE` the path is a specifier, and a module can have the same text.
#[inline]
pub(crate) fn get_record(&self, record: &ImportRecord) -> Option<IndexInt> {
if !record.flags.contains(ImportRecordFlags::PATH_IS_MODULE) {
return None;
}
self.get(record.path.text)
}

pub(crate) fn get(&self, text: impl AsRef<[u8]>) -> Option<IndexInt> {
Expand Down
73 changes: 30 additions & 43 deletions src/bundler/barrel_imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,22 @@ fn record_target(
if rec.source_index.is_valid() {
return Some(rec.source_index.get());
}
map?.get_path(&rec.path)
map?.get_record(rec)
}

/// The surviving record of an HMR duplicate import (`dedup_fallback`), else `ir` itself.
#[inline]
fn surviving_record<'a>(
records: &'a [bun_ast::ImportRecord<'a>],
dedup_fallback: &StringArrayHashMap<u32>,
ir: &'a bun_ast::ImportRecord<'a>,
) -> &'a bun_ast::ImportRecord<'a> {
if ir.flags.contains(import_record::Flags::IS_UNUSED) {
if let Some(&survivor) = dedup_fallback.get(ir.path.text) {
return &records[survivor as usize];
}
}
ir
}

/// The export names the result of `import()` / `require()` record `idx` can
Expand Down Expand Up @@ -486,11 +501,10 @@ pub(crate) fn schedule_barrel_deferred_imports(
// surviving record's path.text becomes the resolved absolute path.
// named_imports entries created for the dedup'd record still point at
// its index, so the direct path lookup below fails for those entries.
// Build a fallback: raw specifier → surviving record's resolved path
// text, using non-unused records in this file. See #28886.
let mut dedup_fallback: StringArrayHashMap<&[u8]> = StringArrayHashMap::default();
// Build a fallback: raw specifier → index of the surviving record. See #28886.
let mut dedup_fallback: StringArrayHashMap<u32> = StringArrayHashMap::default();
if dev_handle.is_some() {
for ir_probe in file_import_records.as_slice() {
for (idx, ir_probe) in file_import_records.as_slice().iter().enumerate() {
if ir_probe.flags.contains(import_record::Flags::IS_UNUSED)
|| ir_probe.flags.contains(import_record::Flags::IS_INTERNAL)
{
Expand All @@ -499,10 +513,14 @@ pub(crate) fn schedule_barrel_deferred_imports(
if ir_probe.original_path.is_empty() {
continue;
}
if ir_probe.original_path == ir_probe.path.text {
if !ir_probe.source_index.is_valid()
&& !ir_probe
.flags
.contains(import_record::Flags::PATH_IS_MODULE)
{
continue;
}
dedup_fallback.put(ir_probe.original_path, ir_probe.path.text)?;
dedup_fallback.put(ir_probe.original_path, idx as u32)?;
}
}

Expand All @@ -522,25 +540,8 @@ pub(crate) fn schedule_barrel_deferred_imports(
// path map as a read-only fallback. Do NOT write back to the import
// record — the dev server intentionally leaves source_indices unset
// and other code (IncrementalGraph, printer) depends on that.
// For dedup'd HMR records (is_unused), fall back to a sibling's
// resolved path text since the record itself still has the raw
// specifier in path.text.
let resolved_path_text = if ir.flags.contains(import_record::Flags::IS_UNUSED) {
dedup_fallback
.get(ir.path.text)
.copied()
.unwrap_or(ir.path.text)
} else {
ir.path.text
};
let target = if ir.source_index.is_valid() {
ir.source_index.get()
} else if let Some(map) = path_to_source_index_map {
match map.get().get(resolved_path_text) {
Some(t) => t,
None => continue,
}
} else {
let ir = surviving_record(file_import_records.as_slice(), &dedup_fallback, ir);
let Some(target) = record_target(ir, path_to_source_index_map) else {
continue;
};

Expand All @@ -558,7 +559,7 @@ pub(crate) fn schedule_barrel_deferred_imports(
}
// Persist the export request on DevServer so it survives across builds.
if let Some(dev) = dev_handle {
persist_barrel_export(&dev, resolved_path_text, alias);
persist_barrel_export(&dev, ir.path.text, alias);
Comment thread
robobun marked this conversation as resolved.
Resolved
}
}
}
Expand Down Expand Up @@ -618,22 +619,8 @@ pub(crate) fn schedule_barrel_deferred_imports(
if matches!(ir.kind, ImportKind::Require | ImportKind::Dynamic) {
continue;
}
let resolved_path_text = if ir.flags.contains(import_record::Flags::IS_UNUSED) {
dedup_fallback
.get(ir.path.text)
.copied()
.unwrap_or(ir.path.text)
} else {
ir.path.text
};
let ir_target = if ir.source_index.is_valid() {
ir.source_index.get()
} else if let Some(map) = path_to_source_index_map {
match map.get().get(resolved_path_text) {
Some(t) => t,
None => continue,
}
} else {
let ir = surviving_record(file_import_records.as_slice(), &dedup_fallback, ir);
let Some(ir_target) = record_target(ir, path_to_source_index_map) else {
continue;
};

Expand Down
41 changes: 26 additions & 15 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5928,6 +5928,10 @@ pub mod bv2_impl {
(*parts_col.add(record.source_index.get() as usize)).len()
} == 0
{
// The dev server files this failure under `path.text` in every namespace.
record.set_module_path(Fs::Path::init(
sources[record.source_index.get() as usize].path.text,
));
record.source_index = Index::INVALID;
// The importer isn't bundled again when the stylesheet is fixed, so its
// output must treat this import the same as a stylesheet that didn't fail.
Expand Down Expand Up @@ -6790,13 +6794,14 @@ pub mod bv2_impl {
resolve_queue.get_or_put(path_primary.text).expect("oom");
if resolve_entry.found_existing {
// SAFETY: arena-allocated `ParseTask` stored in the queue; arena outlives the pass.
import_record.path = unsafe { &**resolve_entry.value_ptr }.path;
import_record
.set_module_path(unsafe { &**resolve_entry.value_ptr }.path);
continue;
}

// For virtual files, use the path text as-is (no relative path computation needed).
path_primary.pretty = self.graph.heap.alloc_slice_copy(path_primary.text);
import_record.path = path_primary;
import_record.set_module_path(path_primary);
let _ = path_primary.text; // key already interned by get_or_put
bun_core::scoped_log!(
Bundle,
Expand Down Expand Up @@ -7099,9 +7104,7 @@ pub mod bv2_impl {
{
// Overload `path.text` to point to the final URL
// This information cannot be queried while printing because a lock wouldn't get held.
import_record.path.text = path.text;
import_record.path.namespace = b"file";
import_record.path.pretty = self
let pretty = self
.arena()
.alloc_str(&format!(
"{}/{}{}",
Expand All @@ -7112,13 +7115,20 @@ pub mod bv2_impl {
bstr::BStr::new(bun_paths::extension(path.text)),
))
.as_bytes();
import_record.path.is_disabled = false;
import_record.set_module_path(Fs::Path {
text: path.text,
namespace: b"file",
pretty,
is_disabled: false,
..import_record.path
});
} else {
import_record.path.text = path.text;
import_record.path.pretty = rel;
import_record.path = self
.path_with_pretty_initialized(path, target)
.expect("oom");
import_record.set_module_path(
self.path_with_pretty_initialized(path, target)
.expect("oom"),
);
if loader == Loader::Html
|| entry.kind == bake_types::CacheKind::Css
{
Expand Down Expand Up @@ -7159,8 +7169,9 @@ pub mod bv2_impl {

if let Some(id) = self.path_to_source_index_map(target).get(path.text) {
if self.dev_server.is_some() && loader != Loader::Html {
import_record.path =
self.graph.input_files.items_source()[id as usize].path;
import_record.set_module_path(
self.graph.input_files.items_source()[id as usize].path,
);
} else {
import_record.source_index = Index::init(id);
}
Expand All @@ -7174,15 +7185,15 @@ pub mod bv2_impl {
let resolve_entry = resolve_queue.get_or_put(path.text).expect("oom");
if resolve_entry.found_existing {
// SAFETY: arena-allocated `ParseTask` stored in the queue; arena outlives the pass.
import_record.path = unsafe { &**resolve_entry.value_ptr }.path;
import_record.set_module_path(unsafe { &**resolve_entry.value_ptr }.path);
continue;
}

*path = self
.path_with_pretty_initialized(path, target)
.expect("oom");

import_record.path = *path;
import_record.set_module_path(*path);
// key already interned by get_or_put — no key_ptr on StringHashMapGetOrPut
bun_core::scoped_log!(Bundle, "created ParseTask: {}", bstr::BStr::new(&path.text));
// Arena-owned.
Expand Down Expand Up @@ -7420,7 +7431,7 @@ pub mod bv2_impl {
if !only_selected_record(ctx.only_records, i) {
continue;
}
if let Some(source_index) = path_to_source_index_map.get_path(&record.path) {
if let Some(source_index) = path_to_source_index_map.get_record(record) {
if save_import_record_source_index
|| input_file_loaders[source_index as usize].is_css()
{
Expand Down Expand Up @@ -7763,7 +7774,7 @@ pub mod bv2_impl {
let resolved_index = if star_ir.source_index.is_valid() {
star_ir.source_index.get()
} else if let Some(idx) =
this.graph.build_graphs[result_ast_target].get_path(&star_ir.path)
this.graph.build_graphs[result_ast_target].get_record(star_ir)
{
idx
} else {
Expand Down
16 changes: 12 additions & 4 deletions src/runtime/bake/dev_server/incremental_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1034,10 +1034,18 @@ impl<const SIDE: bake::Side> IncrementalGraph<SIDE> {
// Invalid source indices in CSS are external URLs.
EdgeAttachmentMode::Css => return Ok(EdgeAttachmentResult::Stop),
// Check IncrementalGraph for a file from a prior build.
EdgeAttachmentMode::JsOrHtml => match self.bundled_files.get_index(key) {
Some(i) => FileIndex::<SIDE>::init(i as u32),
None => return Ok(EdgeAttachmentResult::Continue),
},
EdgeAttachmentMode::JsOrHtml => {
// Not linked and not pointed at a module: `key` is a specifier, not a file.
if !ir_source_index.is_valid()
&& !ir_flags.contains(bun_ast::ImportRecordFlags::PATH_IS_MODULE)
{
return Ok(EdgeAttachmentResult::Continue);
}
match self.bundled_files.get_index(key) {
Some(i) => FileIndex::<SIDE>::init(i as u32),
None => return Ok(EdgeAttachmentResult::Continue),
}
}
}
};

Expand Down
7 changes: 7 additions & 0 deletions src/runtime/bake/dev_server/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,13 @@ impl DevServer {
continue; // ignore non-JavaScript imports
}
} else {
// Without `PATH_IS_MODULE` the path is a specifier, not an in-graph file.
if !import
.flags
.contains(bun_ast::ImportRecordFlags::PATH_IS_MODULE)
{
continue;
}
// Find the in-graph import.
let Some(file) = self.client_graph.bundled_files.get(import.path.text) else {
continue;
Expand Down
Loading
Loading