Skip to content
Open
Show file tree
Hide file tree
Changes from 27 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
6 changes: 6 additions & 0 deletions src/bundler/Graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ pub struct InputFile {
pub unique_key_for_additional_file: Box<[u8], AstAlloc>,
pub content_hash_for_additional_file: u64,
pub flags: InputFileFlags,
/// Decoded inline `//# sourceMappingURL=data:...` map for this file;
/// the linker expands `sources[]`/`sourcesContent[]` with its entries
/// and `Chunk::Builder` remaps mappings through it. Usually `None`.
Comment thread
robobun marked this conversation as resolved.
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 +131,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 +149,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
201 changes: 139 additions & 62 deletions src/bundler/LinkerContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1044,89 +1044,90 @@ 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's stitcher (`SourceMapStore::join_vlq`) assumes one
// `sources[]` slot per input, so chaining is gated to `Bun.build`.
Comment thread
robobun marked this conversation as resolved.
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:
// `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.
// Many-to-one: a source file can own several chunks. Maps each
// compilation `source_index` to its base index in the generated
// `sources[]`; a file with an inline map spans
// `base ..= base + external_source_names.len`.
Comment thread
robobun marked this conversation as resolved.
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 +1167,9 @@ impl<'a> LinkerContext<'a> {
)?;

prev_end_state = chunk.end_state;
prev_end_state.source_index = mapping_source_index;
// `chunk.end_state.source_index` is chunk-relative (0 without an
// inline map); rebase it onto this file's slot base.
Comment thread
robobun marked this conversation as resolved.
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 +1213,71 @@ impl<'a> LinkerContext<'a> {
}
}

/// Emit one outer source's quoted path plus its chained inner paths, in
/// the slot layout `Chunk::Builder` emits against: slot 0 = the outer
/// file, slots 1..N = inner `sources[i]`.
Comment thread
robobun marked this conversation as resolved.
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: resolve each against the intermediate's dir, then
// re-relativize to `chunk_abs_dir` for the emitted JSON.
Comment thread
robobun marked this conversation as resolved.
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
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;
}
// The checked join returns `None` on overflow (adversarial
// map); emit the raw, spec-valid name instead of panicking.
Comment thread
robobun marked this conversation as resolved.
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 +2261,14 @@ 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's stitcher assumes one `sources[]` slot per file;
// chaining is gated to the `Bun.build` path.
Comment thread
robobun marked this conversation as resolved.
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 +2317,7 @@ impl<'a> LinkerContext<'a> {
} else {
None
},
input_source_map,
mangled_props: Some(mangled_props),
module_info,
..Default::default()
Expand Down
24 changes: 24 additions & 0 deletions src/bundler/ParseTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ 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:...` map; `None`
/// when absent, disabled, or malformed. Moved into
/// `graph.input_files.input_source_map` by `on_parse_task_complete`.
Comment thread
robobun marked this conversation as resolved.
pub(crate) input_source_map: Option<Box<bun_sourcemap::InputSourceMap>>,
}

pub(crate) struct ResultError {
Expand Down Expand Up @@ -2623,6 +2628,10 @@ 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;
// Copy `source_map` out before the tombstone: get_ast reborrows
// `(*transpiler).options` mutably, invalidating `topts` under
// Stacked Borrows.
Comment thread
robobun marked this conversation as resolved.
Outdated
Comment thread
robobun marked this conversation as resolved.
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 +2692,19 @@ pub mod parse_worker {

*step = Step::Resolve;

// Scan for an inline `//# sourceMappingURL=data:...` map to chain
// into the output sourcemap. Runs on `source.contents` regardless
// of origin (file read or plugin `onLoad`, covering #6173).
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 +2721,8 @@ pub mod parse_worker {
} else {
0
},

input_source_map,
})
}

Expand Down
2 changes: 2 additions & 0 deletions src/bundler/ServerComponentParseTask.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ fn task_callback(
unique_key_for_additional_file: bun_ast::StoreStr::EMPTY,
content_hash_for_additional_file: 0,
package_name: bun_ast::StoreStr::EMPTY,
// Generated wrapper: nothing to chain.
input_source_map: None,
})
}

Expand Down
15 changes: 15 additions & 0 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4955,6 +4955,13 @@ pub mod bv2_impl {
// `memcpy` of `graph.ast`), and `CssChunk::asts` `forget()`s its
// aliases, so this is the unique drop.
{
// `input_source_map` slots are global-heap `Box`es; the
// slab-only `MultiArrayList::drop` would strand them, so
// drain explicitly (same pattern as `css` below).
Comment thread
robobun marked this conversation as resolved.
for m in self.graph.input_files.items_input_source_map_mut() {
drop(m.take());
}

macro_rules! take_ast_cols {
($ast:expr) => {{
let ast = $ast;
Expand Down Expand Up @@ -7085,6 +7092,14 @@ pub mod bv2_impl {
// Record which loader we used for this file
this.graph.input_files.items_loader_mut()[result_source_index] = result.loader;

// Move the decoded inline sourcemap onto the SoA slot,
// dropping any earlier occupant (incremental reparse).
Comment thread
robobun marked this conversation as resolved.
{
let slot = &mut this.graph.input_files.items_input_source_map_mut()
[result_source_index];
*slot = core::mem::take(&mut result.input_source_map);
}

bun_core::scoped_log!(
Bundle,
"onParse({}, {}) = {} imports, {} exports",
Expand Down
Loading
Loading