1use anyhow::{Context, Result, bail};
2use serde::{Deserialize, Serialize};
3use tracing::Instrument;
4use turbo_rcstr::{RcStr, rcstr};
5use turbo_tasks::{
6 NonLocalValue, ResolvedVc, TaskInput, TryJoinIterExt, Upcast, ValueToString, Vc,
7 trace::TraceRawVcs,
8};
9use turbo_tasks_fs::FileSystemPath;
10use turbo_tasks_hash::{DeterministicHash, hash_xxh3_hash64};
11use turbopack_core::{
12 asset::{Asset, AssetContent},
13 chunk::{
14 Chunk, ChunkGroupResult, ChunkItem, ChunkType, ChunkableModule, ChunkingConfig,
15 ChunkingConfigs, ChunkingContext, EntryChunkGroupResult, EvaluatableAsset,
16 EvaluatableAssets, MinifyType, ModuleId, SourceMapsType,
17 availability_info::AvailabilityInfo,
18 chunk_group::{MakeChunkGroupResult, make_chunk_group},
19 module_id_strategies::{DevModuleIdStrategy, ModuleIdStrategy},
20 },
21 environment::Environment,
22 ident::AssetIdent,
23 module::Module,
24 module_graph::{
25 ModuleGraph,
26 chunk_group_info::ChunkGroup,
27 export_usage::{ExportUsageInfo, ModuleExportUsageInfo},
28 },
29 output::{OutputAsset, OutputAssets},
30};
31use turbopack_ecmascript::{
32 async_chunk::module::AsyncLoaderModule,
33 chunk::EcmascriptChunk,
34 manifest::{chunk_asset::ManifestAsyncModule, loader_item::ManifestLoaderChunkItem},
35};
36use turbopack_ecmascript_runtime::RuntimeType;
37
38use crate::ecmascript::{
39 chunk::EcmascriptBrowserChunk,
40 evaluate::chunk::EcmascriptBrowserEvaluateChunk,
41 list::asset::{EcmascriptDevChunkList, EcmascriptDevChunkListSource},
42};
43
44#[turbo_tasks::value]
45#[derive(Debug, Clone, Copy, Hash, TaskInput)]
46pub enum CurrentChunkMethod {
47 StringLiteral,
48 DocumentCurrentScript,
49}
50
51pub const CURRENT_CHUNK_METHOD_DOCUMENT_CURRENT_SCRIPT_EXPR: &str =
52 "typeof document === \"object\" ? document.currentScript : undefined";
53
54#[derive(
55 Debug,
56 TaskInput,
57 Clone,
58 Copy,
59 PartialEq,
60 Eq,
61 Hash,
62 Serialize,
63 Deserialize,
64 TraceRawVcs,
65 DeterministicHash,
66 NonLocalValue,
67)]
68pub enum ContentHashing {
69 Direct {
73 length: u8,
76 },
77}
78
79pub struct BrowserChunkingContextBuilder {
80 chunking_context: BrowserChunkingContext,
81}
82
83impl BrowserChunkingContextBuilder {
84 pub fn name(mut self, name: RcStr) -> Self {
85 self.chunking_context.name = Some(name);
86 self
87 }
88
89 pub fn hot_module_replacement(mut self) -> Self {
90 self.chunking_context.enable_hot_module_replacement = true;
91 self
92 }
93
94 pub fn use_file_source_map_uris(mut self) -> Self {
95 self.chunking_context.should_use_file_source_map_uris = true;
96 self
97 }
98
99 pub fn tracing(mut self, enable_tracing: bool) -> Self {
100 self.chunking_context.enable_tracing = enable_tracing;
101 self
102 }
103
104 pub fn module_merging(mut self, enable_module_merging: bool) -> Self {
105 self.chunking_context.enable_module_merging = enable_module_merging;
106 self
107 }
108
109 pub fn asset_base_path(mut self, asset_base_path: Option<RcStr>) -> Self {
110 self.chunking_context.asset_base_path = asset_base_path;
111 self
112 }
113
114 pub fn chunk_base_path(mut self, chunk_base_path: Option<RcStr>) -> Self {
115 self.chunking_context.chunk_base_path = chunk_base_path;
116 self
117 }
118
119 pub fn chunk_suffix_path(mut self, chunk_suffix_path: Option<RcStr>) -> Self {
120 self.chunking_context.chunk_suffix_path = chunk_suffix_path;
121 self
122 }
123
124 pub fn runtime_type(mut self, runtime_type: RuntimeType) -> Self {
125 self.chunking_context.runtime_type = runtime_type;
126 self
127 }
128
129 pub fn manifest_chunks(mut self, manifest_chunks: bool) -> Self {
130 self.chunking_context.manifest_chunks = manifest_chunks;
131 self
132 }
133
134 pub fn minify_type(mut self, minify_type: MinifyType) -> Self {
135 self.chunking_context.minify_type = minify_type;
136 self
137 }
138
139 pub fn source_maps(mut self, source_maps: SourceMapsType) -> Self {
140 self.chunking_context.source_maps_type = source_maps;
141 self
142 }
143
144 pub fn current_chunk_method(mut self, method: CurrentChunkMethod) -> Self {
145 self.chunking_context.current_chunk_method = method;
146 self
147 }
148
149 pub fn module_id_strategy(
150 mut self,
151 module_id_strategy: ResolvedVc<Box<dyn ModuleIdStrategy>>,
152 ) -> Self {
153 self.chunking_context.module_id_strategy = module_id_strategy;
154 self
155 }
156
157 pub fn export_usage(mut self, export_usage: Option<ResolvedVc<ExportUsageInfo>>) -> Self {
158 self.chunking_context.export_usage = export_usage;
159 self
160 }
161
162 pub fn chunking_config<T>(mut self, ty: ResolvedVc<T>, chunking_config: ChunkingConfig) -> Self
163 where
164 T: Upcast<Box<dyn ChunkType>>,
165 {
166 self.chunking_context
167 .chunking_configs
168 .push((ResolvedVc::upcast(ty), chunking_config));
169 self
170 }
171
172 pub fn use_content_hashing(mut self, content_hashing: ContentHashing) -> Self {
173 self.chunking_context.content_hashing = Some(content_hashing);
174 self
175 }
176
177 pub fn build(self) -> Vc<BrowserChunkingContext> {
178 BrowserChunkingContext::cell(self.chunking_context)
179 }
180}
181
182#[turbo_tasks::value]
189#[derive(Debug, Clone, Hash, TaskInput)]
190pub struct BrowserChunkingContext {
191 name: Option<RcStr>,
192 root_path: FileSystemPath,
194 should_use_file_source_map_uris: bool,
196 output_root: FileSystemPath,
198 output_root_to_root_path: RcStr,
200 client_root: FileSystemPath,
202 chunk_root_path: FileSystemPath,
204 asset_root_path: FileSystemPath,
206 chunk_base_path: Option<RcStr>,
209 chunk_suffix_path: Option<RcStr>,
212 asset_base_path: Option<RcStr>,
215 enable_hot_module_replacement: bool,
217 enable_tracing: bool,
219 enable_module_merging: bool,
221 environment: ResolvedVc<Environment>,
223 runtime_type: RuntimeType,
225 minify_type: MinifyType,
227 content_hashing: Option<ContentHashing>,
229 source_maps_type: SourceMapsType,
231 current_chunk_method: CurrentChunkMethod,
233 manifest_chunks: bool,
235 module_id_strategy: ResolvedVc<Box<dyn ModuleIdStrategy>>,
237 export_usage: Option<ResolvedVc<ExportUsageInfo>>,
239 chunking_configs: Vec<(ResolvedVc<Box<dyn ChunkType>>, ChunkingConfig)>,
241}
242
243impl BrowserChunkingContext {
244 pub fn builder(
245 root_path: FileSystemPath,
246 output_root: FileSystemPath,
247 output_root_to_root_path: RcStr,
248 client_root: FileSystemPath,
249 chunk_root_path: FileSystemPath,
250 asset_root_path: FileSystemPath,
251 environment: ResolvedVc<Environment>,
252 runtime_type: RuntimeType,
253 ) -> BrowserChunkingContextBuilder {
254 BrowserChunkingContextBuilder {
255 chunking_context: BrowserChunkingContext {
256 name: None,
257 root_path,
258 output_root,
259 output_root_to_root_path,
260 client_root,
261 chunk_root_path,
262 should_use_file_source_map_uris: false,
263 asset_root_path,
264 chunk_base_path: None,
265 chunk_suffix_path: None,
266 asset_base_path: None,
267 enable_hot_module_replacement: false,
268 enable_tracing: false,
269 enable_module_merging: false,
270 environment,
271 runtime_type,
272 minify_type: MinifyType::NoMinify,
273 content_hashing: None,
274 source_maps_type: SourceMapsType::Full,
275 current_chunk_method: CurrentChunkMethod::StringLiteral,
276 manifest_chunks: false,
277 module_id_strategy: ResolvedVc::upcast(DevModuleIdStrategy::new_resolved()),
278 export_usage: None,
279 chunking_configs: Default::default(),
280 },
281 }
282 }
283}
284
285impl BrowserChunkingContext {
286 pub fn runtime_type(&self) -> RuntimeType {
291 self.runtime_type
292 }
293
294 pub fn chunk_base_path(&self) -> Option<RcStr> {
296 self.chunk_base_path.clone()
297 }
298
299 pub fn chunk_suffix_path(&self) -> Option<RcStr> {
301 self.chunk_suffix_path.clone()
302 }
303
304 pub fn source_maps_type(&self) -> SourceMapsType {
306 self.source_maps_type
307 }
308
309 pub fn minify_type(&self) -> MinifyType {
311 self.minify_type
312 }
313}
314
315#[turbo_tasks::value_impl]
316impl BrowserChunkingContext {
317 #[turbo_tasks::function]
318 fn generate_evaluate_chunk(
319 self: Vc<Self>,
320 ident: Vc<AssetIdent>,
321 other_chunks: Vc<OutputAssets>,
322 evaluatable_assets: Vc<EvaluatableAssets>,
323 module_graph: Vc<ModuleGraph>,
325 ) -> Vc<Box<dyn OutputAsset>> {
326 Vc::upcast(EcmascriptBrowserEvaluateChunk::new(
327 self,
328 ident,
329 other_chunks,
330 evaluatable_assets,
331 module_graph,
332 ))
333 }
334
335 #[turbo_tasks::function]
336 fn generate_chunk_list_register_chunk(
337 self: Vc<Self>,
338 ident: Vc<AssetIdent>,
339 evaluatable_assets: Vc<EvaluatableAssets>,
340 other_chunks: Vc<OutputAssets>,
341 source: EcmascriptDevChunkListSource,
342 ) -> Vc<Box<dyn OutputAsset>> {
343 Vc::upcast(EcmascriptDevChunkList::new(
344 self,
345 ident,
346 evaluatable_assets,
347 other_chunks,
348 source,
349 ))
350 }
351
352 #[turbo_tasks::function]
353 async fn generate_chunk(
354 self: Vc<Self>,
355 chunk: Vc<Box<dyn Chunk>>,
356 ) -> Result<Vc<Box<dyn OutputAsset>>> {
357 Ok(
358 if let Some(ecmascript_chunk) =
359 Vc::try_resolve_downcast_type::<EcmascriptChunk>(chunk).await?
360 {
361 Vc::upcast(EcmascriptBrowserChunk::new(self, ecmascript_chunk))
362 } else if let Some(output_asset) =
363 Vc::try_resolve_sidecast::<Box<dyn OutputAsset>>(chunk).await?
364 {
365 output_asset
366 } else {
367 bail!("Unable to generate output asset for chunk");
368 },
369 )
370 }
371
372 #[turbo_tasks::function]
373 pub fn current_chunk_method(&self) -> Vc<CurrentChunkMethod> {
374 self.current_chunk_method.cell()
375 }
376}
377
378#[turbo_tasks::value_impl]
379impl ChunkingContext for BrowserChunkingContext {
380 #[turbo_tasks::function]
381 fn name(&self) -> Vc<RcStr> {
382 if let Some(name) = &self.name {
383 Vc::cell(name.clone())
384 } else {
385 Vc::cell(rcstr!("unknown"))
386 }
387 }
388
389 #[turbo_tasks::function]
390 fn root_path(&self) -> Vc<FileSystemPath> {
391 self.root_path.clone().cell()
392 }
393
394 #[turbo_tasks::function]
395 fn output_root(&self) -> Vc<FileSystemPath> {
396 self.output_root.clone().cell()
397 }
398
399 #[turbo_tasks::function]
400 fn output_root_to_root_path(&self) -> Vc<RcStr> {
401 Vc::cell(self.output_root_to_root_path.clone())
402 }
403
404 #[turbo_tasks::function]
405 fn environment(&self) -> Vc<Environment> {
406 *self.environment
407 }
408
409 #[turbo_tasks::function]
410 fn chunk_root_path(&self) -> Vc<FileSystemPath> {
411 self.chunk_root_path.clone().cell()
412 }
413
414 #[turbo_tasks::function]
415 async fn chunk_path(
416 &self,
417 asset: Option<Vc<Box<dyn Asset>>>,
418 ident: Vc<AssetIdent>,
419 extension: RcStr,
420 ) -> Result<Vc<FileSystemPath>> {
421 debug_assert!(
422 extension.starts_with("."),
423 "`extension` should include the leading '.', got '{extension}'"
424 );
425 let root_path = self.chunk_root_path.clone();
426 let name = match self.content_hashing {
427 None => {
428 ident
429 .output_name(self.root_path.clone(), extension)
430 .owned()
431 .await?
432 }
433 Some(ContentHashing::Direct { length }) => {
434 let Some(asset) = asset else {
435 bail!("chunk_path requires an asset when content hashing is enabled");
436 };
437 let content = asset.content().await?;
438 if let AssetContent::File(file) = &*content {
439 let hash = hash_xxh3_hash64(&file.await?);
440 let length = length as usize;
441 format!("{hash:0length$x}{extension}").into()
442 } else {
443 bail!(
444 "chunk_path requires an asset with file content when content hashing is \
445 enabled"
446 );
447 }
448 }
449 };
450 Ok(root_path.join(&name)?.cell())
451 }
452
453 #[turbo_tasks::function]
454 async fn asset_url(&self, ident: FileSystemPath) -> Result<Vc<RcStr>> {
455 let asset_path = ident.to_string();
456 let asset_path = asset_path
457 .strip_prefix(&format!("{}/", self.client_root.path))
458 .context("expected asset_path to contain client_root")?;
459
460 Ok(Vc::cell(
461 format!(
462 "{}{}",
463 self.asset_base_path
464 .as_ref()
465 .map(|s| s.as_str())
466 .unwrap_or("/"),
467 asset_path
468 )
469 .into(),
470 ))
471 }
472
473 #[turbo_tasks::function]
474 fn reference_chunk_source_maps(&self, _chunk: Vc<Box<dyn OutputAsset>>) -> Vc<bool> {
475 Vc::cell(match self.source_maps_type {
476 SourceMapsType::Full => true,
477 SourceMapsType::None => false,
478 })
479 }
480
481 #[turbo_tasks::function]
482 fn reference_module_source_maps(&self, _module: Vc<Box<dyn Module>>) -> Vc<bool> {
483 Vc::cell(match self.source_maps_type {
484 SourceMapsType::Full => true,
485 SourceMapsType::None => false,
486 })
487 }
488
489 #[turbo_tasks::function]
490 async fn asset_path(
491 &self,
492 content_hash: RcStr,
493 original_asset_ident: Vc<AssetIdent>,
494 ) -> Result<Vc<FileSystemPath>> {
495 let source_path = original_asset_ident.path().await?;
496 let basename = source_path.file_name();
497 let asset_path = match source_path.extension_ref() {
498 Some(ext) => format!(
499 "{basename}.{content_hash}.{ext}",
500 basename = &basename[..basename.len() - ext.len() - 1],
501 content_hash = &content_hash[..8]
502 ),
503 None => format!(
504 "{basename}.{content_hash}",
505 content_hash = &content_hash[..8]
506 ),
507 };
508 Ok(self.asset_root_path.join(&asset_path)?.cell())
509 }
510
511 #[turbo_tasks::function]
512 fn is_hot_module_replacement_enabled(&self) -> Vc<bool> {
513 Vc::cell(self.enable_hot_module_replacement)
514 }
515
516 #[turbo_tasks::function]
517 fn chunking_configs(&self) -> Result<Vc<ChunkingConfigs>> {
518 Ok(Vc::cell(self.chunking_configs.iter().cloned().collect()))
519 }
520
521 #[turbo_tasks::function]
522 fn should_use_file_source_map_uris(&self) -> Vc<bool> {
523 Vc::cell(self.should_use_file_source_map_uris)
524 }
525
526 #[turbo_tasks::function]
527 fn is_tracing_enabled(&self) -> Vc<bool> {
528 Vc::cell(self.enable_tracing)
529 }
530
531 #[turbo_tasks::function]
532 fn is_module_merging_enabled(&self) -> Vc<bool> {
533 Vc::cell(self.enable_module_merging)
534 }
535
536 #[turbo_tasks::function]
537 pub fn minify_type(&self) -> Vc<MinifyType> {
538 self.minify_type.cell()
539 }
540
541 #[turbo_tasks::function]
542 async fn chunk_group(
543 self: ResolvedVc<Self>,
544 ident: Vc<AssetIdent>,
545 chunk_group: ChunkGroup,
546 module_graph: Vc<ModuleGraph>,
547 availability_info: AvailabilityInfo,
548 ) -> Result<Vc<ChunkGroupResult>> {
549 let span = tracing::info_span!("chunking", ident = ident.to_string().await?.to_string());
550 async move {
551 let this = self.await?;
552 let modules = chunk_group.entries();
553 let input_availability_info = availability_info;
554 let MakeChunkGroupResult {
555 chunks,
556 availability_info,
557 } = make_chunk_group(
558 modules,
559 module_graph,
560 ResolvedVc::upcast(self),
561 input_availability_info,
562 )
563 .await?;
564
565 let mut assets = chunks
566 .iter()
567 .map(|chunk| self.generate_chunk(**chunk).to_resolved())
568 .try_join()
569 .await?;
570
571 if this.enable_hot_module_replacement {
572 let mut ident = ident;
573 match input_availability_info {
574 AvailabilityInfo::Root => {}
575 AvailabilityInfo::Untracked => {
576 ident = ident.with_modifier(rcstr!("untracked"));
577 }
578 AvailabilityInfo::Complete { available_modules } => {
579 ident =
580 ident.with_modifier(available_modules.hash().await?.to_string().into());
581 }
582 }
583 assets.push(
584 self.generate_chunk_list_register_chunk(
585 ident,
586 EvaluatableAssets::empty(),
587 Vc::cell(assets.clone()),
588 EcmascriptDevChunkListSource::Dynamic,
589 )
590 .to_resolved()
591 .await?,
592 );
593 }
594
595 Ok(ChunkGroupResult {
596 assets: ResolvedVc::cell(assets),
597 availability_info,
598 }
599 .cell())
600 }
601 .instrument(span)
602 .await
603 }
604
605 #[turbo_tasks::function]
606 async fn evaluated_chunk_group(
607 self: ResolvedVc<Self>,
608 ident: Vc<AssetIdent>,
609 chunk_group: ChunkGroup,
610 module_graph: Vc<ModuleGraph>,
611 availability_info: AvailabilityInfo,
612 ) -> Result<Vc<ChunkGroupResult>> {
613 let span = {
614 let ident = ident.to_string().await?.to_string();
615 tracing::info_span!("chunking", chunking_type = "evaluated", ident = ident)
616 };
617 async move {
618 let this = self.await?;
619
620 let entries = chunk_group.entries();
621
622 let MakeChunkGroupResult {
623 chunks,
624 availability_info,
625 } = make_chunk_group(
626 entries,
627 module_graph,
628 ResolvedVc::upcast(self),
629 availability_info,
630 )
631 .await?;
632
633 let mut assets: Vec<ResolvedVc<Box<dyn OutputAsset>>> = chunks
634 .iter()
635 .map(|chunk| self.generate_chunk(**chunk).to_resolved())
636 .try_join()
637 .await?;
638
639 let other_assets = Vc::cell(assets.clone());
640
641 let entries = Vc::cell(
642 chunk_group
643 .entries()
644 .map(|m| {
645 ResolvedVc::try_downcast::<Box<dyn EvaluatableAsset>>(m)
646 .context("evaluated_chunk_group entries must be evaluatable assets")
647 })
648 .collect::<Result<Vec<_>>>()?,
649 );
650
651 if this.enable_hot_module_replacement {
652 assets.push(
653 self.generate_chunk_list_register_chunk(
654 ident,
655 entries,
656 other_assets,
657 EcmascriptDevChunkListSource::Entry,
658 )
659 .to_resolved()
660 .await?,
661 );
662 }
663
664 assets.push(
665 self.generate_evaluate_chunk(ident, other_assets, entries, module_graph)
666 .to_resolved()
667 .await?,
668 );
669
670 Ok(ChunkGroupResult {
671 assets: ResolvedVc::cell(assets),
672 availability_info,
673 }
674 .cell())
675 }
676 .instrument(span)
677 .await
678 }
679
680 #[turbo_tasks::function]
681 fn entry_chunk_group(
682 self: Vc<Self>,
683 _path: FileSystemPath,
684 _evaluatable_assets: Vc<EvaluatableAssets>,
685 _module_graph: Vc<ModuleGraph>,
686 _extra_chunks: Vc<OutputAssets>,
687 _availability_info: AvailabilityInfo,
688 ) -> Result<Vc<EntryChunkGroupResult>> {
689 bail!("Browser chunking context does not support entry chunk groups")
690 }
691
692 #[turbo_tasks::function]
693 fn chunk_item_id_from_ident(&self, ident: Vc<AssetIdent>) -> Vc<ModuleId> {
694 self.module_id_strategy.get_module_id(ident)
695 }
696
697 #[turbo_tasks::function]
698 async fn async_loader_chunk_item(
699 self: Vc<Self>,
700 module: Vc<Box<dyn ChunkableModule>>,
701 module_graph: Vc<ModuleGraph>,
702 availability_info: AvailabilityInfo,
703 ) -> Result<Vc<Box<dyn ChunkItem>>> {
704 Ok(if self.await?.manifest_chunks {
705 let manifest_asset =
706 ManifestAsyncModule::new(module, module_graph, Vc::upcast(self), availability_info);
707 Vc::upcast(ManifestLoaderChunkItem::new(
708 manifest_asset,
709 module_graph,
710 Vc::upcast(self),
711 ))
712 } else {
713 let module = AsyncLoaderModule::new(module, Vc::upcast(self), availability_info);
714 Vc::upcast(module.as_chunk_item(module_graph, Vc::upcast(self)))
715 })
716 }
717
718 #[turbo_tasks::function]
719 async fn async_loader_chunk_item_id(
720 self: Vc<Self>,
721 module: Vc<Box<dyn ChunkableModule>>,
722 ) -> Result<Vc<ModuleId>> {
723 Ok(if self.await?.manifest_chunks {
724 self.chunk_item_id_from_ident(ManifestLoaderChunkItem::asset_ident_for(module))
725 } else {
726 self.chunk_item_id_from_ident(AsyncLoaderModule::asset_ident_for(module))
727 })
728 }
729
730 #[turbo_tasks::function]
731 async fn module_export_usage(
732 self: Vc<Self>,
733 module: ResolvedVc<Box<dyn Module>>,
734 ) -> Result<Vc<ModuleExportUsageInfo>> {
735 if let Some(export_usage) = self.await?.export_usage {
736 Ok(export_usage.await?.used_exports(module))
737 } else {
738 Ok(ModuleExportUsageInfo::all())
739 }
740 }
741}