diff --git a/docs/bundler/index.mdx b/docs/bundler/index.mdx index b9a1837b9a10..a455745d60de 100644 --- a/docs/bundler/index.mdx +++ b/docs/bundler/index.mdx @@ -963,7 +963,7 @@ The `naming` field customizes the names and locations of the generated files. It - `[name]` - The name of the entrypoint file, without the extension. - `[ext]` - The extension of the generated bundle. -- `[hash]` - A hash of the bundle contents: 8 lowercase alphanumeric characters. If two outputs with different contents would print the same characters, both get enough extra characters to differ (as do the outputs that reference them). `[hash9]` through `[hash13]` set a wider minimum; `[hash13]` is the full 64-bit hash. +- `[hash]` - A hash of the bundle contents: 8 lowercase alphanumeric characters. With a [`sourcemap`](#sourcemap), the contents include the source map, so an edit that changes only the map (a comment, for example) also changes the hash of that bundle and of every bundle that imports it. If two outputs with different contents would print the same characters, both get enough extra characters to differ (as do the outputs that reference them). `[hash9]` through `[hash13]` set a wider minimum; `[hash13]` is the full 64-bit hash. - `[dir]` - The relative path from the project root to the parent directory of the source file. For example: diff --git a/scripts/rust-miri.ts b/scripts/rust-miri.ts index c07f2f89bdbe..c5f07bb68c6d 100644 --- a/scripts/rust-miri.ts +++ b/scripts/rust-miri.ts @@ -54,6 +54,7 @@ const MIRI_CRATES = [ "bun_resolve_builtins", "bun_semver", "bun_shell_parser", + "bun_sourcemap", "bun_threading", "bun_url", "bun_wyhash", diff --git a/src/bundler/Chunk.rs b/src/bundler/Chunk.rs index e5ec3cb1d1dd..a51fe7ec6da7 100644 --- a/src/bundler/Chunk.rs +++ b/src/bundler/Chunk.rs @@ -272,6 +272,13 @@ impl<'a> Chunk<'a> { .unwrap_or_else(|| bun_core::fmt::ContentHash::short(self.isolated_hash)) } + /// Names this chunk and its source map. It covers what the final content hash covers. + pub(crate) fn debug_id(&self) -> source_map::DebugIDFormatter { + source_map::DebugIDFormatter { + id: self.id().value, + } + } + /// The chunks reachable from chunk `start` through cross-chunk imports of the given kinds, `start` first. pub(crate) fn reachable_chunks( chunks: &[Chunk], @@ -824,12 +831,7 @@ impl IntermediateOutput { let debug_id_len = if ENABLE_SOURCE_MAP_SHIFTS && FeatureFlags::SOURCE_MAP_DEBUG_ID { - bun_core::fmt::count(format_args!( - "\n//# debugId={}\n", - source_map::DebugIDFormatter { - id: chunk.isolated_hash - } - )) + bun_core::fmt::count(format_args!("\n//# debugId={}\n", chunk.debug_id())) } else { 0 }; @@ -1036,14 +1038,8 @@ impl IntermediateOutput { // This comment must go before the //# sourceMappingURL comment let mut cursor: &mut [u8] = remain; let before_len = cursor.len(); - write!( - &mut cursor, - "\n//# debugId={}\n", - source_map::DebugIDFormatter { - id: chunk.isolated_hash - } - ) - .unwrap_or_else(|_| panic!("unexpected NoSpaceLeft error from bufPrint")); + write!(&mut cursor, "\n//# debugId={}\n", chunk.debug_id()) + .unwrap_or_else(|_| panic!("unexpected NoSpaceLeft error from bufPrint")); let written = before_len - cursor.len(); remain = &mut remain[written..]; } @@ -1075,13 +1071,7 @@ impl IntermediateOutput { if ENABLE_SOURCE_MAP_SHIFTS && FeatureFlags::SOURCE_MAP_DEBUG_ID { // This comment must go before the //# sourceMappingURL comment let mut debug_id_fmt = Vec::new(); - let _ = write!( - &mut debug_id_fmt, - "\n//# debugId={}\n", - source_map::DebugIDFormatter { - id: chunk.isolated_hash - } - ); + let _ = write!(&mut debug_id_fmt, "\n//# debugId={}\n", chunk.debug_id()); let _ = arena; // Note: StringJoiner::done* allocates from global mimalloc; arena token is plumbing-only. break 'brk joiner.done_with_end(&debug_id_fmt)?; diff --git a/src/bundler/LinkerContext.rs b/src/bundler/LinkerContext.rs index 3f0643608158..d32f7578d3ac 100644 --- a/src/bundler/LinkerContext.rs +++ b/src/bundler/LinkerContext.rs @@ -7,9 +7,7 @@ use bun_ast::{Data, Loc, Log, Range, Source}; use bun_collections::{ArrayHashMap, AutoBitSet, HashMap, MultiArrayList, VecExt, index_sort}; use bun_core::{self as bun, FeatureFlags, Output}; use bun_core::{MutableString, string_joiner::StringJoiner, strings}; -use bun_sourcemap::{ - self as SourceMap, DebugIDFormatter, LineOffsetTable, SourceMapPieces, SourceMapState, -}; +use bun_sourcemap::{self as SourceMap, LineOffsetTable, SourceMapPieces, SourceMapState}; // Note: alias the *module* (not the `ThreadPool` struct) so // `ThreadPoolLib::Task` / `ThreadPoolLib::Batch` resolve as nested items. use crate::bake_types as bake; @@ -1208,7 +1206,6 @@ impl<'a> LinkerContext<'a> { pub(crate) fn generate_source_map_for_chunk( &mut self, - isolated_hash: u64, _worker: &mut crate::thread_pool::Worker, results: &MultiArrayList>, chunk_abs_dir: &[u8], @@ -1352,17 +1349,7 @@ impl<'a> LinkerContext<'a> { } let mapping_end = j.len; - if FeatureFlags::SOURCE_MAP_DEBUG_ID { - j.push_static(b"\",\n \"debugId\": \""); - let mut buf = Vec::::new(); - use std::io::Write; - write!(&mut buf, "{}", DebugIDFormatter { id: isolated_hash }) - .expect("infallible: in-memory write"); - j.push_owned(buf.into_boxed_slice()); - j.push_static(b"\",\n \"names\": []\n}"); - } else { - j.push_static(b"\",\n \"names\": []\n}"); - } + SourceMapPieces::push_suffix(&mut j); let done = j.done()?; debug_assert!(done[0] == b'{'); @@ -1924,6 +1911,11 @@ pub(crate) unsafe fn pending_part_range_prologue<'c, 'a>( } impl<'a> LinkerContext<'a> { + /// The `PathTemplate::print` argument for a chunk path, the same for its hash and its final path. + pub(crate) fn sanitize_parent_dirs(&self) -> bool { + !self.options.compile_mode.is_executable() + } + pub(crate) fn generate_isolated_hash(&mut self, chunk: &Chunk, arena: &'a Bump) -> u64 { let _trace = bun::perf::trace("Bundler.generateIsolatedHash"); @@ -1978,6 +1970,10 @@ impl<'a> LinkerContext<'a> { // any import to be considered different if the import's output path has changed. hasher.write(&chunk.template.data); + // esbuild's template has `[dir]`, `[name]` and `[ext]` filled in by now. Ours prints them here. + debug_assert!(chunk.template.placeholder.hash.is_none()); + hasher.write_path(&chunk.template, self.sanitize_parent_dirs()); + let public_path: &[u8] = if chunk .flags .contains(crate::chunk::Flags::IS_BROWSER_CHUNK_FROM_SERVER_BUILD) @@ -2009,8 +2005,19 @@ impl<'a> LinkerContext<'a> { // data in the spans between them. match &chunk.intermediate_output { crate::chunk::IntermediateOutput::Pieces(pieces) => { + // Standalone HTML prints the bytes of an asset here, not its path. + let inlined_assets = self.options.compile_mode.is_standalone_html().then(|| { + self.parse_graph() + .input_files + .items_content_hash_for_additional_file() + }); for piece in pieces.slice() { hasher.write(piece.data()); + if let Some(content_hashes) = inlined_assets + && piece.query.kind() == crate::chunk::QueryKind::Asset + { + hasher.write(&content_hashes[piece.query.index() as usize].to_ne_bytes()); + } } } crate::chunk::IntermediateOutput::Joiner(joiner) => { @@ -2041,6 +2048,9 @@ impl<'a> LinkerContext<'a> { hasher.write(&chunk.output_source_map.mappings); hasher.write(&chunk.output_source_map.suffix); + // The mode decides what follows the code: a debug ID, a link to the map, or the map itself. + hasher.write(&[chunk.content.sourcemap(self.options.source_maps) as u8]); + hasher.digest() } @@ -2580,11 +2590,11 @@ impl<'a> LinkerContext<'a> { } /// Each chunk's final content hash: its own isolated hash (plus the - /// asset paths its output pieces reference) and that of every chunk it + /// asset paths and chunks its output pieces name) and that of every chunk it /// transitively reaches through cross-chunk imports or output pieces, so a /// change anywhere below a chunk renames it. Cycles are fine: reachability /// is a fixpoint over bitsets, and each chunk digests its own hash first, - /// then the rest of its closure in chunk order (so two chunks that reach + /// then the sum of the rest of its closure (so two chunks that reach /// each other still differ). /// /// The width is the template's (`[hash]` = 8, `[hashN]` = N) unless two @@ -2636,13 +2646,19 @@ impl<'a> LinkerContext<'a> { } } crate::chunk::QueryKind::Chunk | crate::chunk::QueryKind::ChunkId => { - out.push(piece.query.index()) + let chunk_index = piece.query.index(); + out.push(chunk_index); + // Two pieces can trade chunks and keep the closure. esbuild keeps the name then. + hash.write(&chunks[chunk_index as usize].isolated_hash.to_ne_bytes()); } crate::chunk::QueryKind::Scb => { let chunk_index = self.graph.files.items_entry_point_chunk_index() [piece.query.index() as usize]; if chunk_index != u32::MAX { out.push(chunk_index); + hash.write( + &chunks[chunk_index as usize].isolated_hash.to_ne_bytes(), + ); } } crate::chunk::QueryKind::None | crate::chunk::QueryKind::HtmlImport => {} @@ -2682,14 +2698,17 @@ impl<'a> LinkerContext<'a> { .iter() .enumerate() .map(|(i, bits)| { - let mut hash = ContentHasher::default(); - hash.write(&own[i].to_ne_bytes()); + // A sum: one more entry point can put the same chunks in another order. + let mut rest: u64 = 0; let mut iter = bits.iterator::(); while let Some(j) = iter.next() { if j != i { - hash.write(&own[j].to_ne_bytes()); + rest = rest.wrapping_add(own[j]); } } + let mut hash = ContentHasher::default(); + hash.write(&own[i].to_ne_bytes()); + hash.write(&rest.to_ne_bytes()); hash.digest() }) .collect(); diff --git a/src/bundler/bundle_v2.rs b/src/bundler/bundle_v2.rs index 4ca977c4bf0d..d1e47fc5d1e7 100644 --- a/src/bundler/bundle_v2.rs +++ b/src/bundler/bundle_v2.rs @@ -8042,7 +8042,6 @@ pub mod bv2_impl { Css { result: crate::Result>, source_index: IndexInt, - source_map: Option, }, Html { source_index: IndexInt, @@ -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, } } } @@ -8149,6 +8147,46 @@ pub mod bv2_impl { bun_core::scoped_log!(ContentHasher, "HASH_UPDATE: {:?}\n", i); self.hasher.update(bun_core::slice_as_bytes(i)); } + /// Hashes the path `template` prints with `/` separators. A template without its hash prints no `[hash]`. + pub(crate) fn write_path( + &mut self, + template: &options::PathTemplate, + sanitize_parent_dirs: bool, + ) { + struct Path<'a> { + hasher: &'a mut bun_hash::XxHash64Streaming, + len: u64, + } + impl bun_io::Write for Path<'_> { + fn write_all(&mut self, bytes: &[u8]) -> bun_io::Result<()> { + bun_core::scoped_log!( + ContentHasher, + "HASH_UPDATE path: {}\n", + bstr::BStr::new(bytes) + ); + self.len += bytes.len() as u64; + let mut remain = bytes; + if bun_paths::SEP != b'/' { + while let Some(i) = strings::index_of_char_usize(remain, bun_paths::SEP) { + self.hasher.update(&remain[..i]); + self.hasher.update(b"/"); + remain = &remain[i + 1..]; + } + } + self.hasher.update(remain); + Ok(()) + } + } + let mut path = Path { + hasher: &mut self.hasher, + len: 0, + }; + template + .print(&mut path, sanitize_parent_dirs) + .expect("infallible: a hasher takes any bytes"); + let len = path.len; + self.hasher.update(&len.to_ne_bytes()); + } pub(crate) fn digest(&self) -> u64 { self.hasher.digest() } diff --git a/src/bundler/linker_context/generateChunksInParallel.rs b/src/bundler/linker_context/generateChunksInParallel.rs index 87be060506b5..68ed546f3a71 100644 --- a/src/bundler/linker_context/generateChunksInParallel.rs +++ b/src/bundler/linker_context/generateChunksInParallel.rs @@ -390,7 +390,7 @@ pub(crate) fn generate_chunks_in_parallel<'a, const IS_DEV_SERVER: bool>( // runtime bunfs references to out-of-root entrypoints resolve. chunk .template - .print(&mut rel_path, !c.options.compile_mode.is_executable()) + .print(&mut rel_path, c.sanitize_parent_dirs()) .expect("write to Vec"); path::resolve_path::platform_to_posix_in_place::(&mut rel_path); @@ -764,9 +764,10 @@ pub(crate) fn generate_chunks_in_parallel<'a, const IS_DEV_SERVER: bool>( match sourcemap_option { tag @ (SourceMapOption::External | SourceMapOption::Linked) => { + let debug_id = chunks[ci].debug_id(); let output_source_map = chunks[ci] .output_source_map - .finalize(&code_result.shifts) + .finalize(&code_result.shifts, debug_id) .expect("Failed to allocate memory for external source map"); if tag == SourceMapOption::Linked { @@ -831,9 +832,10 @@ pub(crate) fn generate_chunks_in_parallel<'a, const IS_DEV_SERVER: bool>( standalone_sourcemaps[ci] = Some(output_source_map); } SourceMapOption::Inline => { + let debug_id = chunks[ci].debug_id(); let output_source_map = chunks[ci] .output_source_map - .finalize(&code_result.shifts) + .finalize(&code_result.shifts, debug_id) .expect("Failed to allocate memory for inline source map"); let encode_len = bun_base64::encode_len(&output_source_map); @@ -1018,9 +1020,10 @@ pub(crate) fn generate_chunks_in_parallel<'a, const IS_DEV_SERVER: bool>( match chunk.content.sourcemap(c.options.source_maps) { tag @ (SourceMapOption::External | SourceMapOption::Linked) => { + let debug_id = chunk.debug_id(); let output_source_map = chunk .output_source_map - .finalize(&code_result.shifts) + .finalize(&code_result.shifts, debug_id) .expect("Failed to allocate memory for external source map"); let mut source_map_final_rel_path: Vec = Vec::with_capacity(chunk.final_rel_path.len() + b".map".len()); @@ -1068,9 +1071,10 @@ pub(crate) fn generate_chunks_in_parallel<'a, const IS_DEV_SERVER: bool>( })); } SourceMapOption::Inline => { + let debug_id = chunk.debug_id(); let output_source_map = chunk .output_source_map - .finalize(&code_result.shifts) + .finalize(&code_result.shifts, debug_id) .expect("Failed to allocate memory for external source map"); let encode_len = bun_base64::encode_len(&output_source_map); diff --git a/src/bundler/linker_context/generateCompileResultForCssChunk.rs b/src/bundler/linker_context/generateCompileResultForCssChunk.rs index 27b06097537e..e727f90a4cc1 100644 --- a/src/bundler/linker_context/generateCompileResultForCssChunk.rs +++ b/src/bundler/linker_context/generateCompileResultForCssChunk.rs @@ -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) => { @@ -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, }; } } @@ -166,7 +163,6 @@ fn generate_compile_result_for_css_chunk_impl( CompileResult::Css { result: Ok(output), source_index: idx.get(), - source_map: None, } } } diff --git a/src/bundler/linker_context/postProcessCSSChunk.rs b/src/bundler/linker_context/postProcessCSSChunk.rs index 20aa7d039457..84ceaaf443ee 100644 --- a/src/bundler/linker_context/postProcessCSSChunk.rs +++ b/src/bundler/linker_context/postProcessCSSChunk.rs @@ -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( @@ -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 @@ -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 = - 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(); @@ -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. @@ -81,15 +65,12 @@ 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() { @@ -100,28 +81,6 @@ pub(crate) fn post_process_css_chunk( // 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 @@ -143,21 +102,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(()) } diff --git a/src/bundler/linker_context/postProcessJSChunk.rs b/src/bundler/linker_context/postProcessJSChunk.rs index 1455395e4bc6..0034be143680 100644 --- a/src/bundler/linker_context/postProcessJSChunk.rs +++ b/src/bundler/linker_context/postProcessJSChunk.rs @@ -780,7 +780,6 @@ pub(crate) fn post_process_js_chunk( // TODO: meta contents - chunk.isolated_hash = c.generate_isolated_hash(chunk, worker_arena); chunk .flags .set(crate::chunk::Flags::IS_EXECUTABLE, is_executable); @@ -795,7 +794,6 @@ pub(crate) fn post_process_js_chunk( // `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, &compile_results_for_source_map, &resolver.opts.output_dir, @@ -803,6 +801,9 @@ pub(crate) fn post_process_js_chunk( )?; } + // After the source map, which the hash covers. + chunk.isolated_hash = c.generate_isolated_hash(chunk, worker_arena); + // Free the renamer now that printing is done; nothing reads it after // post-processing. chunk.renamer = Default::default(); diff --git a/src/bundler/linker_context/writeOutputFilesToDisk.rs b/src/bundler/linker_context/writeOutputFilesToDisk.rs index 82403fa9e140..88eb1d8f72a8 100644 --- a/src/bundler/linker_context/writeOutputFilesToDisk.rs +++ b/src/bundler/linker_context/writeOutputFilesToDisk.rs @@ -292,9 +292,10 @@ pub(crate) fn write_output_files_to_disk( match chunk.content.sourcemap(c.options.source_maps) { tag @ (SourceMapOption::External | SourceMapOption::Linked) => { + let debug_id = chunk.debug_id(); let output_source_map = chunk .output_source_map - .finalize(&code_result.shifts) + .finalize(&code_result.shifts, debug_id) .unwrap_or_else(|_| { panic!("Failed to allocate memory for external source map") }); @@ -362,9 +363,10 @@ pub(crate) fn write_output_files_to_disk( })); } SourceMapOption::Inline => { + let debug_id = chunk.debug_id(); let output_source_map = chunk .output_source_map - .finalize(&code_result.shifts) + .finalize(&code_result.shifts, debug_id) .unwrap_or_else(|_| { panic!("Failed to allocate memory for external source map") }); diff --git a/src/sourcemap/lib.rs b/src/sourcemap/lib.rs index 5ba27ee24a31..bf1dddb96607 100644 --- a/src/sourcemap/lib.rs +++ b/src/sourcemap/lib.rs @@ -322,6 +322,11 @@ pub struct DebugIDFormatter { pub id: u64, } +impl DebugIDFormatter { + /// How many characters a debug ID prints. + pub const LEN: usize = 32; +} + impl core::fmt::Display for DebugIDFormatter { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { // The RFC asks for a UUID (128 bits / 32 hex chars). Our hashes are 64 @@ -714,10 +719,45 @@ impl SourceMapPieces { SourceMapPieces::default() } + const BEFORE_DEBUG_ID: &'static [u8] = b"\",\n \"debugId\": \""; + const DEBUG_ID_PLACEHOLDER: &'static [u8] = &[b'0'; DebugIDFormatter::LEN]; + const AFTER_DEBUG_ID: &'static [u8] = b"\",\n \"names\": []\n}"; + + /// What follows the mappings, with a placeholder for the debug ID that `finalize` writes. + pub fn push_suffix(j: &mut bun_core::string_joiner::StringJoiner<'_>) { + if bun_core::FeatureFlags::SOURCE_MAP_DEBUG_ID { + j.push_static(Self::BEFORE_DEBUG_ID); + j.push_static(Self::DEBUG_ID_PLACEHOLDER); + } + j.push_static(Self::AFTER_DEBUG_ID); + } + pub fn finalize( &mut self, shifts_: &[SourceMapShifts], + debug_id: DebugIDFormatter, ) -> Result, bun_alloc::AllocError> { + if bun_core::FeatureFlags::SOURCE_MAP_DEBUG_ID { + use std::io::Write as _; + + // Written in place: a map that was not split is one buffer, returned below as it is. + let end = if self.suffix.is_empty() { + &mut self.prefix + } else { + &mut self.suffix + }; + let id_end = end.len().saturating_sub(Self::AFTER_DEBUG_ID.len()); + let slot = end[..id_end].last_chunk_mut::<{ DebugIDFormatter::LEN }>(); + debug_assert!( + slot.as_deref() + .is_some_and(|slot| slot == Self::DEBUG_ID_PLACEHOLDER) + ); + if let Some(slot) = slot { + let mut slot: &mut [u8] = slot; + write!(slot, "{debug_id}").expect("a debug ID prints DebugIDFormatter::LEN bytes"); + } + } + // Nothing to splice: avoid re-joining (and re-allocating) the entire // source-map JSON when there are no mappings or suffix to merge in. if self.mappings.is_empty() && self.suffix.is_empty() { @@ -1150,3 +1190,47 @@ fn find_source_mapping_url_u16(source: &[u16]) -> Option SourceMapPieces { + let mut j = bun_core::string_joiner::StringJoiner::default(); + j.push_static(b"{\n \"mappings\": \""); + let mapping_start = j.len; + j.push_static(b"AAAA"); + let mapping_end = j.len; + SourceMapPieces::push_suffix(&mut j); + let done = j.done().unwrap(); + + let mut pieces = SourceMapPieces::init(); + if split { + pieces.prefix.extend_from_slice(&done[..mapping_start]); + pieces + .mappings + .extend_from_slice(&done[mapping_start..mapping_end]); + pieces.suffix.extend_from_slice(&done[mapping_end..]); + } else { + pieces.prefix = done.into_vec(); + } + pieces + } + + #[test] + fn finalize_writes_the_debug_id_in_place() { + let id = DebugIDFormatter { + id: 0x0123_4567_89AB_CDEF, + }; + let json: &[u8] = b"{\n \"mappings\": \"AAAA\",\n \"debugId\": \"0123456789ABCDEF64756E2164756E21\",\n \"names\": []\n}"; + + assert_eq!(&*pieces(true).finalize(&[], id).unwrap(), json); + + // A map that was not split is one buffer, which can be hundreds of megabytes. + let mut whole = pieces(false); + let buffer = whole.prefix.as_ptr(); + let finalized = whole.finalize(&[], id).unwrap(); + assert_eq!(&*finalized, json); + assert_eq!(finalized.as_ptr(), buffer); + } +} diff --git a/test/bundler/__snapshots__/bun-build-api.test.ts.snap b/test/bundler/__snapshots__/bun-build-api.test.ts.snap index 9c1e219a9291..87fa5b73f764 100644 --- a/test/bundler/__snapshots__/bun-build-api.test.ts.snap +++ b/test/bundler/__snapshots__/bun-build-api.test.ts.snap @@ -66,14 +66,14 @@ NS.then(({ fn }) => { " `; -exports[`Bun.build BuildArtifact properties: hash 1`] = `"xccrrspv"`; +exports[`Bun.build BuildArtifact properties: hash 1`] = `"a6745pw5"`; -exports[`Bun.build BuildArtifact properties + entry.naming: hash 1`] = `"jvbamvbx"`; - -exports[`Bun.build BuildArtifact properties sourcemap: hash index.js 1`] = `"xccrrspv"`; +exports[`Bun.build BuildArtifact properties + entry.naming: hash 1`] = `"zaa2wmv7"`; exports[`Bun.build BuildArtifact properties sourcemap: hash index.js.map 1`] = `"00000000"`; +exports[`Bun.build BuildArtifact properties sourcemap: hash in memory 1`] = `"8q8sck9q"`; + exports[`Bun.build new Response(BuildArtifact) sets content type: response text 1`] = ` "var __defProp = Object.defineProperty; var __returnValue = (v) => v; diff --git a/test/bundler/bun-build-api.test.ts b/test/bundler/bun-build-api.test.ts index 3726e1144c7e..88e3ac8adf41 100644 --- a/test/bundler/bun-build-api.test.ts +++ b/test/bundler/bun-build-api.test.ts @@ -876,17 +876,17 @@ describe("Bun.build", () => { const outdir = tempDirWithFiles("build-artifact-properties-sourcemap", { "package.json": `{}`, }); - const x = await Bun.build({ - entrypoints: [join(import.meta.dir, "./fixtures/trivial/index.js")], - sourcemap: "external", - outdir, - }); + const build = (options: { sourcemap: "none" | "external"; outdir?: string }) => + Bun.build({ entrypoints: [join(import.meta.dir, "./fixtures/trivial/index.js")], ...options }); + const x = await build({ sourcemap: "external", outdir }); const [blob, map] = x.outputs; expect(blob.type).toBe("text/javascript;charset=utf-8"); expect(blob.size).toBeGreaterThan(1); expect(path.relative(outdir, blob.path)).toBe("index.js"); - expect(blob.hash).toBeTruthy(); - expect(blob.hash).toMatchSnapshot("hash index.js"); + // The hash covers the source map, and the `sources` of the map depend on where the temp outdir is. + // So no value is pinned here. An output without a hash prints zeros. + expect(blob.hash).toMatch(/^[0-9a-z]{8}$/); + expect(blob.hash).not.toBe("00000000"); expect(blob.kind).toBe("entry-point"); expect(blob.loader).toBe("jsx"); expect(blob.sourcemap).toBe(map); @@ -899,6 +899,12 @@ describe("Bun.build", () => { expect(map.kind).toBe("sourcemap"); expect(map.loader).toBe("file"); expect(map.sourcemap).toBe(null); + + // The same build gives the same hash, and a build without a source map gives another one. + expect((await build({ sourcemap: "external", outdir })).outputs[0].hash).toBe(blob.hash); + expect((await build({ sourcemap: "none", outdir })).outputs[0].hash).not.toBe(blob.hash); + // An in-memory build has no temp path in its `sources`, so its hash has a snapshot. + expect((await build({ sourcemap: "external" })).outputs[0].hash).toMatchSnapshot("hash in memory"); Bun.gc(true); }); diff --git a/test/bundler/bundler_naming.test.ts b/test/bundler/bundler_naming.test.ts index 3e65de2c24b5..7a858cb0c2d1 100644 --- a/test/bundler/bundler_naming.test.ts +++ b/test/bundler/bundler_naming.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "bun:test"; import { bunEnv, bunExe, tempDir } from "harness"; -import { readdirSync } from "node:fs"; +import { readdirSync, writeFileSync } from "node:fs"; +import { basename, join } from "node:path"; import { ESBUILD, itBundled } from "./expectBundled"; describe("bundler", () => { @@ -417,6 +418,328 @@ describe("bundler", () => { }); }); +// `[hash]` is a hash of what an output contains, so two builds never write two +// contents to one path. A debug ID names a chunk and its source map the same way. +describe("bundler", () => { + const hashed = { entry: "[name]-[hash].[ext]", chunk: "[name]-[hash].[ext]", asset: "[name]-[hash].[ext]" }; + const page = ``; + + // The text of each output, by path. + async function build(config: Bun.BuildConfig) { + const { outputs } = await Bun.build(config); + const texts: Record = {}; + for (const output of outputs) texts[output.path] = await output.text(); + return texts; + } + + // The paths that both builds wrote, with different contents. + function pathsWithTwoContents(first: Record, second: Record) { + return Object.keys(first).filter(path => path in second && first[path] !== second[path]); + } + + // The outputs that have a hash in their name. + function hashedOutputs(texts: Record) { + return Object.fromEntries(Object.entries(texts).filter(([path]) => /-[0-9a-z]{8}\./.test(path))); + } + + // The debug ID of each JavaScript chunk, in output order. A chunk prints its ID, and so does its own source map. + function debugIds(texts: Record) { + const maps = Object.keys(texts).filter(path => path.endsWith(".map")); + return Object.entries(texts).flatMap(([path, code]) => { + const id = code.match(/^\/\/# debugId=([0-9A-F]{32})$/m)?.[1]; + if (!id) return []; + const inline = code.match(/sourceMappingURL=data:application\/json;base64,([\w+/=]+)/)?.[1]; + // A standalone HTML file holds its one chunk, and the map keeps the name of that chunk. + const map = inline ? Buffer.from(inline, "base64").toString() : (texts[path + ".map"] ?? texts[maps[0]]); + expect(JSON.parse(map).debugId).toBe(id); + return [id]; + }); + } + + test.concurrent( + "naming/ChunkHashCoversChunkPath (a new entry point moves the [dir] of an imported one)", + async () => { + using dir = tempDir("naming-chunk-hash-dir", { + "src/router.js": `export const open = () => import("./pages/a.js");`, + "src/pages/a.js": `import { open } from "../router.js";\nexport const a = open;`, + "src/pages/b.js": `import { open } from "../router.js";\nexport const b = open;`, + "src/index.js": `console.log("index");`, + }); + // [dir] is relative to the root, and the default root is the directory that all entry points share. + const withEntrypoints = (...entrypoints: string[]) => + build({ entrypoints: entrypoints.map(file => join(String(dir), file)), splitting: true }); + const first = await withEntrypoints("src/pages/a.js", "src/pages/b.js"); + const second = await withEntrypoints("src/pages/a.js", "src/pages/b.js", "src/index.js"); + + // Both builds put router.js in a chunk with the default name, chunk-[hash].js. + expect(Object.values(first).join("\n")).toContain(`import("./a.js")`); + expect(Object.values(second).join("\n")).toContain(`import("./pages/a.js")`); + expect(pathsWithTwoContents(first, second)).toEqual([]); + }, + ); + + test.concurrent( + "naming/ChunkHashCoversChunkPath (a plugin gives an imported entry point another [name])", + async () => { + using dir = tempDir("naming-chunk-hash-name", { + "entry.js": `import("./lazy.js").then(lazy => console.log(lazy.value));`, + "lazy.js": `export const value = 1;`, + }); + const withName = (name: string) => + build({ + entrypoints: [join(String(dir), "entry.js"), name], + splitting: true, + naming: hashed, + plugins: [ + { + name: "virtual", + setup(builder) { + builder.onResolve({ filter: /^virtual-/ }, () => ({ path: join(String(dir), "lazy.js") })); + }, + }, + ], + }); + const first = await withName("virtual-a"); + const second = await withName("virtual-b"); + + expect(Object.values(first).join("\n")).toMatch(/import\("\.\/virtual-a-\w+\.js"\)/); + expect(Object.values(second).join("\n")).toMatch(/import\("\.\/virtual-b-\w+\.js"\)/); + expect(pathsWithTwoContents(first, second)).toEqual([]); + }, + ); + + test.concurrent("naming/ChunkHashCoversChunkPath (the [dir] of the chunk itself moves)", async () => { + using dir = tempDir("naming-chunk-hash-own-dir", { + "src/shared.js": `export const shared = "shared";`, + "src/pages/a.js": `import { shared } from "../shared.js";\nconsole.log("a", shared);`, + "src/pages/b.js": `import { shared } from "../shared.js";\nconsole.log("b", shared);`, + }); + // The chunk of a.js imports the shared chunk by a path that is relative to its own directory. + const withRoot = async (root: string) => { + const { outputs } = await Bun.build({ + entrypoints: [join(String(dir), "src/pages/a.js"), join(String(dir), "src/pages/b.js")], + root: join(String(dir), root), + splitting: true, + naming: { entry: "[dir]/[name]-[hash].[ext]" }, + }); + const a = outputs.find(output => basename(output.path).startsWith("a-"))!; + return { hash: a.hash, sharedChunkIn: (await a.text()).match(/from "(.+)chunk-\w+\.js"/)![1] }; + }; + const first = await withRoot("src/pages"); + const second = await withRoot("src"); + + expect([first.sharedChunkIn, second.sharedChunkIn]).toEqual(["./", "../"]); + expect(second.hash).not.toBe(first.hash); + }); + + test.concurrent( + "naming/ChunkHashCoversChunkReference (an import() gets a chunk that another import() has)", + async () => { + using dir = tempDir("naming-chunk-hash-retarget", { + "router.js": "", + "pages/a.js": `import { first, second, third } from "../router.js";\nexport const a = [first, second, third];`, + "pages/b.js": `import { first, second, third } from "../router.js";\nexport const b = [third, second, first];`, + "x.js": `export const name = "x";`, + "y.js": `export const name = "y";`, + }); + const withSecond = async (target: "x" | "y") => { + writeFileSync( + join(String(dir), "router.js"), + `export const first = () => import("./x.js"); + export const second = () => import("./${target}.js"); + export const third = () => import("./y.js");`, + ); + // The chunks of router.js, x.js and y.js keep the default name, chunk-[hash].js. + const texts = await build({ + entrypoints: [join(String(dir), "pages/a.js"), join(String(dir), "pages/b.js")], + splitting: true, + naming: { entry: "[name]-[hash].[ext]" }, + }); + // Which of x.js and y.js each import() loads, in the order that the chunk of router.js prints them. + const loads = Array.from( + Object.values(texts) + .join("\n") + .matchAll(/import\("(.+?)"\)/g), + ([, path]) => texts[path].match(/"(x|y)"/)![1], + ); + return { texts, loads }; + }; + const first = await withSecond("y"); + const second = await withSecond("x"); + + expect([first.loads, second.loads]).toEqual([ + ["x", "y", "y"], + ["x", "x", "y"], + ]); + expect(pathsWithTwoContents(first.texts, second.texts)).toEqual([]); + // The chunks of x.js and y.js did not change, so they are the two outputs that keep their name. + const kept = Object.keys(first.texts).filter(path => path in second.texts); + expect(kept.map(path => first.texts[path].match(/"(x|y)"/)?.[1]).sort()).toEqual(["x", "y"]); + }, + ); + + for (const sourcemap of ["none", "external", "linked", "inline"] as const) { + test.concurrent(`naming/ChunkHashIgnoresChunkOrder (sourcemap: ${sourcemap})`, async () => { + const routes = ["r1", "r2", "r3", "r4", "r5", "r6", "r7"]; + using dir = tempDir("naming-chunk-hash-order", { + "main.js": `export const shared = "shared"; + export const routes = [${routes.map(route => `() => import("./${route}.js")`).join(", ")}];`, + ...Object.fromEntries( + routes.map(route => [ + `${route}.js`, + `import { shared } from "./main.js";\nexport default "${route} " + shared;`, + ]), + ), + "other.js": `console.log("other");`, + }); + // main.js and its seven routes are eight entry points. The ninth puts the chunks in another order. + const withEntrypoints = (...entrypoints: string[]) => + build({ entrypoints: entrypoints.map(file => join(String(dir), file)), splitting: true, sourcemap }); + const first = hashedOutputs(await withEntrypoints("main.js")); + const second = await withEntrypoints("main.js", "other.js"); + + // The chunk of each route, and the chunk that the routes import `shared` from. + expect(Object.keys(first).filter(path => path.endsWith(".js"))).toHaveLength(8); + expect(second).toMatchObject(first); + }); + } + + for (const sourcemap of ["external", "linked", "inline"] as const) { + for (const to of ["memory", "disk", "standalone HTML"] as const) { + test.concurrent(`naming/ChunkHashCoversSourceMap (sourcemap: ${sourcemap}, to ${to})`, async () => { + using dir = tempDir("naming-chunk-hash-sourcemap", { "index.html": page, "entry.js": "" }); + // Only the comment differs, so only the source map differs. + const withComment = (comment: string) => { + writeFileSync(join(String(dir), "entry.js"), `// ${comment}\nconsole.log("x");\n`); + return build({ + naming: hashed, + sourcemap, + ...(to === "standalone HTML" + ? { entrypoints: [join(String(dir), "index.html")], compile: true, target: "browser" } + : { + entrypoints: [join(String(dir), "entry.js")], + outdir: to === "disk" ? join(String(dir), "out") : undefined, + }), + }); + }; + const first = await withComment("first comment"); + const second = await withComment("second, longer comment"); + + expect(pathsWithTwoContents(first, second)).toEqual([]); + expect(debugIds(first)).toHaveLength(1); + expect(debugIds(second)).not.toEqual(debugIds(first)); + }); + } + } + + test.concurrent("naming/ChunkHashCoversSourceMap (without a source map, a comment changes no output)", async () => { + using dir = tempDir("naming-chunk-hash-comment", { "entry.js": "" }); + const withComment = (comment: string) => { + writeFileSync(join(String(dir), "entry.js"), `// ${comment}\nconsole.log("x");\n`); + return build({ entrypoints: [join(String(dir), "entry.js")], naming: hashed }); + }; + const first = await withComment("first comment"); + const second = await withComment("second, longer comment"); + + expect(second).toEqual(first); + }); + + test.concurrent("naming/ChunkHashCoversSourceMap (a stylesheet has no source map)", async () => { + using dir = tempDir("naming-chunk-hash-css", { "style.css": `body { color: red; }` }); + const withSourcemap = (sourcemap: "none" | "linked") => + build({ entrypoints: [join(String(dir), "style.css")], naming: hashed, sourcemap }); + + expect(await withSourcemap("linked")).toEqual(await withSourcemap("none")); + }); + + test.concurrent("naming/ChunkHashCoversSourceMapMode", async () => { + using dir = tempDir("naming-chunk-hash-mode", { "entry.js": `console.log("x");` }); + // Each mode ends the chunk in its own way: nothing, a debug ID, a link to the map, or the map. + const chunks: string[] = []; + for (const sourcemap of ["none", "external", "linked", "inline"] as const) { + const texts = await build({ entrypoints: [join(String(dir), "entry.js")], naming: hashed, sourcemap }); + chunks.push(...Object.keys(texts).filter(path => path.endsWith(".js"))); + } + + expect(new Set(chunks).size).toBe(4); + }); + + for (const sourcemap of ["external", "linked", "inline"] as const) { + test.concurrent(`naming/DebugIdCoversAssetPath (sourcemap: ${sourcemap})`, async () => { + using dir = tempDir("naming-debug-id", { + "entry.js": `import logo from "./logo.png";\nconsole.log(logo);`, + "logo.png": "", + }); + // The chunk prints the path of the asset, and that path has the hash of the asset. + const withLogo = async (bytes: string) => { + writeFileSync(join(String(dir), "logo.png"), bytes); + const texts = await build({ entrypoints: [join(String(dir), "entry.js")], naming: hashed, sourcemap }); + return { logo: Object.keys(texts).find(path => path.endsWith(".png")), debugIds: debugIds(texts) }; + }; + const first = await withLogo("A"); + const second = await withLogo("B"); + + expect(second.logo).not.toBe(first.logo); + expect(first.debugIds).toHaveLength(1); + expect(second.debugIds).not.toEqual(first.debugIds); + }); + + for (const to of ["memory", "disk"] as const) { + test.concurrent(`naming/DebugIdOfEachChunk (sourcemap: ${sourcemap}, to ${to})`, async () => { + using dir = tempDir("naming-debug-id-chunks", { + "shared.js": "", + "a.js": `import { shared } from "./shared.js";\nconsole.log("a", shared);`, + "b.js": `import { shared } from "./shared.js";\nconsole.log("b", shared);`, + }); + // The chunks of a.js and b.js print the path of the chunk of shared.js, which has the hash of that chunk. + const withShared = async (value: string) => { + writeFileSync(join(String(dir), "shared.js"), `export const shared = "${value}";`); + return debugIds( + await build({ + entrypoints: [join(String(dir), "a.js"), join(String(dir), "b.js")], + splitting: true, + naming: hashed, + sourcemap, + outdir: to === "disk" ? join(String(dir), "out") : undefined, + }), + ); + }; + const first = await withShared("one"); + const second = await withShared("two"); + + expect(new Set(first).size).toBe(3); + expect(first.filter((id, chunk) => id === second[chunk])).toEqual([]); + }); + } + } + + test.concurrent("naming/ChunkHashCoversInlinedAsset (standalone HTML)", async () => { + using dir = tempDir("naming-chunk-hash-standalone", { + "index.html": page, + "entry.js": `import logo from "./logo.png";\nconsole.log(logo);`, + "logo.png": "", + }); + // Without a [hash] in the asset name, the asset has one path for any contents. + const withLogo = async (bytes: string) => { + writeFileSync(join(String(dir), "logo.png"), bytes); + const texts = await build({ + entrypoints: [join(String(dir), "index.html")], + compile: true, + target: "browser", + naming: { entry: "[name]-[hash].[ext]", asset: "[name].[ext]" }, + }); + expect(Object.values(texts).join("\n")).toContain( + `data:image/png;base64,${Buffer.from(bytes).toString("base64")}`, + ); + return texts; + }; + const first = await withLogo("A"); + const second = await withLogo("B"); + + expect(pathsWithTwoContents(first, second)).toEqual([]); + }); +}); + // Bun.build({ naming: "[name" }) used to SIGABRT; validation now rejects it. describe("bundler", () => { for (const [naming, option] of [ diff --git a/test/bundler/bundler_promiseall_deadcode.test.ts b/test/bundler/bundler_promiseall_deadcode.test.ts index c5960784c32a..dc499046ea4e 100644 --- a/test/bundler/bundler_promiseall_deadcode.test.ts +++ b/test/bundler/bundler_promiseall_deadcode.test.ts @@ -150,7 +150,7 @@ describe("bundler", () => { await Promise.resolve(); AsyncEntryPoint(); - //# debugId=B9EF7E5F2ACBD11D64756E2164756E21 + //# debugId=6A58830872B12DFA64756E2164756E21 //# sourceMappingURL=out.js.map " `); @@ -379,7 +379,7 @@ describe("bundler", () => { await Promise.resolve(); AsyncEntryPoint(); - //# debugId=6678C3B13A630A4064756E2164756E21 + //# debugId=2AAB94494647B3DB64756E2164756E21 //# sourceMappingURL=out.js.map " `); diff --git a/test/bundler/bundler_splitting.test.ts b/test/bundler/bundler_splitting.test.ts index 120d1617be3e..a5f8c9b6640c 100644 --- a/test/bundler/bundler_splitting.test.ts +++ b/test/bundler/bundler_splitting.test.ts @@ -4847,7 +4847,12 @@ describe("bundler", () => { expect(result.logs).toBeEmpty(); const admin = result.outputs.find(o => o.path.replaceAll("\\", "/").endsWith("pages/admin/index.js"))!; const map = result.outputs.find(o => o.path === admin.path + ".map"); - return { code: await admin.text(), map: map && JSON.parse(await map.text()) }; + const code = await admin.text(); + // The code without the hashes of the chunks and without the debug ID, which come from the hash. + const references = result.outputs + .reduce((text, output) => text.replaceAll(output.hash!, "[hash]"), code) + .replace(/debugId=\w+/, "debugId=[id]"); + return { code, references, map: map && JSON.parse(await map.text()) }; } // `util("admin")` follows the dynamic import on the same line, so the map @@ -4905,9 +4910,11 @@ describe("bundler", () => { using dir = tempDir("splitting-reference-paths-outdir", files); const inMemory = await buildAdminEntry(String(dir), { sourcemap: "linked" }); const onDisk = await buildAdminEntry(String(dir), { sourcemap: "linked", outdir: join(String(dir), "out") }); - expect(onDisk.code).toBe(inMemory.code); + // The two builds write different `sources` in their maps, and the hash of a chunk covers its map. + // So the hashes differ, and nothing else does. + expect(onDisk.references).toBe(inMemory.references); expect(onDisk.map.mappings).toBe(inMemory.map.mappings); - expect(readFileSync(join(String(dir), "out", "pages", "admin", "index.js"), "utf8")).toBe(inMemory.code); + expect(readFileSync(join(String(dir), "out", "pages", "admin", "index.js"), "utf8")).toBe(onDisk.code); await expectUtilCallToBeMapped(onDisk.code, onDisk.map); }); }); diff --git a/test/bundler/html-import-manifest.test.ts b/test/bundler/html-import-manifest.test.ts index 2698ff6936ef..cf113d6b14da 100644 --- a/test/bundler/html-import-manifest.test.ts +++ b/test/bundler/html-import-manifest.test.ts @@ -141,11 +141,11 @@ console.log(favicon); "files": [ { "input": "client.html", - "path": "./client-qjznw47b.js", + "path": "./client-emkzgtn9.js", "loader": "js", "isEntry": true, "headers": { - "etag": "l5IfNVyE54s", + "etag": "zpTzn5CadSk", "content-type": "text/javascript;charset=utf-8" } }, @@ -155,17 +155,17 @@ console.log(favicon); "loader": "html", "isEntry": true, "headers": { - "etag": "xh1kdn7wbmI", + "etag": "Z8eUkQ2Mbyo", "content-type": "text/html;charset=utf-8" } }, { "input": "client.html", - "path": "./client-gsg59jv4.css", + "path": "./client-ktkb7geg.css", "loader": "css", "isEntry": true, "headers": { - "etag": "cJnwBSkS-4Q", + "etag": "M9oTK-dwjpA", "content-type": "text/css;charset=utf-8" } }, @@ -325,17 +325,17 @@ console.log("About manifest:", aboutHtml); { "headers": { "content-type": "text/javascript;charset=utf-8", - "etag": "Dl7kT6q7eY4", + "etag": "OFwXdH2gPbI", }, "input": "home.html", "isEntry": true, "loader": "js", - "path": "./home-ey4favse.js", + "path": "./home-rwqmx0xj.js", }, { "headers": { "content-type": "text/html;charset=utf-8", - "etag": "IhvRaM9jGDU", + "etag": "Ttj8meWS96w", }, "input": "home.html", "isEntry": true, @@ -345,12 +345,12 @@ console.log("About manifest:", aboutHtml); { "headers": { "content-type": "text/css;charset=utf-8", - "etag": "hT1GudlsHgI", + "etag": "meNO9STq36E", }, "input": "home.html", "isEntry": true, "loader": "css", - "path": "./home-5x6sscy2.css", + "path": "./home-s3en4az1.css", }, ], "index": "./home.html", @@ -360,17 +360,17 @@ console.log("About manifest:", aboutHtml); { "headers": { "content-type": "text/javascript;charset=utf-8", - "etag": "RCRrF1EbBvo", + "etag": "SPfBLKGgkb8", }, "input": "about.html", "isEntry": true, "loader": "js", - "path": "./about-44bqhv6t.js", + "path": "./about-8q1c10hz.js", }, { "headers": { "content-type": "text/html;charset=utf-8", - "etag": "2OGqRD6vx54", + "etag": "N4Qr9nULlWA", }, "input": "about.html", "isEntry": true, @@ -380,12 +380,12 @@ console.log("About manifest:", aboutHtml); { "headers": { "content-type": "text/css;charset=utf-8", - "etag": "ti_Q2k-EP3o", + "etag": "aCSQlKMHZRs", }, "input": "about.html", "isEntry": true, "loader": "css", - "path": "./about-pfgtf4zt.css", + "path": "./about-84gm375v.css", }, ], "index": "./about.html", diff --git a/test/js/bun/http/bun-serve-html.test.ts b/test/js/bun/http/bun-serve-html.test.ts index 928f19dc0891..4b63a949f94e 100644 --- a/test/js/bun/http/bun-serve-html.test.ts +++ b/test/js/bun/http/bun-serve-html.test.ts @@ -224,7 +224,7 @@ console.log("How...dashing?"); "content-length": "316", "content-type": "text/javascript;charset=utf-8", "date": "", - "etag": ""f862dbeedf9b72bc"", + "etag": ""8daf2b7ce4b99264"", "sourcemap": "/chunk-HASH.js.map", } `); diff --git a/test/regression/issue/cyclic-imports-async-bundler.test.js b/test/regression/issue/cyclic-imports-async-bundler.test.js index 1932c6182961..2fa66420d7c0 100644 --- a/test/regression/issue/cyclic-imports-async-bundler.test.js +++ b/test/regression/issue/cyclic-imports-async-bundler.test.js @@ -166,7 +166,7 @@ test("cyclic imports with async dependencies should generate async wrappers", as await Promise.resolve(); AsyncEntryPoint(); - //# debugId=5B573DC06E466ACE64756E2164756E21 + //# debugId=51F7FE2ACF27457264756E2164756E21 //# sourceMappingURL=entryBuild.js.map " `);