Skip to content
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
31e2ddc
bundler: chain inline input sourcemaps through to output
robobun May 12, 2026
ee030e8
[autofix.ci] apply automated fixes
autofix-ci[bot] May 12, 2026
87f50bc
test: cover plugin onLoad with inline sourcemap (#6173)
robobun May 12, 2026
e84d5c4
address review findings: OOM, leak, test guards
robobun May 12, 2026
ac7b108
sourcemap: anchor findSourceMappingURL to the final line
robobun May 12, 2026
9eca1d4
address 3 review findings from claude[bot]
robobun May 12, 2026
47757dc
ci: retrigger
robobun May 12, 2026
a10f2b2
test: gate chained-sourcemap tests behind describe.todo
robobun May 16, 2026
02b8ec8
bundler: chain inline input sourcemaps through to output (Rust port)
robobun May 16, 2026
35cbe17
[autofix.ci] apply automated fixes
autofix-ci[bot] May 16, 2026
4983e19
sourcemap: trim whitespace on both sides of the URL after sourceMappi…
robobun May 19, 2026
6dd5c39
bundler: init Graph::InputFile.input_source_map in Default impl
robobun May 20, 2026
2b61ec7
sourcemap: drop unused Mapping import from InputSourceMap.rs
robobun May 20, 2026
f5187ac
bundler: hoist source_map option before get_ast to avoid stacked-borr…
robobun May 20, 2026
1ae23e1
sourcemap: drop redundant mut on estr bindings (-D unused-mut)
robobun Jun 2, 2026
8266423
sourcemap: hint find_line_with_hint from intermediate line, not remapped
robobun Jun 2, 2026
60af998
[autofix.ci] apply automated fixes
autofix-ci[bot] Jun 2, 2026
5b10b6e
sourcemap: align comments with main's port-note cleanup
robobun Jun 6, 2026
ee552a7
sourcemap: route write_sources_for paths through source_map_relative_…
robobun Jun 27, 2026
cfe8ba5
sourcemap: use Number::value() accessor (field now private)
robobun Jun 27, 2026
b83f1e7
bundler: cap inline-sourcemap source names; isolate chain tests
robobun Jun 27, 2026
37dad95
test: run inline-sourcemap chain suite concurrently
robobun Jun 27, 2026
3a6030d
test: size oversized-name case past the largest platform MAX_PATH_BYTES
robobun Jun 27, 2026
c9447b7
sourcemap: adapt InputSourceMap to renamed json parse and Result-base…
robobun Aug 15, 2026
5f22626
sourcemap: read inline maps through the tape-based JSON accessors
robobun Aug 15, 2026
7d009dd
sourcemap: trim explanatory comments to their load-bearing core
robobun Aug 15, 2026
27ec2ea
sourcemap: use bun_core::strings helpers for byte search (source lint)
robobun Aug 15, 2026
39e1360
bundler: skip inline-map scan under DevServer; pass URL-schemed sourc…
robobun Aug 15, 2026
2d79d79
bundler: emit inner source names verbatim for virtual-namespace modules
robobun Aug 15, 2026
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
9 changes: 9 additions & 0 deletions src/bundler/Graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,13 @@ pub struct InputFile {
pub unique_key_for_additional_file: Box<[u8], AstAlloc>,
pub content_hash_for_additional_file: u64,
pub flags: InputFileFlags,
/// When this file carried an inline `//# sourceMappingURL=data:...`
/// comment, the decoded inner map plus its `sourcesContent` bytes. The
/// linker expands outer `sources[]` / `sourcesContent[]` with these
/// inner entries and the `Chunk::Builder` remaps its mappings through
/// the inner `find_mapping` so final stack traces surface in the
/// authored source. `None` when no chain is available (most inputs).
Comment thread
robobun marked this conversation as resolved.
Outdated
pub input_source_map: Option<Box<bun_sourcemap::InputSourceMap>>,
}
Comment thread
claude[bot] marked this conversation as resolved.

impl Default for InputFile {
Expand All @@ -127,6 +134,7 @@ impl Default for InputFile {
unique_key_for_additional_file: AstAlloc::vec().into_boxed_slice(),
content_hash_for_additional_file: 0,
flags: InputFileFlags::default(),
input_source_map: None,
}
}
}
Expand All @@ -144,6 +152,7 @@ bun_collections::multi_array_columns! {
unique_key_for_additional_file: Box<[u8], AstAlloc>,
content_hash_for_additional_file: u64,
flags: InputFileFlags,
input_source_map: Option<Box<bun_sourcemap::InputSourceMap>>,
}
}

