Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
14 changes: 14 additions & 0 deletions src/bundler/Graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,20 @@ impl<'a> Graph<'a> {
}
}

impl Graph<'_> {
/// The page of a server-side HTML import, looked up by the path of its stand-in source.
pub(crate) fn html_import_page(&self, stand_in: IndexInt) -> Option<IndexInt> {
let path = &self.input_files.items_source()[stand_in as usize].path;
self.build_graphs[options::Target::Browser].get_path(path)
}

/// `(stand-in, page)` for each server-side HTML import whose page is in the graph.
pub fn html_import_pages(&self) -> impl Iterator<Item = (IndexInt, IndexInt)> + '_ {
let stand_ins = self.html_imports.server_source_indices.iter();
stand_ins.filter_map(|&stand_in| Some((stand_in, self.html_import_page(stand_in)?)))
}
}

// The resolver
// crate re-exports the canonical enum from `bun_options_types`; re-export it
// here so `InputFile` and the derived `items_side_effects()` SoA accessor share
Expand Down
22 changes: 8 additions & 14 deletions src/bundler/LinkerContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -746,20 +746,14 @@ impl<'a> LinkerContext<'a> {
// bounded by `server_len`.
let html_import: u32 =
unsafe { (*parse_graph).html_imports.server_source_indices.slice()[i] };
// SAFETY: `input_files` SoA is append-only; read-only here.
let path_text = unsafe {
&(*parse_graph).input_files.items_source()[html_import as usize]
.path
.text
};
// SAFETY: sole `&mut` into the per-target map for this lookup.
let source_index: u32 = unsafe {
(*parse_graph).path_to_source_index_map(Target::Browser)
}
.get(path_text)
.unwrap_or_else(|| {
panic!("Assertion failed: HTML import file not found in pathToSourceIndexMap");
});
let source_index: u32 = self
.parse_graph()
.html_import_page(html_import)
.unwrap_or_else(|| {
panic!(
"Assertion failed: HTML import file not found in pathToSourceIndexMap"
);
});

// SAFETY: sole `&mut` into `html_source_indices` for this push.
unsafe {
Expand Down
46 changes: 42 additions & 4 deletions src/runtime/cli/test/ChangedFilesFilter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
//! graph (transitive imports) without linking or emitting code.
//! 3. Starting from each changed file that appears in the graph, walk the
//! reverse import edges to find every test entry point that can reach it.
//! The graph can hold a file more than once. Every source of a changed file starts the walk.
//!
//! Only those test entry points are returned.

Expand Down Expand Up @@ -192,10 +193,11 @@ pub(crate) fn filter<'a>(
let sources = bundle.graph.input_files.items_source();
let import_records = bundle.graph.ast.items_import_records();

// Map absolute source path -> source index for paths that participate in
// the graph. This lets us look up changed-file paths quickly.
// Absolute source path -> first source index of that path. The keys borrow from `sources`.
let mut path_to_index: StringHashMap<u32> = StringHashMap::new();
path_to_index.reserve(sources.len());
// (first, later) source of one path: the scan holds a file once per target.
let mut copies: Vec<(u32, u32)> = Vec::new();

// Reverse graph: for each source index, the list of source indexes that
// import it. Built once, then used for a backward BFS from every changed
Expand All @@ -211,7 +213,7 @@ pub(crate) fn filter<'a>(
if index.is_runtime() {
continue;
}
let path_text: &[u8] = source.path.text;
let path_text: &'static [u8] = source.path.text;
if path_text.is_empty() {
continue;
}
Expand All @@ -222,7 +224,12 @@ pub(crate) fn filter<'a>(
}
// All scanned entry points are absolute, and the resolver emits
// absolute file paths as well.
path_to_index.put_assume_capacity(path_text, u32::try_from(idx).unwrap());
let hash = path_to_index.hash_key(path_text);
if let Some(&first) = path_to_index.get_hashed(hash, path_text) {
copies.push((first, index.get()));
continue;
Comment thread
robobun marked this conversation as resolved.
Resolved
}
bun_core::handle_oom(path_to_index.put_static_key_hashed(hash, path_text, index.get()));
// Copy out of the bundler's arena so the caller can use these paths
// after the BundleV2 heap is gone.
graph_files.push(Box::<[u8]>::from(path_text));
Expand All @@ -241,6 +248,23 @@ pub(crate) fn filter<'a>(
importers[dep.get() as usize].push(importer);
}
}
Comment thread
robobun marked this conversation as resolved.
Resolved
for (stand_in, page) in bundle.graph.html_import_pages() {
// Only an HTML import, or a script that re-exports one, loads the page: `with { type: "text" }` reads the file.
let imports_the_page = |record: &bun_ast::ImportRecord| {
record.source_index.get() == stand_in
&& record.loader.is_none_or(|loader| {
loader == bun_ast::Loader::Html || loader.is_javascript_like()
})
};
Comment thread
robobun marked this conversation as resolved.
Resolved
// An HTML import's record points at a stand-in without import records: link its importer to the page.
for i in 0..importers[stand_in as usize].len() {
let importer = importers[stand_in as usize][i];
let records = import_records[importer as usize].as_slice();
if records.iter().any(imports_the_page) {
importers[page as usize].push(importer);
}
}
}

// Map the original test_files slot -> bundler source index. An entry
// point that failed to resolve is skipped by enqueueEntryPoints, so
Expand All @@ -264,7 +288,15 @@ pub(crate) fn filter<'a>(
}
}
}
for &(first, later) in &copies {
if affected.is_set(first as usize) {
affected.set(later as usize);
queue.push(later);
}
}
}
// Path text is only valid for the bundle pass: drop the borrowed keys before the teardown.
drop(path_to_index);

while let Some(idx) = queue.pop() {
for &importer in &importers[idx as usize] {
Expand All @@ -275,6 +307,12 @@ pub(crate) fn filter<'a>(
queue.push(importer);
}
}
for &(first, later) in &copies {
// A test file maps to the first source of its path, and any source of that path selects it.
if affected.is_set(later as usize) {
affected.set(first as usize);
}
}

// A test file is selected if (a) its entry point source index is marked
// affected, or (b) the test file itself is in the changed set (covers
Expand Down
Loading
Loading