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, ModuleExportUsage},
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 dynamic_chunk_content_loading(
110 mut self,
111 enable_dynamic_chunk_content_loading: bool,
112 ) -> Self {
113 self.chunking_context.enable_dynamic_chunk_content_loading =
114 enable_dynamic_chunk_content_loading;
115 self
116 }
117
118 pub fn asset_base_path(mut self, asset_base_path: Option<RcStr>) -> Self {
119 self.chunking_context.asset_base_path = asset_base_path;
120 self
121 }
122
123 pub fn chunk_base_path(mut self, chunk_base_path: Option<RcStr>) -> Self {
124 self.chunking_context.chunk_base_path = chunk_base_path;
125 self
126 }
127
128 pub fn chunk_suffix_path(mut self, chunk_suffix_path: Option<RcStr>) -> Self {
129 self.chunking_context.chunk_suffix_path = chunk_suffix_path;
130 self
131 }
132
133 pub fn runtime_type(mut self, runtime_type: RuntimeType) -> Self {
134 self.chunking_context.runtime_type = runtime_type;
135 self
136 }
137
138 pub fn manifest_chunks(mut self, manifest_chunks: bool) -> Self {
139 self.chunking_context.manifest_chunks = manifest_chunks;
140 self
141 }
142
143 pub fn minify_type(mut self, minify_type: MinifyType) -> Self {
144 self.chunking_context.minify_type = minify_type;
145 self
146 }
147
148 pub fn source_maps(mut self, source_maps: SourceMapsType) -> Self {
149 self.chunking_context.source_maps_type = source_maps;
150 self
151 }
152
153 pub fn current_chunk_method(mut self, method: CurrentChunkMethod) -> Self {
154 self.chunking_context.current_chunk_method = method;
155 self
156 }
157
158 pub fn module_id_strategy(
159 mut self,
160 module_id_strategy: ResolvedVc<Box<dyn ModuleIdStrategy>>,
161 ) -> Self {
162 self.chunking_context.module_id_strategy = module_id_strategy;
163 self
164 }
165
166 pub fn export_usage(mut self, export_usage: Option<ResolvedVc<ExportUsageInfo>>) -> Self {
167 self.chunking_context.export_usage = export_usage;
168 self
169 }
170
171 pub fn chunking_config<T>(mut self, ty: ResolvedVc<T>, chunking_config: ChunkingConfig) -> Self
172 where
173 T: Upcast<Box<dyn ChunkType>>,
174 {
175 self.chunking_context
176 .chunking_configs
177 .push((ResolvedVc::upcast(ty), chunking_config));
178 self
179 }
180
181 pub fn use_content_hashing(mut self, content_hashing: ContentHashing) -> Self {
182 self.chunking_context.content_hashing = Some(content_hashing);
183 self
184 }
185
186 pub fn build(self) -> Vc<BrowserChunkingContext> {
187 BrowserChunkingContext::cell(self.chunking_context)
188 }
189}
190
191#[turbo_tasks::value]
198#[derive(Debug, Clone, Hash, TaskInput)]
199pub struct BrowserChunkingContext {
200 name: Option<RcStr>,
201 root_path: FileSystemPath,
203 should_use_file_source_map_uris: bool,
205 output_root: FileSystemPath,
207 output_root_to_root_path: RcStr,
209 client_root: FileSystemPath,
211 chunk_root_path: FileSystemPath,
213 asset_root_path: FileSystemPath,
215 chunk_base_path: Option<RcStr>,
218 chunk_suffix_path: Option<RcStr>,
221 asset_base_path: Option<RcStr>,
224 enable_hot_module_replacement: bool,
226 enable_tracing: bool,
228 enable_module_merging: bool,
230 enable_dynamic_chunk_content_loading: bool,
232 environment: ResolvedVc<Environment>,
234 runtime_type: RuntimeType,
236 minify_type: MinifyType,
238 content_hashing: Option<ContentHashing>,
240 source_maps_type: SourceMapsType,
242 current_chunk_method: CurrentChunkMethod,
244 manifest_chunks: bool,
246 module_id_strategy: ResolvedVc<Box<dyn ModuleIdStrategy>>,
248 export_usage: Option<ResolvedVc<ExportUsageInfo>>,
250 chunking_configs: Vec<(ResolvedVc<Box<dyn ChunkType>>, ChunkingConfig)>,
252}
253
254impl BrowserChunkingContext {
255 pub fn builder(
256 root_path: FileSystemPath,
257 output_root: FileSystemPath,
258 output_root_to_root_path: RcStr,
259 client_root: FileSystemPath,
260 chunk_root_path: FileSystemPath,
261 asset_root_path: FileSystemPath,
262 environment: ResolvedVc<Environment>,
263 runtime_type: RuntimeType,
264 ) -> BrowserChunkingContextBuilder {
265 BrowserChunkingContextBuilder {
266 chunking_context: BrowserChunkingContext {
267 name: None,
268 root_path,
269 output_root,
270 output_root_to_root_path,
271 client_root,
272 chunk_root_path,
273 should_use_file_source_map_uris: false,
274 asset_root_path,
275 chunk_base_path: None,
276 chunk_suffix_path: None,
277 asset_base_path: None,
278 enable_hot_module_replacement: false,
279 enable_tracing: false,
280 enable_module_merging: false,
281 enable_dynamic_chunk_content_loading: false,
282 environment,
283 runtime_type,
284 minify_type: MinifyType::NoMinify,
285 content_hashing: None,
286 source_maps_type: SourceMapsType::Full,
287 current_chunk_method: CurrentChunkMethod::StringLiteral,
288 manifest_chunks: false,
289 module_id_strategy: ResolvedVc::upcast(DevModuleIdStrategy::new_resolved()),
290 export_usage: None,
291 chunking_configs: Default::default(),
292 },
293 }
294 }
295}
296
297#[turbo_tasks::value_impl]
298impl BrowserChunkingContext {
299 #[turbo_tasks::function]
300 fn generate_evaluate_chunk(
301 self: Vc<Self>,
302 ident: Vc<AssetIdent>,
303 other_chunks: Vc<OutputAssets>,
304 evaluatable_assets: Vc<EvaluatableAssets>,
305 module_graph: Vc<ModuleGraph>,
307 ) -> Vc<Box<dyn OutputAsset>> {
308 Vc::upcast(EcmascriptBrowserEvaluateChunk::new(
309 self,
310 ident,
311 other_chunks,
312 evaluatable_assets,
313 module_graph,
314 ))
315 }
316
317 #[turbo_tasks::function]
318 fn generate_chunk_list_register_chunk(
319 self: Vc<Self>,
320 ident: Vc<AssetIdent>,
321 evaluatable_assets: Vc<EvaluatableAssets>,
322 other_chunks: Vc<OutputAssets>,
323 source: EcmascriptDevChunkListSource,
324 ) -> Vc<Box<dyn OutputAsset>> {
325 Vc::upcast(EcmascriptDevChunkList::new(
326 self,
327 ident,
328 evaluatable_assets,
329 other_chunks,
330 source,
331 ))
332 }
333
334 #[turbo_tasks::function]
335 async fn generate_chunk(
336 self: Vc<Self>,
337 chunk: Vc<Box<dyn Chunk>>,
338 ) -> Result<Vc<Box<dyn OutputAsset>>> {
339 Ok(
340 if let Some(ecmascript_chunk) =
341 Vc::try_resolve_downcast_type::<EcmascriptChunk>(chunk).await?
342 {
343 Vc::upcast(EcmascriptBrowserChunk::new(self, ecmascript_chunk))
344 } else if let Some(output_asset) =
345 Vc::try_resolve_sidecast::<Box<dyn OutputAsset>>(chunk).await?
346 {
347 output_asset
348 } else {
349 bail!("Unable to generate output asset for chunk");
350 },
351 )
352 }
353
354 #[turbo_tasks::function]
355 pub fn current_chunk_method(&self) -> Vc<CurrentChunkMethod> {
356 self.current_chunk_method.cell()
357 }
358
359 #[turbo_tasks::function]
364 pub fn runtime_type(&self) -> Vc<RuntimeType> {
365 self.runtime_type.cell()
366 }
367
368 #[turbo_tasks::function]
370 pub fn chunk_base_path(&self) -> Vc<Option<RcStr>> {
371 Vc::cell(self.chunk_base_path.clone())
372 }
373
374 #[turbo_tasks::function]
376 pub fn chunk_suffix_path(&self) -> Vc<Option<RcStr>> {
377 Vc::cell(self.chunk_suffix_path.clone())
378 }
379
380 #[turbo_tasks::function]
382 pub fn source_maps_type(&self) -> Vc<SourceMapsType> {
383 self.source_maps_type.cell()
384 }
385
386 #[turbo_tasks::function]
388 pub fn minify_type(&self) -> Vc<MinifyType> {
389 self.minify_type.cell()
390 }
391}
392
393#[turbo_tasks::value_impl]
394impl ChunkingContext for BrowserChunkingContext {
395 #[turbo_tasks::function]
396 fn name(&self) -> Vc<RcStr> {
397 if let Some(name) = &self.name {
398 Vc::cell(name.clone())
399 } else {
400 Vc::cell(rcstr!("unknown"))
401 }
402 }
403
404 #[turbo_tasks::function]
405 fn root_path(&self) -> Vc<FileSystemPath> {
406 self.root_path.clone().cell()
407 }
408
409 #[turbo_tasks::function]
410 fn output_root(&self) -> Vc<FileSystemPath> {
411 self.output_root.clone().cell()
412 }
413
414 #[turbo_tasks::function]
415 fn output_root_to_root_path(&self) -> Vc<RcStr> {
416 Vc::cell(self.output_root_to_root_path.clone())
417 }
418
419 #[turbo_tasks::function]
420 fn environment(&self) -> Vc<Environment> {
421 *self.environment
422 }
423
424 #[turbo_tasks::function]
425 fn chunk_root_path(&self) -> Vc<FileSystemPath> {
426 self.chunk_root_path.clone().cell()
427 }
428
429 #[turbo_tasks::function]
430 async fn chunk_path(
431 &self,
432 asset: Option<Vc<Box<dyn Asset>>>,
433 ident: Vc<AssetIdent>,
434 prefix: Option<RcStr>,
435 extension: RcStr,
436 ) -> Result<Vc<FileSystemPath>> {
437 debug_assert!(
438 extension.starts_with("."),
439 "`extension` should include the leading '.', got '{extension}'"
440 );
441 let root_path = self.chunk_root_path.clone();
442 let name = match self.content_hashing {
443 None => {
444 ident
445 .output_name(self.root_path.clone(), prefix, extension)
446 .owned()
447 .await?
448 }
449 Some(ContentHashing::Direct { length }) => {
450 let Some(asset) = asset else {
451 bail!("chunk_path requires an asset when content hashing is enabled");
452 };
453 let content = asset.content().await?;
454 if let AssetContent::File(file) = &*content {
455 let hash = hash_xxh3_hash64(&file.await?);
456 let length = length as usize;
457 if let Some(prefix) = prefix {
458 format!("{prefix}-{hash:0length$x}{extension}").into()
459 } else {
460 format!("{hash:0length$x}{extension}").into()
461 }
462 } else {
463 bail!(
464 "chunk_path requires an asset with file content when content hashing is \
465 enabled"
466 );
467 }
468 }
469 };
470 Ok(root_path.join(&name)?.cell())
471 }
472
473 #[turbo_tasks::function]
474 async fn asset_url(&self, ident: FileSystemPath) -> Result<Vc<RcStr>> {
475 let asset_path = ident.to_string();
476 let asset_path = asset_path
477 .strip_prefix(&format!("{}/", self.client_root.path))
478 .context("expected asset_path to contain client_root")?;
479
480 Ok(Vc::cell(
481 format!(
482 "{}{}",
483 self.asset_base_path
484 .as_ref()
485 .map(|s| s.as_str())
486 .unwrap_or("/"),
487 asset_path
488 )
489 .into(),
490 ))
491 }
492
493 #[turbo_tasks::function]
494 fn reference_chunk_source_maps(&self, _chunk: Vc<Box<dyn OutputAsset>>) -> Vc<bool> {
495 Vc::cell(match self.source_maps_type {
496 SourceMapsType::Full => true,
497 SourceMapsType::None => false,
498 })
499 }
500
501 #[turbo_tasks::function]
502 fn reference_module_source_maps(&self, _module: Vc<Box<dyn Module>>) -> Vc<bool> {
503 Vc::cell(match self.source_maps_type {
504 SourceMapsType::Full => true,
505 SourceMapsType::None => false,
506 })
507 }
508
509 #[turbo_tasks::function]
510 async fn asset_path(
511 &self,
512 content_hash: RcStr,
513 original_asset_ident: Vc<AssetIdent>,
514 ) -> Result<Vc<FileSystemPath>> {
515 let source_path = original_asset_ident.path().await?;
516 let basename = source_path.file_name();
517 let asset_path = match source_path.extension_ref() {
518 Some(ext) => format!(
519 "{basename}.{content_hash}.{ext}",
520 basename = &basename[..basename.len() - ext.len() - 1],
521 content_hash = &content_hash[..8]
522 ),
523 None => format!(
524 "{basename}.{content_hash}",
525 content_hash = &content_hash[..8]
526 ),
527 };
528 Ok(self.asset_root_path.join(&asset_path)?.cell())
529 }
530
531 #[turbo_tasks::function]
532 fn is_hot_module_replacement_enabled(&self) -> Vc<bool> {
533 Vc::cell(self.enable_hot_module_replacement)
534 }
535
536 #[turbo_tasks::function]
537 fn chunking_configs(&self) -> Result<Vc<ChunkingConfigs>> {
538 Ok(Vc::cell(self.chunking_configs.iter().cloned().collect()))
539 }
540
541 #[turbo_tasks::function]
542 fn should_use_file_source_map_uris(&self) -> Vc<bool> {
543 Vc::cell(self.should_use_file_source_map_uris)
544 }
545
546 #[turbo_tasks::function]
547 fn is_tracing_enabled(&self) -> Vc<bool> {
548 Vc::cell(self.enable_tracing)
549 }
550
551 #[turbo_tasks::function]
552 fn is_module_merging_enabled(&self) -> Vc<bool> {
553 Vc::cell(self.enable_module_merging)
554 }
555
556 #[turbo_tasks::function]
557 fn is_dynamic_chunk_content_loading_enabled(&self) -> Vc<bool> {
558 Vc::cell(self.enable_dynamic_chunk_content_loading)
559 }
560
561 #[turbo_tasks::function]
562 pub fn minify_type(&self) -> Vc<MinifyType> {
563 self.minify_type.cell()
564 }
565
566 #[turbo_tasks::function]
567 async fn chunk_group(
568 self: ResolvedVc<Self>,
569 ident: Vc<AssetIdent>,
570 chunk_group: ChunkGroup,
571 module_graph: Vc<ModuleGraph>,
572 availability_info: AvailabilityInfo,
573 ) -> Result<Vc<ChunkGroupResult>> {
574 let span = tracing::info_span!("chunking", name = ident.to_string().await?.to_string());
575 async move {
576 let this = self.await?;
577 let modules = chunk_group.entries();
578 let input_availability_info = availability_info;
579 let MakeChunkGroupResult {
580 chunks,
581 availability_info,
582 } = make_chunk_group(
583 modules,
584 module_graph,
585 ResolvedVc::upcast(self),
586 input_availability_info,
587 )
588 .await?;
589
590 let mut assets = chunks
591 .iter()
592 .map(|chunk| self.generate_chunk(**chunk).to_resolved())
593 .try_join()
594 .await?;
595
596 if this.enable_hot_module_replacement {
597 let mut ident = ident;
598 match input_availability_info {
599 AvailabilityInfo::Root => {}
600 AvailabilityInfo::Untracked => {
601 ident = ident.with_modifier(rcstr!("untracked"));
602 }
603 AvailabilityInfo::Complete { available_modules } => {
604 ident =
605 ident.with_modifier(available_modules.hash().await?.to_string().into());
606 }
607 }
608 assets.push(
609 self.generate_chunk_list_register_chunk(
610 ident,
611 EvaluatableAssets::empty(),
612 Vc::cell(assets.clone()),
613 EcmascriptDevChunkListSource::Dynamic,
614 )
615 .to_resolved()
616 .await?,
617 );
618 }
619
620 Ok(ChunkGroupResult {
621 assets: ResolvedVc::cell(assets),
622 availability_info,
623 }
624 .cell())
625 }
626 .instrument(span)
627 .await
628 }
629
630 #[turbo_tasks::function]
631 async fn evaluated_chunk_group(
632 self: ResolvedVc<Self>,
633 ident: Vc<AssetIdent>,
634 chunk_group: ChunkGroup,
635 module_graph: Vc<ModuleGraph>,
636 availability_info: AvailabilityInfo,
637 ) -> Result<Vc<ChunkGroupResult>> {
638 let span = {
639 let ident = ident.to_string().await?.to_string();
640 tracing::info_span!("chunking", chunking_type = "evaluated", ident = ident)
641 };
642 async move {
643 let this = self.await?;
644
645 let entries = chunk_group.entries();
646
647 let MakeChunkGroupResult {
648 chunks,
649 availability_info,
650 } = make_chunk_group(
651 entries,
652 module_graph,
653 ResolvedVc::upcast(self),
654 availability_info,
655 )
656 .await?;
657
658 let mut assets: Vec<ResolvedVc<Box<dyn OutputAsset>>> = chunks
659 .iter()
660 .map(|chunk| self.generate_chunk(**chunk).to_resolved())
661 .try_join()
662 .await?;
663
664 let other_assets = Vc::cell(assets.clone());
665
666 let entries = Vc::cell(
667 chunk_group
668 .entries()
669 .map(|m| {
670 ResolvedVc::try_downcast::<Box<dyn EvaluatableAsset>>(m)
671 .context("evaluated_chunk_group entries must be evaluatable assets")
672 })
673 .collect::<Result<Vec<_>>>()?,
674 );
675
676 if this.enable_hot_module_replacement {
677 assets.push(
678 self.generate_chunk_list_register_chunk(
679 ident,
680 entries,
681 other_assets,
682 EcmascriptDevChunkListSource::Entry,
683 )
684 .to_resolved()
685 .await?,
686 );
687 }
688
689 assets.push(
690 self.generate_evaluate_chunk(ident, other_assets, entries, module_graph)
691 .to_resolved()
692 .await?,
693 );
694
695 Ok(ChunkGroupResult {
696 assets: ResolvedVc::cell(assets),
697 availability_info,
698 }
699 .cell())
700 }
701 .instrument(span)
702 .await
703 }
704
705 #[turbo_tasks::function]
706 fn entry_chunk_group(
707 self: Vc<Self>,
708 _path: FileSystemPath,
709 _evaluatable_assets: Vc<EvaluatableAssets>,
710 _module_graph: Vc<ModuleGraph>,
711 _extra_chunks: Vc<OutputAssets>,
712 _availability_info: AvailabilityInfo,
713 ) -> Result<Vc<EntryChunkGroupResult>> {
714 bail!("Browser chunking context does not support entry chunk groups")
715 }
716
717 #[turbo_tasks::function]
718 fn chunk_item_id_from_ident(&self, ident: Vc<AssetIdent>) -> Vc<ModuleId> {
719 self.module_id_strategy.get_module_id(ident)
720 }
721
722 #[turbo_tasks::function]
723 async fn async_loader_chunk_item(
724 self: Vc<Self>,
725 module: Vc<Box<dyn ChunkableModule>>,
726 module_graph: Vc<ModuleGraph>,
727 availability_info: AvailabilityInfo,
728 ) -> Result<Vc<Box<dyn ChunkItem>>> {
729 Ok(if self.await?.manifest_chunks {
730 let manifest_asset =
731 ManifestAsyncModule::new(module, module_graph, Vc::upcast(self), availability_info);
732 Vc::upcast(ManifestLoaderChunkItem::new(
733 manifest_asset,
734 module_graph,
735 Vc::upcast(self),
736 ))
737 } else {
738 let module = AsyncLoaderModule::new(module, Vc::upcast(self), availability_info);
739 Vc::upcast(module.as_chunk_item(module_graph, Vc::upcast(self)))
740 })
741 }
742
743 #[turbo_tasks::function]
744 async fn async_loader_chunk_item_id(
745 self: Vc<Self>,
746 module: Vc<Box<dyn ChunkableModule>>,
747 ) -> Result<Vc<ModuleId>> {
748 Ok(if self.await?.manifest_chunks {
749 self.chunk_item_id_from_ident(ManifestLoaderChunkItem::asset_ident_for(module))
750 } else {
751 self.chunk_item_id_from_ident(AsyncLoaderModule::asset_ident_for(module))
752 })
753 }
754
755 #[turbo_tasks::function]
756 async fn module_export_usage(
757 self: Vc<Self>,
758 module: ResolvedVc<Box<dyn Module>>,
759 ) -> Result<Vc<ModuleExportUsage>> {
760 if let Some(export_usage) = self.await?.export_usage {
761 Ok(export_usage.await?.used_exports(module).await?)
762 } else {
763 Ok(ModuleExportUsage::all())
766 }
767 }
768}