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
4 changes: 4 additions & 0 deletions src/bundler/Graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ pub struct HtmlImports {

pub struct InputFile {
pub(crate) source: bun_ast::Source,
/// The `?query` of the import that made this module. `PathToSourceIndexMap` has it in the key.
pub ignored_suffix: &'static [u8],
pub(crate) secondary_path: AstVec<u8>,
pub(crate) loader: options::Loader,
pub side_effects: SideEffects,
Expand All @@ -118,6 +120,7 @@ impl Default for InputFile {
fn default() -> Self {
Self {
source: bun_ast::Source::default(),
ignored_suffix: b"",
secondary_path: AstAlloc::vec(),
loader: options::Loader::default(),
side_effects: SideEffects::default(),
Expand All @@ -135,6 +138,7 @@ impl Default for InputFile {
bun_collections::multi_array_columns! {
pub trait InputFileColumns for InputFile {
source: bun_ast::Source,
ignored_suffix: &'static [u8],
secondary_path: AstVec<u8>,
loader: options::Loader,
side_effects: SideEffects,
Expand Down
8 changes: 8 additions & 0 deletions src/bundler/LinkerContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1881,6 +1881,9 @@ impl<'a> LinkerContext<'a> {
if let crate::chunk::Content::Javascript(js) = &chunk.content {
// SAFETY: parse_graph backref; exclusive access via &mut *.
let sources = unsafe { (*self.parse_graph).input_files.items_source_mut() };
// SAFETY: same backref; a different column from `sources`.
let ignored_suffixes =
unsafe { (*self.parse_graph).input_files.items_ignored_suffix() };
for part_range in js.parts_in_chunk_in_order.iter() {
let source: &mut Source = &mut sources[part_range.source_index.get() as usize];

Expand Down Expand Up @@ -1913,6 +1916,11 @@ impl<'a> LinkerContext<'a> {

// Then include the file path
hasher.write(file_path);
// `write` hashes the length too: no write for no suffix keeps the hashes as they were.
let ignored_suffix = ignored_suffixes[part_range.source_index.get() as usize];
if !ignored_suffix.is_empty() {
hasher.write(ignored_suffix);
}

// Then include the part range
hasher.write_ints(&[part_range.part_index_begin, part_range.part_index_end]);
Expand Down
5 changes: 5 additions & 0 deletions src/bundler/ParseTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ pub struct ParseTask {
// lifetime-erased `'static` — paths borrow from `DirnameStore`
// (process-lifetime BSS string pool); see `bun_resolver::fs::Path<'a>`.
pub(crate) path: Fs::Path<'static>,
/// See `InputFile::ignored_suffix`.
pub(crate) ignored_suffix: &'static [u8],
pub(crate) secondary_path_for_commonjs_interop: Option<Fs::Path<'static>>,
pub(crate) contents_or_fd: ContentsOrFd,
pub(crate) external_free_function: ExternalFreeFunction,
Expand Down Expand Up @@ -276,6 +278,7 @@ impl ParseTask {
package_name,
known_target,
// defaults:
ignored_suffix: b"",
secondary_path_for_commonjs_interop: None,
external_free_function: ExternalFreeFunction::NONE,
loader: None,
Expand Down Expand Up @@ -305,6 +308,7 @@ impl Default for ParseTask {
ParseTask {
ctx: None,
path: Fs::Path::init(b""),
ignored_suffix: b"",
secondary_path_for_commonjs_interop: None,
contents_or_fd: ContentsOrFd::Contents(b""),
external_free_function: ExternalFreeFunction::NONE,
Expand Down Expand Up @@ -590,6 +594,7 @@ pub mod parse_worker {
loader: Some(Loader::Js),
known_target: target,
// defaults:
ignored_suffix: b"",
secondary_path_for_commonjs_interop: None,
external_free_function: ExternalFreeFunction::NONE,
task: ThreadPoolLib::Task {
Expand Down
203 changes: 152 additions & 51 deletions src/bundler/bundle_v2.rs

Large diffs are not rendered by default.

44 changes: 29 additions & 15 deletions src/bundler/linker_context/MetafileBuilder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ pub(crate) fn generate_chunk_json(

let parse_graph = c.parse_graph();
let sources = parse_graph.input_files.items_source();
let ignored_suffixes = parse_graph.input_files.items_ignored_suffix();

// Start chunk entry: "path/to/output.js": {
write_json_string(&mut json, &chunk.final_rel_path)?;
Expand Down Expand Up @@ -109,7 +110,11 @@ pub(crate) fn generate_chunk_json(
first_chunk_input = false;

json.extend_from_slice(b"\n ");
write_json_string(&mut json, file_path)?;
write_input_path(
&mut json,
file_path,
ignored_suffixes[file_source_index as usize],
)?;
write!(
json,
": {{\n \"bytesInOutput\": {}\n }}",
Expand Down Expand Up @@ -173,7 +178,11 @@ pub(crate) fn generate_chunk_json(
let entry_source = &sources[entry_source_index as usize];
if !entry_source.path.text.is_empty() && !entry_source.path.pretty.is_empty() {
json.extend_from_slice(b",\n \"entryPoint\": ");
write_json_string(&mut json, entry_source.path.pretty)?;
write_input_path(
&mut json,
entry_source.path.pretty,
ignored_suffixes[entry_source_index as usize],
)?;
}
}
}
Expand Down Expand Up @@ -214,6 +223,7 @@ pub(crate) fn generate(c: &mut LinkerContext, chunks: &mut [Chunk]) -> crate::Re
let mut first_input = true;
let parse_graph = c.parse_graph();
let sources = parse_graph.input_files.items_source();
let ignored_suffixes = parse_graph.input_files.items_ignored_suffix();
let loaders = parse_graph.input_files.items_loader();
let import_records_list = parse_graph.ast.items_import_records();

Expand Down Expand Up @@ -272,11 +282,7 @@ pub(crate) fn generate(c: &mut LinkerContext, chunks: &mut [Chunk]) -> crate::Re
j.push_static(b"\n ");
{
let mut buf: Vec<u8> = Vec::new();
write!(
buf,
"{}",
bfmt::format_json_string_utf8(path, Default::default())
)?;
write_input_path(&mut buf, path, ignored_suffixes[source_index as usize])?;
j.push_owned(buf.into_boxed_slice());
}
{
Expand Down Expand Up @@ -304,27 +310,23 @@ pub(crate) fn generate(c: &mut LinkerContext, chunks: &mut [Chunk]) -> crate::Re
// Bundled imports use the target source's pretty path (same string as the
// "inputs" key). `record.path.text` is unreliable here: dedup can set
// `source_index` without rewriting the path. Externals/chunk refs fall through.
let import_path: &[u8] = 'path: {
let (import_path, import_suffix): (&[u8], &[u8]) = 'path: {
if record.source_index.is_valid()
&& record.source_index.get() != Index::RUNTIME.get()
{
let idx = record.source_index.get() as usize;
if idx < sources.len() {
let pretty = sources[idx].path.pretty;
if !pretty.is_empty() {
break 'path pretty;
break 'path (pretty, ignored_suffixes[idx]);
}
}
}
record.path.text
(record.path.text, b"")
};
{
let mut buf: Vec<u8> = Vec::new();
write!(
buf,
"{}",
bfmt::format_json_string_utf8(import_path, Default::default())
)?;
write_input_path(&mut buf, import_path, import_suffix)?;
j.push_owned(buf.into_boxed_slice());
}
j.push_static(b",\n \"kind\": \"");
Expand Down Expand Up @@ -470,6 +472,18 @@ fn write_json_string(writer: &mut impl Write, str: &[u8]) -> std::io::Result<()>
)
}

/// `pretty` plus the `?query` of the import that made the input, like esbuild.
fn write_input_path(
writer: &mut impl Write,
pretty: &[u8],
ignored_suffix: &[u8],
) -> std::io::Result<()> {
if ignored_suffix.is_empty() {
return write_json_string(writer, pretty);
}
write_json_string(writer, &[pretty, ignored_suffix].concat())
}

// ──────────────────────────────────────────────────────────────────────────
// Minimal dynamic JSON value tree for `generate_markdown`.
//
Expand Down
22 changes: 14 additions & 8 deletions src/runtime/cli/test/ChangedFilesFilter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ pub(crate) fn filter<'a>(
};

let sources = bundle.graph.input_files.items_source();
let ignored_suffixes = bundle.graph.input_files.items_ignored_suffix();
let import_records = bundle.graph.ast.items_import_records();

// Map absolute source path -> source index for paths that participate in
Expand Down Expand Up @@ -221,9 +222,17 @@ pub(crate) fn filter<'a>(
if !source.path.is_file() {
continue;
}
// A file that is also imported with a `?query` is more than one source.
let seen = path_to_index.contains_key(path_text);
if seen && !ignored_suffixes[idx].is_empty() {
continue;
}
// 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());
if seen {
continue;
}
// 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 Down Expand Up @@ -256,14 +265,11 @@ pub(crate) fn filter<'a>(
let mut affected = bun_core::handle_oom(DynamicBitSet::init_empty(sources.len()));
let mut queue: Vec<u32> = Vec::new();

{
for changed_path in changed_files.keys() {
if let Some(&idx) = path_to_index.get(changed_path.as_ref()) {
if !affected.is_set(idx as usize) {
affected.set(idx as usize);
queue.push(idx);
}
}
// Not through `path_to_index`: a file that is also imported with a `?query` is two sources.
for (idx, source) in sources.iter().enumerate() {
if source.path.is_file() && changed_files.contains(source.path.text) {
affected.set(idx);
queue.push(u32::try_from(idx).unwrap());
}
}

Expand Down
31 changes: 31 additions & 0 deletions test/bundler/bundler_files.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,37 @@ describe("bundler files option", () => {
expect(output).toContain('"b"');
});

test("in-memory file imported with a query suffix", async () => {
const result = await Bun.build({
entrypoints: ["/entry.js"],
target: "bun",
files: {
"/entry.js": `
import source from "./lib.js?raw";
import { value } from "./lib.js?v=1";
console.log(JSON.stringify([source, value]));
`,
"/lib.js": `export const value = 42;`,
},
});

expect(result.success).toBe(true);
expect(result.outputs.length).toBe(1);

using dir = tempDir("bundler-files-query-suffix", {});
await Bun.write(`${dir}/out.js`, result.outputs[0]);
await using proc = Bun.spawn({
cmd: [bunExe(), `${dir}/out.js`],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toBe("");
expect(stdout).toBe('["export const value = 42;",42]\n');
expect(exitCode).toBe(0);
});

test("in-memory file overrides real file on disk", async () => {
// Create a temp directory with a real file
using dir = tempDir("bundler-files-override", {
Expand Down
Loading
Loading