Expand Down
219 changes: 163 additions & 56 deletions src/bundler/LinkerContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1044,89 +1044,101 @@ impl<'a> LinkerContext<'a> {

let sources = self.parse_graph().input_files.items_source();
let quoted_source_map_contents = self.graph.files.items_quoted_source_contents();
// DevServer uses a separate sourcemap stitcher (`SourceMapStore::join_vlq`)
// that hard-codes one `sources[]` slot per input; threading inner-map
// expansion through there would corrupt its output. `DevServer == None`
// gates the whole feature so the HMR path stays byte-identical.
Comment thread
robobun marked this conversation as resolved.
Outdated
let input_source_maps: Option<&[Option<Box<bun_sourcemap::InputSourceMap>>]> =
if self.dev_server.is_none() {
Some(self.parse_graph().input_files.items_input_source_map())
} else {
None
};

// Entries in `results` do not 1:1 map to source files, the mapping
// is actually many to one, where a source file can have multiple chunks
// in the sourcemap.
//
// This hashmap is going to map:
// This hashmap maps:
// `source_index` (per compilation) in a chunk
// -->
// Which source index in the generated sourcemap, referred to
// as the "mapping source index" within this function to be distinct.
// Base source index in the generated sourcemap (inclusive). When
// the input file did not carry an inline sourcemap, the chunk's
// mappings all use that base. When the input file carried an
// inline `//# sourceMappingURL=`, the chunk's mappings were
// remapped through that inner map at print time and now span
// `base .. base + inner.external_source_names.len - 1`.
Comment thread
robobun marked this conversation as resolved.
Outdated
let mut source_id_map: ArrayHashMap<u32, i32> = ArrayHashMap::new();

let source_indices = results.items_source_index();

j.push_static(b"{\n \"version\": 3,\n \"sources\": [");
let mut next_mapping_source_index: i32 = 0;
if !source_indices.is_empty() {
{
let index = source_indices[0];
let path = &sources[index as usize].path;
source_id_map.put_no_clobber(index, 0)?;

// Note: the relative path lives in a local owned buffer
// (drops at scope exit).
let rel_path_storage;
let pretty: &[u8] = if path.is_file() {
rel_path_storage = Self::source_map_relative_path(chunk_abs_dir, path.text)?;
&rel_path_storage
} else {
path.pretty
};

let mut quote_buf = MutableString::init(pretty.len() + 2)?;
js_printer::quote_for_json(pretty, &mut quote_buf, false)?;
// `to_default_owned` moves the buffer into the joiner
// (joiner owns it until `done`).
j.push_owned(quote_buf.to_default_owned());
}

let mut next_mapping_source_index: i32 = 1;
for &index in &source_indices[1..] {
for (chunk_i, &index) in source_indices.iter().enumerate() {
let gop = source_id_map.get_or_put(index)?;
if gop.found_existing {
continue;
}

*gop.value_ptr = next_mapping_source_index;
next_mapping_source_index += 1;

let path = &sources[index as usize].path;

let rel_path_storage;
let pretty: &[u8] = if path.is_file() {
rel_path_storage = Self::source_map_relative_path(chunk_abs_dir, path.text)?;
&rel_path_storage
} else {
path.pretty
// `1` for the intermediate input, plus one slot per inner
// source listed in its `sourceMappingURL`.
Comment thread
robobun marked this conversation as resolved.
let inner: Option<&bun_sourcemap::InputSourceMap> =
input_source_maps.and_then(|m| m[index as usize].as_deref());
let expansion: i32 = 1 + match inner {
Some(ism) => {
i32::try_from(ism.map.external_source_names.len()).expect("int cast")
}
None => 0,
};

let mut quote_buf = MutableString::init(pretty.len() + ", ".len() + 2)?;
quote_buf.append_assume_capacity(b", ");
js_printer::quote_for_json(pretty, &mut quote_buf, false)?;
j.push_owned(quote_buf.to_default_owned());
next_mapping_source_index += expansion;

write_sources_for(
&mut j,
chunk_abs_dir,
&sources[index as usize].path,
inner,
chunk_i > 0,
)?;
}
}

j.push_static(b"],\n \"sourcesContent\": [");

let source_indices_for_contents = source_id_map.keys();
if !source_indices_for_contents.is_empty() {
j.push_static(b"\n ");
j.push_static(
quoted_source_map_contents[source_indices_for_contents[0] as usize]
.as_deref()
.unwrap_or(b""),
);

for &index in &source_indices_for_contents[1..] {
j.push_static(b",\n ");
j.push_static(
quoted_source_map_contents[index as usize]
let mut emitted_contents: usize = 0;
for &index in source_indices_for_contents.iter() {
// Slot 0: the intermediate input file's contents (already
// JSON-quoted by `compute_quoted_source_contents`).
Comment thread
robobun marked this conversation as resolved.
{
let sep: &[u8] = if emitted_contents == 0 {
b"\n "
} else {
b",\n "
};
j.push_static(sep);
let content = quoted_source_map_contents[index as usize]
.as_deref()
.unwrap_or(b""),
);
.unwrap_or(b"null");
j.push_static(if content.is_empty() { b"null" } else { content });
emitted_contents += 1;
}
// Slots 1..N: inner sources' contents, if any.
if let Some(ism) = input_source_maps.and_then(|m| m[index as usize].as_deref()) {
for content in ism.sources_content.iter() {
j.push_static(b",\n ");
if !content.is_empty() {
let mut quote_buf = MutableString::init(content.len() + 2)?;
js_printer::quote_for_json(content, &mut quote_buf, false)?;
j.push_owned(quote_buf.to_default_owned());
} else {
j.push_static(b"null");
}
emitted_contents += 1;
}
}
}
}
j.push_static(b"\n ],\n \"mappings\": \"");
Expand Down Expand Up @@ -1166,7 +1178,12 @@ impl<'a> LinkerContext<'a> {
)?;

prev_end_state = chunk.end_state;
prev_end_state.source_index = mapping_source_index;
// If the input carried an inline map, `chunk.end_state.source_index`
// is the inner source_index of the last mapping within the chunk
// (the Builder emits remapped absolute-within-chunk indices).
// Otherwise it's 0. Either way, the final absolute index is
// `mapping_source_index + chunk.end_state.source_index`.
Comment thread
robobun marked this conversation as resolved.
Outdated
prev_end_state.source_index = mapping_source_index + chunk.end_state.source_index;
prev_column_offset = chunk.final_generated_column;

if prev_end_state.generated_line == 0 {
Expand Down Expand Up @@ -1210,6 +1227,86 @@ impl<'a> LinkerContext<'a> {
}
}

/// Emit one outer source's quoted path, plus any inner source paths
/// contributed by its `//# sourceMappingURL=` (one slot per inner source,
/// in `external_source_names` order). `leading_comma` is true when this is
/// not the first path appended to the running `sources[]` array — we
/// prefix `", "` before the outer path in that case.
///
/// Layout matches the one `Chunk::Builder` assumes in `Chunk.rs`:
/// slot 0 → the intermediate input (this outer file)
/// slot 1..N → inner `sources[i]` (chained)
Comment thread
robobun marked this conversation as resolved.
Outdated
fn write_sources_for(
joiner: &mut StringJoiner,
chunk_abs_dir: &[u8],
outer_path: &bun_paths::fs::Path,
input_map: Option<&bun_sourcemap::InputSourceMap>,
leading_comma: bool,
) -> Result<(), BunError> {
// 1) the intermediate input.
let rel_path_storage;
let pretty: &[u8] = if outer_path.is_file() {
rel_path_storage = LinkerContext::source_map_relative_path(chunk_abs_dir, outer_path.text)?;
&rel_path_storage
} else {
outer_path.pretty
};
{
let mut quote_buf = MutableString::init(pretty.len() + ", ".len() + 2)?;
if leading_comma {
quote_buf.append_assume_capacity(b", ");
}
js_printer::quote_for_json(pretty, &mut quote_buf, false)?;
joiner.push_owned(quote_buf.to_default_owned());
}

// 2) inner sources, if any. Each inner `sources[i]` is resolved
// relative to the directory of the intermediate file it came from,
// then made relative to `chunk_abs_dir` (the chunk's output dir) for
// the emitted JSON. Absolute inner paths stay absolute before
// relativization.
Comment thread
robobun marked this conversation as resolved.
Outdated
if let Some(ism) = input_map {
let base_dir = bun_paths::resolve_path::dirname::<bun_paths::resolve_path::platform::Auto>(
outer_path.text,
);
Comment thread
claude[bot] marked this conversation as resolved.
Outdated
// `name` is capped at `MAX_PATH_BYTES` by the parser, so emitting it
// relative (or, on a join that still overflows, verbatim) never
// overflows the fixed-size path buffers.
Comment thread
robobun marked this conversation as resolved.
Outdated
let emit = |joiner: &mut StringJoiner, p: &[u8]| -> Result<(), BunError> {
let mut quote_buf = MutableString::init(p.len() + ", ".len() + 2)?;
quote_buf.append_assume_capacity(b", ");
js_printer::quote_for_json(p, &mut quote_buf, false)?;
joiner.push_owned(quote_buf.to_default_owned());
Ok(())
};
let mut join_buf = bun_paths::path_buffer_pool::get();
for name in ism.map.external_source_names.iter() {
let name: &[u8] = name.as_ref();
if bun_paths::resolve_path::Platform::AUTO.is_absolute(name) {
let rel = LinkerContext::source_map_relative_path(chunk_abs_dir, name)?;
emit(joiner, &rel)?;
continue;
}
// Relative inner name: join against `base_dir` to get an
// absolute path, then re-relativize to `chunk_abs_dir`. The
// checked join returns `None` when `base_dir + name` exceeds
// the buffer (an adversarial inline map); fall back to the raw
// (spec-valid) name rather than panicking.
Comment thread
robobun marked this conversation as resolved.
Outdated
match bun_paths::resolve_path::join_abs_string_buf_checked::<
bun_paths::resolve_path::platform::Auto,
>(base_dir, join_buf.as_mut_slice(), &[name])
{
Some(abs_path) => {
let rel = LinkerContext::source_map_relative_path(chunk_abs_dir, abs_path)?;
emit(joiner, &rel)?;
}
None => emit(joiner, name)?,
}
}
Comment thread
claude[bot] marked this conversation as resolved.
}
Ok(())
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum ScanCssImportsResult {
Ok,
Expand Down Expand Up @@ -2193,6 +2290,15 @@ impl<'a> LinkerContext<'a> {
// SAFETY: `self.mangled_props` is not mutated during printing; detached borrow
// outlives only this call (see above).
unsafe { bun_ptr::detach_lifetime_ref(&self.mangled_props) };
// DevServer uses a separate sourcemap stitcher that hard-codes one
// `sources[]` slot per file; passing `input_source_map` would
// corrupt its output. Gate the whole feature on the Bun.build path.
Comment thread
robobun marked this conversation as resolved.
Outdated
let input_source_map: Option<&bun_sourcemap::InputSourceMap> = if self.dev_server.is_none()
{
parse_graph.input_files.items_input_source_map()[source_index.get() as usize].as_deref()
} else {
None
};

let print_options = js_printer::Options {
bundling: true,
Expand Down Expand Up @@ -2241,6 +2347,7 @@ impl<'a> LinkerContext<'a> {
} else {
None
},
input_source_map,
mangled_props: Some(mangled_props),
module_info,
..Default::default()
Expand Down
36 changes: 36 additions & 0 deletions src/bundler/ParseTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,14 @@ pub(crate) struct Success {

/// The package name from package.json, used for barrel optimization.
pub(crate) package_name: ast::StoreStr,

/// Decoded trailing inline `//# sourceMappingURL=data:...` inner map,
/// parsed from the source bytes. `None` when the file had no inline
/// sourcemap comment, when sourcemaps are disabled on the build, or
/// when the inline payload was malformed (caller silently falls back
/// to the raw file bytes). Moved into `graph.input_files.input_source_map`
/// by `on_parse_task_complete`.
Comment thread
robobun marked this conversation as resolved.
Outdated
pub(crate) input_source_map: Option<Box<bun_sourcemap::InputSourceMap>>,
}

pub(crate) struct ResultError {
Expand Down Expand Up @@ -2623,6 +2631,12 @@ pub mod parse_worker {
// SAFETY: task.ctx backref valid for the bundle pass (outlives `'r`).
let task_ctx = unsafe { task.ctx() };
let module_type = opts.module_type;
// Hoist the `source_map` flag before the tombstone: we need it
// below after `get_ast` runs, but reading `topts.source_map` there
// would touch the invalidated shared borrow (get_ast reborrows
// `(*transpiler).options` mutably via raw pointer, which pops
// `topts`'s tag under Stacked Borrows). Copy it out now.
Comment thread
robobun marked this conversation as resolved.
Outdated
let source_map_option = topts.source_map;
// `topts` (a `&BundleOptions`) is dead past this point; the callees take
// raw `*mut Transpiler` and reborrow `(*transpiler).options` mutably.
let _ = topts;
Expand Down Expand Up @@ -2683,6 +2697,26 @@ pub mod parse_worker {

*step = Step::Resolve;

// Chain any inline `//# sourceMappingURL=data:...` map the input
// file carries (e.g. a `.vue`/`.svelte` compiler's trailing
// comment on the intermediate `.js`) into the output sourcemap.
// This scan runs on `source.contents` whether they came from a
// file read or a plugin `onLoad` return, so this covers #6173
// too. Gated on:
// - source maps enabled on the build (no cost otherwise)
// - loader can have source maps (js/ts/jsx/tsx; skip binary/asset)
// - non-empty contents (the scanner would find nothing)
// Malformed payloads return `None` and fall back cleanly.
Comment thread
robobun marked this conversation as resolved.
Outdated
let input_source_map: Option<Box<bun_sourcemap::InputSourceMap>> = if source_map_option
!= options::SourceMapOption::None
&& loader.can_have_source_map()
&& !source.contents.is_empty()
{
bun_sourcemap::InputSourceMap::parse_from_source(&source.contents)
} else {
None
};
Comment thread
claude[bot] marked this conversation as resolved.
Outdated

Ok(Success {
ast,
source: source.clone(),
Expand All @@ -2699,6 +2733,8 @@ pub mod parse_worker {
} else {
0
},

input_source_map,
})
}

Expand Down
3 changes: 3 additions & 0 deletions src/bundler/ServerComponentParseTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,9 @@ fn task_callback(
unique_key_for_additional_file: bun_ast::StoreStr::EMPTY,
content_hash_for_additional_file: 0,
package_name: bun_ast::StoreStr::EMPTY,
// Server-component wrappers are generated, not authored — no inline
// sourcemap comment to chain through.
Comment thread
robobun marked this conversation as resolved.
Outdated
input_source_map: None,
})
}

Expand Down
Loading
Loading