Skip to content
Open
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
eb5c2ff
bundler: chain inline input sourcemaps through to output
robobun May 12, 2026
82f4025
[autofix.ci] apply automated fixes
autofix-ci[bot] May 12, 2026
c7885a5
test: cover plugin onLoad with inline sourcemap (#6173)
robobun May 12, 2026
64ed7f6
address review findings: OOM, leak, test guards
robobun May 12, 2026
dd29d7d
sourcemap: anchor findSourceMappingURL to the final line
robobun May 12, 2026
557971b
address 3 review findings from claude[bot]
robobun May 12, 2026
ccff538
ci: retrigger
robobun May 12, 2026
2597a8a
build: serialize release smoke-test against strip
robobun May 12, 2026
193fc55
sourcemap: swap std.mem.indexOfAny for bun.strings.indexAnyComptime
robobun May 12, 2026
6182656
test: gate chained-sourcemap tests behind describe.todo
robobun May 16, 2026
313b504
bundler: chain inline input sourcemaps through to output (Rust port)
robobun May 16, 2026
31fafb8
[autofix.ci] apply automated fixes
autofix-ci[bot] May 16, 2026
b72e85e
sourcemap: trim whitespace on both sides of the URL after sourceMappi…
robobun May 19, 2026
8255258
bundler: init Graph::InputFile.input_source_map in Default impl
robobun May 20, 2026
e33965e
sourcemap: drop unused Mapping import from InputSourceMap.rs
robobun May 20, 2026
446bcc8
bundler: hoist source_map option before get_ast to avoid stacked-borr…
robobun May 20, 2026
42e1787
sourcemap: drop redundant mut on estr bindings (-D unused-mut)
robobun Jun 2, 2026
e803a32
sourcemap: hint find_line_with_hint from intermediate line, not remapped
robobun Jun 2, 2026
96fd1f2
[autofix.ci] apply automated fixes
autofix-ci[bot] Jun 2, 2026
4d879a3
js_printer: use transmute for input_source_map lifetime erasure (clippy)
robobun Jun 2, 2026
8020783
sourcemap: align comments with main's port-note cleanup
robobun Jun 6, 2026
312b1dc
Merge remote-tracking branch 'origin/main' into farm/7467a033/chain-e…
robobun Jun 17, 2026
731de50
bundler: chain external input sourcemaps + thread chains through the …
robobun Jun 17, 2026
f8f9d52
[autofix.ci] apply automated fixes
autofix-ci[bot] Jun 17, 2026
faf54b5
address review: drop stale dev-server gates, OOM handling, HMR cache …
robobun Jun 17, 2026
6d1ff14
address review: honour sourceRoot, guard non-file namespaces in write…
robobun Jun 17, 2026
0c49f06
[autofix.ci] apply automated fixes
autofix-ci[bot] Jun 17, 2026
e18bcc6
address review: pass URL-style inner source names through verbatim
robobun Jun 18, 2026
2fb4ac1
address review: use length-checked path join for user-controlled sour…
robobun Jun 18, 2026
354dfdd
[autofix.ci] apply automated fixes
autofix-ci[bot] Jun 18, 2026
238418d
test(26713): normalize path separators in makeFixture pre-build sanit…
robobun Jun 18, 2026
1a800b4
Merge branch 'main' into farm/7467a033/chain-external-input-sourcemaps
Jarred-Sumner Jun 19, 2026
f04b85e
Merge branch 'main' into farm/7467a033/chain-external-input-sourcemaps
robobun Jun 26, 2026
3466b4a
docs: mention the sidecar .map origin on the other three input_source…
robobun Jun 26, 2026
824273d
Merge origin/main into farm/7467a033/chain-external-input-sourcemaps
robobun Jun 27, 2026
c67b78d
[autofix.ci] apply automated fixes
autofix-ci[bot] Jun 27, 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
24 changes: 20 additions & 4 deletions scripts/build/bun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,16 @@ export function emitBun(n: Ninja, cfg: Config, sources: Sources): BunOutput {
// ASAN binaries to run from subprocesses (shadow memory layout conflict
// with ELF_ET_DYN_BASE, see sanitizers/856). We try with setarch first,
// fall back to direct invocation.
emitSmokeTest(n, cfg, exe, exeName);
//
// We order the smoke-test AFTER strip (when strip runs) because
// `cfg.jsRuntime` — the bun that wraps the smoke-test command — is
// typically `${buildDir}/bun` on a dev's PATH, i.e. the same file
// `strip` writes to. Without an explicit dep, ninja can schedule the
// smoke test concurrently with strip on a full rebuild and the
// jsRuntime invocation races against strip's write with an
// `EACCES`-family failure ("Permission denied"). Making the stripped
// binary an order-only input to the smoke test serializes them.
emitSmokeTest(n, cfg, exe, exeName, strippedExe);

return { exe, strippedExe, dsym, deps, codegen, rustObjects, objects: allObjects };
}
Expand Down Expand Up @@ -626,15 +635,18 @@ function emitLinkOnly(n: Ninja, cfg: Config): BunOutput {
linkerMapOutput: cfg.linux && cfg.release && !cfg.asan && !cfg.valgrind ? linkerMapPath(cfg) : undefined,
});

