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
91 changes: 71 additions & 20 deletions turbopack/crates/turbopack-browser/src/chunking_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ impl BrowserChunkingContextBuilder {
self
}

/// Generate a separate HMR chunk list for each dynamic chunk group instead of recursively
/// adding every reachable dynamic chunk to the entry chunk list. The HMR transport must honor
/// the subscription's expected version and echo its validation token before use.
pub fn dynamic_hmr_chunk_lists(mut self) -> Self {
self.chunking_context.enable_dynamic_hmr_chunk_lists = true;
self
}

pub fn source_map_source_type(mut self, source_map_source_type: SourceMapSourceType) -> Self {
self.chunking_context.source_map_source_type = source_map_source_type;
self
Expand Down Expand Up @@ -357,6 +365,8 @@ pub struct BrowserChunkingContext {
default_url_behavior: Option<UrlBehavior>,
/// Enable HMR for this chunking
enable_hot_module_replacement: bool,
/// Track dynamic chunk groups in their own HMR chunk lists instead of the entry chunk list.
enable_dynamic_hmr_chunk_lists: bool,
/// Enable nested async availability for this chunking
enable_nested_async_availability: bool,
/// Enable module merging
Expand Down Expand Up @@ -454,6 +464,7 @@ impl BrowserChunkingContext {
url_behaviors: Default::default(),
default_url_behavior: None,
enable_hot_module_replacement: false,
enable_dynamic_hmr_chunk_lists: false,
enable_nested_async_availability: false,
enable_module_merging: false,
enable_dynamic_chunk_content_loading: false,
Expand Down Expand Up @@ -933,6 +944,8 @@ impl ChunkingContext for BrowserChunkingContext {
) -> Result<Vc<ChunkGroupResult>> {
let span = tracing::info_span!("chunking", name = display(ident.to_string().await?));
async move {
let this = self.await?;
let is_async_chunk_group = matches!(&chunk_group, ChunkGroup::Async(_));
let input_availability_info = availability_info;
let MakeChunkGroupResult {
chunks,
Expand All @@ -948,12 +961,43 @@ impl ChunkingContext for BrowserChunkingContext {

let chunks = chunks.await?;

let assets = chunks
let mut assets = chunks
.iter()
.map(|chunk| self.generate_chunk(*chunk))
.try_join()
.await?;

if this.enable_hot_module_replacement
&& this.enable_dynamic_hmr_chunk_lists
&& is_async_chunk_group
{
let ident = if let Some(input_availability_info_ident) =
input_availability_info.ident().await?
{
ident
.owned()
.await?
.with_modifier(input_availability_info_ident)
.into_vc()
} else {
ident
};
let other_assets = Vc::cell(assets);
let chunk_list = self
.generate_chunk_list_register_chunk(
ident,
EvaluatableAssets::empty(),
other_assets,
EcmascriptDevChunkListSource::Dynamic,
)
.to_resolved()
.await?;

// The list is the bootstrap for the dynamic group. The browser runtime first
// validates its HMR baseline, then loads the member chunks.
assets = vec![chunk_list];
}

Ok(ChunkGroupResult {
assets: ResolvedVc::cell(assets),
referenced_assets: OutputAssets::empty_resolved(),
Expand Down Expand Up @@ -1021,26 +1065,33 @@ impl ChunkingContext for BrowserChunkingContext {
);

if this.enable_hot_module_replacement {
// Follow references (async loaders) to get actual dynamic component chunks, so
// the single HMR chunk list covers all lazily-loaded modules.
// inner=false: we only follow Reference inputs transitively, not Asset inputs,
// to avoid pulling in source maps and other asset-adjacent files that can't be
// reloaded by the DOM backend (which only handles CSS chunks via reloadChunk).
let all_dynamic_chunks = expand_output_assets(
references
.iter()
.copied()
.map(ExpandOutputAssetsInput::Reference)
.chain(assets.iter().copied().map(ExpandOutputAssetsInput::Asset)),
false,
)
.await?;

// Combine direct chunks, transitively-reachable dynamic chunks, and any caller-
// provided extras (e.g. RSC client reference chunks built outside this graph).
let extra_chunks_ref = extra_chunks.await?;
let mut hmr_chunks: FxIndexSet<ResolvedVc<Box<dyn OutputAsset>>> =
all_dynamic_chunks.into_iter().collect();
let mut hmr_chunks: FxIndexSet<ResolvedVc<Box<dyn OutputAsset>>> = if this
.enable_dynamic_hmr_chunk_lists
{
// Dynamic groups register their own lists when loaded, so the entry only
// tracks its directly-generated chunks.
assets.iter().copied().collect()
} else {
// Follow references (async loaders) to get actual dynamic component chunks,
// so the single HMR chunk list covers all lazily-loaded modules. `inner=false`
// avoids source maps and other asset-adjacent files that the DOM backend can't
// reload.
expand_output_assets(
references
.iter()
.copied()
.map(ExpandOutputAssetsInput::Reference)
.chain(assets.iter().copied().map(ExpandOutputAssetsInput::Asset)),
false,
)
.await?
.into_iter()
.collect()
};

// Caller-provided chunks (for example RSC client references) are built outside
// this graph and must always remain in the entry list.
hmr_chunks.extend(extra_chunks_ref.iter().copied());
let hmr_other_assets = Vc::cell(hmr_chunks.into_iter().collect());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,27 @@ impl EcmascriptDevChunkListContent {
// variable. Similarly, we register the chunk list with the
// `{chunk_loading_global}_CHUNK_LISTS` global variable.
let chunk_lists_global = format!("{}_CHUNK_LISTS", this.chunk_loading_global);
let dynamic_metadata = match this.source {
EcmascriptDevChunkListSource::Entry => String::new(),
EcmascriptDevChunkListSource::Dynamic => {
let version = self.version().id().owned().await?;
let chunk_versions: FxIndexMap<&str, RcStr> = this
.chunks_contents
.iter()
.map(async |(path, content)| {
Ok((path.as_str(), content.version().id().owned().await?))
})
.try_join()
.await?
.into_iter()
.collect();
format!(
",\nchunkVersions: {},\nversion: {}",
StringifyJs(&chunk_versions),
StringifyJs(&version)
)
}
};
writedoc!(
code,
// `||=` would be better but we need to be es2020 compatible
Expand All @@ -168,11 +189,12 @@ impl EcmascriptDevChunkListContent {
(globalThis[{chunk_lists_global}] || (globalThis[{chunk_lists_global}] = [])).push({{
script: {script_or_path},
chunks: {chunks},
source: {source}
source: {source}{dynamic_metadata}
}});
"#,
chunk_lists_global = StringifyJs(&chunk_lists_global),
chunks = StringifyJs(&chunks),
dynamic_metadata = dynamic_metadata,
source = StringifyJs(&this.source),
)?;

Expand Down
Loading
Loading