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
79 changes: 75 additions & 4 deletions src/bundler/LinkerContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1194,7 +1194,7 @@ impl<'a> LinkerContext<'a> {
.expect("TODO: handle error");
}

/// The relative path from the chunk directory to a file source, as written
/// The relative path from the directory of the file that carries the map to a file source, as written
/// into the source map's `sources` array. `sources` entries are URLs, so the
/// host separator is normalized to `/` (the invariant `Path::pretty` holds).
fn source_map_relative_path(
Expand All @@ -1206,16 +1206,86 @@ impl<'a> LinkerContext<'a> {
Ok(rel)
}

/// The directory of the file that carries `chunk`'s map, when the `sources` are not relative to `output_dir` as given.
fn source_map_base_dir(
&self,
chunk: &Chunk<'_>,
chunk_index: usize,
chunks: &[Chunk<'_>],
) -> Option<Box<[u8]>> {
let output_dir: &[u8] = &self.resolver().opts.output_dir;
// An executable prints these entries as they are, and without an outdir no map file exists to be relative to.
if self.options.compile_mode.is_executable() || output_dir.is_empty() {
Comment thread
robobun marked this conversation as resolved.
Resolved
return None;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

// Standalone HTML inlines the chunk, and an inline map with it, into the HTML file.
let mut template = &chunk.template;
if self.options.compile_mode.is_standalone_html()
&& self.options.source_maps == SourceMapOption::Inline
{
let entry_point_id = chunk.entry_point.entry_point_id();
// `compute_chunks` puts that HTML chunk directly after its JS chunk, so the search starts there.
let (before, after) = chunks.split_at(chunk_index + 1);
if let Some(html) = after.iter().chain(before).find(|html| {
matches!(html.content, crate::chunk::Content::Html)
&& html.entry_point.entry_point_id() == entry_point_id
}) {
template = &html.template;
}
}

// `final_rel_path` is not assigned yet: its `[hash]` needs every chunk post-processed.
let sanitize_parent_dirs = !self.options.compile_mode.is_executable();
let mut pooled = bun_paths::path_buffer_pool::get();
let mut spill = Vec::new();
let mut fixed = bun_io::FixedBufferStream::new_mut(&mut pooled[..]);
let rel_path: &mut [u8] = if template
.print_with_stand_in_hash(&mut fixed, sanitize_parent_dirs)
.is_ok()
{
let len = fixed.pos;
&mut pooled[..len]
} else {
template
.print_with_stand_in_hash(&mut spill, sanitize_parent_dirs)
.expect("write to Vec<u8>");
&mut spill
};
bun_paths::resolve_path::platform_to_posix_in_place::<u8>(rel_path);
let dir =
bun_paths::resolve_path::dirname::<bun_paths::resolve_path::platform::Posix>(rel_path);
if dir.is_empty() || dir == b"." {
return None;
}

let top_level_dir = bun_resolver::fs::FileSystem::get().top_level_dir();
let mut long_path = Vec::new();
Some(Box::from(bun_paths::resolve_path::join_abs_string_spill::<
bun_paths::resolve_path::platform::Auto,
>(
top_level_dir,
&mut long_path,
&[output_dir, dir],
)))
}

pub(crate) fn generate_source_map_for_chunk(
&mut self,
isolated_hash: u64,
_worker: &mut crate::thread_pool::Worker,
chunk: &Chunk<'_>,
chunk_index: usize,
chunks: &[Chunk<'_>],
results: &MultiArrayList<CompileResultForSourceMap<'_>>,
chunk_abs_dir: &[u8],
can_have_shifts: bool,
) -> Result<SourceMapPieces, BunError> {
let _trace = bun::perf::trace("Bundler.generateSourceMapForChunk");

let base_dir = self.source_map_base_dir(chunk, chunk_index, chunks);
let chunk_abs_dir: &[u8] = match &base_dir {
Some(dir) => dir,
None => &self.resolver().opts.output_dir,
};

let mut j = StringJoiner::default();

let sources = self.parse_graph().input_files.items_source();
Expand Down Expand Up @@ -1356,6 +1426,7 @@ impl<'a> LinkerContext<'a> {
j.push_static(b"\",\n \"debugId\": \"");
let mut buf = Vec::<u8>::new();
use std::io::Write;
let isolated_hash = chunk.isolated_hash;
write!(&mut buf, "{}", DebugIDFormatter { id: isolated_hash })
.expect("infallible: in-memory write");
j.push_owned(buf.into_boxed_slice());
Expand Down
4 changes: 1 addition & 3 deletions src/bundler/bundle_v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8042,7 +8042,6 @@ pub mod bv2_impl {
Css {
result: crate::Result<Box<[u8]>>,
source_index: IndexInt,
source_map: Option<bun_sourcemap::Chunk>,
},
Html {
source_index: IndexInt,
Expand Down Expand Up @@ -8091,8 +8090,7 @@ pub mod bv2_impl {
bun_js_printer::PrintResult::Result(r) => r.source_map.as_ref(),
bun_js_printer::PrintResult::Err(_) => None,
},
CompileResult::Css { source_map, .. } => source_map.as_ref(),
CompileResult::Html { .. } => None,
CompileResult::Css { .. } | CompileResult::Html { .. } => None,
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,12 @@ fn generate_compile_result_for_css_chunk_impl(
return CompileResult::Css {
result: Err(crate::Error::PrintError),
source_index: Index::INVALID.get(),
source_map: None,
};
}
}
CompileResult::Css {
result: Ok(allocating_writer.into_boxed_slice()),
source_index: Index::INVALID.get(),
source_map: None,
}
}
CssImportOrderKind::SourceIndex(idx) => {
Expand Down Expand Up @@ -148,7 +146,6 @@ fn generate_compile_result_for_css_chunk_impl(
return CompileResult::Css {
result: Err(crate::Error::PrintError),
source_index: idx.get(),
source_map: None,
};
}
}
Expand All @@ -166,7 +163,6 @@ fn generate_compile_result_for_css_chunk_impl(
CompileResult::Css {
result: Ok(output),
source_index: idx.get(),
source_map: None,
}
}
}
Expand Down
60 changes: 1 addition & 59 deletions src/bundler/linker_context/postProcessCSSChunk.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
use crate::mal_prelude::*;
use bun_collections::MultiArrayList;
use bun_core::string_joiner::{StringJoiner, Watcher};
use bun_sourcemap::{LineColumnOffset, LineColumnOffsetOptional};

use crate::chunk::IntermediateOutput;
use crate::linker_context_mod::{GenerateChunkCtx, LinkerOptionsMode};
use crate::thread_pool;
use crate::{Chunk, CompileResultForSourceMap, Index, options};
use crate::{Chunk, Index};

/// This runs after we've already populated the compile results
pub(crate) fn post_process_css_chunk(
Expand All @@ -22,13 +19,6 @@ pub(crate) fn post_process_css_chunk(
..Default::default()
};

let mut line_offset: LineColumnOffsetOptional =
if c.options.source_maps != options::SourceMapOption::None {
LineColumnOffsetOptional::Value(LineColumnOffset::default())
} else {
LineColumnOffsetOptional::Null
};

let mut newline_before_comment = false;

// TODO: css banner
Expand All @@ -49,10 +39,6 @@ pub(crate) fn post_process_css_chunk(
// Concatenate the generated CSS chunks together
let compile_results = &chunk.compile_results_for_chunk;

let mut compile_results_for_source_map: MultiArrayList<CompileResultForSourceMap> =
MultiArrayList::default();
bun_core::handle_oom(compile_results_for_source_map.set_capacity(compile_results.len()));

let sources: &[bun_ast::Source] = c.parse_graph().input_files.items_source();
for compile_result in compile_results.iter() {
let source_index = compile_result.source_index();
Expand All @@ -63,13 +49,11 @@ pub(crate) fn post_process_css_chunk(
{
if newline_before_comment {
j.push_static(b"\n");
line_offset.advance(b"\n");
}

let pretty: &[u8] = sources[source_index as usize].path.pretty;

j.push_static(b"/* ");
line_offset.advance(b"/* ");

// A `*/` in the path would terminate the comment early and let the
// rest of the path be parsed as CSS in the bundled output.
Expand All @@ -81,47 +65,21 @@ pub(crate) fn post_process_css_chunk(
}
escaped.push(byte);
}
line_offset.advance(&escaped);
j.push_owned(escaped.into_boxed_slice());
} else {
j.push_static(pretty);
line_offset.advance(pretty);
}

j.push_static(b" */\n");
line_offset.advance(b" */\n");
}

if !compile_result.code().is_empty() {
newline_before_comment = true;
}

// Save the offset to the start of the stored JavaScript
// code() borrows from
// compile_results which outlives the joiner; treat as static (no copy/free).
j.push_static(compile_result.code());

if let Some(source_map_chunk) = compile_result.source_map_chunk() {
if c.options.source_maps != options::SourceMapOption::None {
bun_core::handle_oom(compile_results_for_source_map.append(
CompileResultForSourceMap {
source_map_chunk,
// Guaranteed `Value` here
// because `source_maps != None` implies `line_offset` was
// initialised to `Value(_)` above.
generated_offset: match line_offset {
LineColumnOffsetOptional::Value(v) => v,
LineColumnOffsetOptional::Null => unreachable!(),
},
source_index: compile_result.source_index(),
},
));
}

line_offset.reset();
} else {
line_offset.advance(compile_result.code());
}
}

// Make sure the file ends with a newline
Expand All @@ -143,21 +101,5 @@ pub(crate) fn post_process_css_chunk(
chunk.isolated_hash = c.generate_isolated_hash(chunk, alloc);
// chunk.flags.is_executable = is_executable;

if c.options.source_maps != options::SourceMapOption::None {
let can_have_shifts = matches!(chunk.intermediate_output, IntermediateOutput::Pieces(_));
// Copy the `ParentRef` out (not `c.resolver()`) so `output_dir`
// borrows the local, not `c`, avoiding the split-borrow with
// `c.generate_source_map_for_chunk(&mut self, …)` below.
let resolver = c.resolver.expect("resolver set in load()");
let output_dir = &resolver.opts.output_dir;
chunk.output_source_map = c.generate_source_map_for_chunk(
chunk.isolated_hash,
worker,
&compile_results_for_source_map,
output_dir,
can_have_shifts,
)?;
}

Ok(())
}
10 changes: 3 additions & 7 deletions src/bundler/linker_context/postProcessJSChunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -790,15 +790,11 @@ pub(crate) fn post_process_js_chunk(
chunk.intermediate_output,
crate::chunk::IntermediateOutput::Pieces(_)
);
// Copy the `ParentRef` out (not `c.resolver()`) so the arg borrows the
// local, not `c`, avoiding the split-borrow with
// `c.generate_source_map_for_chunk(&mut self, …)`.
let resolver = c.resolver.expect("resolver set in load()");
chunk.output_source_map = c.generate_source_map_for_chunk(
chunk.isolated_hash,
worker,
chunk,
chunk_index,
&ctx.chunks,
&compile_results_for_source_map,
&resolver.opts.output_dir,
can_have_shifts,
)?;
}
Expand Down
73 changes: 73 additions & 0 deletions src/bundler/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2387,6 +2387,57 @@ fn path_template_print_tolerates_malformed_brackets() {
assert_eq!(find_unterminated_placeholder(b"[foo]-[name].[ext]"), None);
}

#[cfg(test)]
#[test]
fn stand_in_hash_prints_the_directory_depth_of_the_final_path() {
fn depth(path: &mut [u8]) -> usize {
bun_paths::resolve_path::platform_to_posix_in_place::<u8>(path);
let dir =
bun_paths::resolve_path::dirname::<bun_paths::resolve_path::platform::Posix>(path);
if dir.is_empty() {
0
} else {
1 + strings::count_char(dir, b'/')
}
}
for data in [
b"[dir]/[name].[ext]".as_slice(),
b"./js/[name].[ext]",
b"[hash]/[name].[ext]",
b"[dir]/[hash12]/[name]-[hash].[ext]",
b"./[name]-[hash].[ext]",
] {
for dir in [b"".as_slice(), b".", b"a/b", b"../x"] {
for sanitize_parent_dirs in [true, false] {
let mut template = PathTemplate {
data: Box::from(data),
placeholder: Placeholder {
dir: Box::from(dir),
name: Box::from(b"n".as_slice()),
ext: Box::from(b"js".as_slice()),
hash: None,
target: Box::default(),
},
};
let mut before = Vec::new();
template
.print_with_stand_in_hash(&mut before, sanitize_parent_dirs)
.unwrap();
template.placeholder.hash = Some(template.content_hash(u64::MAX));
let mut after = Vec::new();
template.print(&mut after, sanitize_parent_dirs).unwrap();
assert_eq!(
depth(&mut before),
depth(&mut after),
"{} with [dir] = {}",
String::from_utf8_lossy(data),
String::from_utf8_lossy(dir),
);
}
}
}
}

impl PathTemplate {
pub(crate) fn needs(&self, field: PlaceholderField) -> bool {
path_template_needs(&self.data, field)
Expand Down Expand Up @@ -2489,6 +2540,28 @@ impl PathTemplate {
)
}

/// [`Self::print`] with a stand-in for a `[hash]` not assigned yet. No hash has a separator, so the directory depth is final.
pub(crate) fn print_with_stand_in_hash<W: bun_io::Write>(
&self,
writer: &mut W,
sanitize_parent_dirs: bool,
) -> bun_io::Result<()> {
path_template_print(
writer,
&self.data,
&self.placeholder.dir,
&self.placeholder.name,
&self.placeholder.ext,
Some(
self.placeholder
.hash
.unwrap_or(bun_core::fmt::ContentHash::short(0)),
),
&self.placeholder.target,
sanitize_parent_dirs,
)
}

/// Max output path length. A `PathBuffer` also needs room for a `.map` or `.jsc` suffix and the NUL.
pub(crate) const MAX_OUTPUT_PATH_LEN: usize = bun_paths::MAX_PATH_BYTES - 1 - ".map".len();

Expand Down
2 changes: 2 additions & 0 deletions test/bake/dev/production.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ describe("production", () => {
// Check that the error message shows the proper source location
expect(buildStderr.toString()).toContain("throw new Error");
expect(buildStderr.toString()).toContain("oh no!");
// The frame names the source file from the working directory of the build.
expect(buildStderr.toString()).toContain("(pages/index.tsx:2:");
});

test("import.meta properties are inlined in production build", async () => {
Expand Down
Loading
Loading