// Strip + smoke test — same as full mode.
// Strip + smoke test — same as full mode. Pass `strippedExe` through
// to the smoke test as an order-only dep so ninja serializes `strip`
// against the smoke-test's `cfg.jsRuntime` (see the comment at the
// emit site above).
let strippedExe: string | undefined;
let dsym: string | undefined;
if (shouldStrip(cfg)) {
strippedExe = emitStrip(n, cfg, exe, flags.stripflags);
if (cfg.darwin) dsym = emitDsymutil(n, cfg, exe, exeName);
}
if (strippedExe === undefined) n.phony("bun", [exe]);
emitSmokeTest(n, cfg, exe, exeName);
emitSmokeTest(n, cfg, exe, exeName, strippedExe);

return {
exe,
Expand All @@ -652,7 +664,7 @@ function emitLinkOnly(n: Ninja, cfg: Config): BunOutput {
* linker didn't catch (missing symbol only referenced at init, ICU ABI
* mismatch, etc.).
*/
function emitSmokeTest(n: Ninja, cfg: Config, exe: string, exeName: string): void {
function emitSmokeTest(n: Ninja, cfg: Config, exe: string, exeName: string, strippedExe?: string): void {
// Cross-compiled binaries can't run on the build host. Skip the smoke
// test entirely — `ninja check` becomes a no-op alias for the exe.
if (cfg.crossTarget !== undefined) {
Expand Down Expand Up @@ -697,6 +709,10 @@ function emitSmokeTest(n: Ninja, cfg: Config, exe: string, exeName: string): voi
outputs: [stamp],
rule: "smoke_test",
inputs: [exe],
// `strippedExe` as an order-only input keeps ninja from racing
// `strip bun` against this smoke test's use of the same file as
// `cfg.jsRuntime` on a dev's PATH. See the emit site above.
orderOnlyInputs: strippedExe !== undefined ? [strippedExe] : undefined,
});

// Phony target — `ninja check` runs the smoke test.
Expand Down
9 changes: 9 additions & 0 deletions src/bundler/Graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,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).
pub input_source_map: Option<Box<bun_sourcemap::InputSourceMap>>,
Comment thread
robobun marked this conversation as resolved.
Outdated
}

impl Default for InputFile {
Expand All @@ -120,6 +127,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 @@ -137,6 +145,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
10 changes: 10 additions & 0 deletions src/bundler/Graph.zig
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,16 @@ pub const InputFile = struct {
content_hash_for_additional_file: u64 = 0,
flags: Flags = .{},

/// Populated when the input file carried a trailing
/// `//# sourceMappingURL=data:application/json;...` comment that we
/// were able to parse. Lets the linker chain the map through — sources
/// and mappings reference the authored origin (`.vue`, `.svelte`, `.ts`
/// that an upstream step compiled away) instead of the intermediate
/// `.js` the bundler ingested.
///
/// Owned — freed on bundler teardown.
input_source_map: ?*bun.SourceMap.InputSourceMap = null,

pub const Flags = packed struct(u8) {
is_plugin_file: bool = false,
/// Set when a barrel-eligible file has `export * from` this file.
Expand Down
215 changes: 157 additions & 58 deletions src/bundler/LinkerContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1092,91 +1092,93 @@ 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();
let input_source_maps: &[Option<Box<bun_sourcemap::InputSourceMap>>] =
self.parse_graph().input_files.items_input_source_map();

// 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`.
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 =
bun_paths::resolve_path::relative_alloc(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 =
bun_paths::resolve_path::relative_alloc(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`.
let inner: Option<&bun_sourcemap::InputSourceMap> =
input_source_maps[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`).
{
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[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 @@ -1216,7 +1218,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`.
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 @@ -1260,6 +1267,91 @@ 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)
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 = bun_paths::resolve_path::relative_alloc(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. When the intermediate lives in a non-file
// namespace (a plugin's virtual module), its `text` is not a
// filesystem path, so emit the inner name as-is rather than joining
// against a meaningless dirname.
if let Some(ism) = input_map {
let is_file = outer_path.is_file();
let base_dir = if is_file {
bun_paths::resolve_path::dirname::<bun_paths::resolve_path::platform::Auto>(
outer_path.text,
)
} else {
b""
};
for name in ism.map.external_source_names.iter() {
let name: &[u8] = name.as_ref();
let rel_path_storage;
let rel_path: &[u8] = if !is_file || bun_sourcemap::is_url_like_source_name(name) {
// Non-file namespace, or a URL-style virtual name
// (`webpack://`, `ng://`, `//host/...`): pass through
// unchanged so DevTools sees the original identifier.
name
} else {
// Use `join_abs` to produce an absolute inner path (when
// the inner map emitted a relative source name) that can
// then be re-relativized against `chunk_abs_dir`.
// `join_abs` returns a borrow into a thread-local buffer;
// we copy out immediately via `relative_alloc`.
let abs_path: &[u8] = if bun_paths::resolve_path::Platform::AUTO.is_absolute(name) {
name
} else {
bun_paths::resolve_path::join_abs::<bun_paths::resolve_path::platform::Auto>(
base_dir, name,
)
};
rel_path_storage =
bun_paths::resolve_path::relative_alloc(chunk_abs_dir, abs_path)?;
&rel_path_storage
Comment thread
coderabbitai[bot] marked this conversation as resolved.
};

let mut quote_buf = MutableString::init(rel_path.len() + ", ".len() + 2)?;
Comment thread
robobun marked this conversation as resolved.
quote_buf.append_assume_capacity(b", ");
js_printer::quote_for_json(rel_path, &mut quote_buf, false)?;
joiner.push_owned(quote_buf.to_default_owned());
}
}
Ok(())
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum ScanCssImportsResult {
Ok,
Expand Down Expand Up @@ -2207,6 +2299,12 @@ 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 sourcemap stitcher (`SourceMapStore::join_vlq`) now
// tracks per-file inner-source expansion via `PackedMap.inner_sources`,
// so chained input sourcemaps flow through both paths.
let input_source_map: Option<&bun_sourcemap::InputSourceMap> =
parse_graph.input_files.items_input_source_map()[source_index.get() as usize]
.as_deref();

let print_options = js_printer::Options {
bundling: true,
Expand Down Expand Up @@ -2255,6 +2353,7 @@ impl<'a> LinkerContext<'a> {
} else {
None
},
input_source_map,
mangled_props: Some(mangled_props),
..Default::default()
};
Expand Down
Loading
Loading