1use anyhow::{Context, Result, bail};
2use tracing::Instrument;
3use turbo_rcstr::{RcStr, rcstr};
4use turbo_tasks::{
5 FxIndexMap, ResolvedVc, TryJoinIterExt, Upcast, ValueToString, ValueToStringRef, Vc,
6};
7use turbo_tasks_fs::FileSystemPath;
8use turbo_tasks_hash::HashAlgorithm;
9use turbopack_core::{
10 asset::{Asset, AssetContent},
11 chunk::{
12 AssetSuffix, Chunk, ChunkGroupResult, ChunkItem, ChunkType, ChunkableModule,
13 ChunkingConfig, ChunkingConfigs, ChunkingContext, ContentHashing, EntryChunkGroupResult,
14 EvaluatableAsset, MinifyType, SourceMapSourceType, SourceMapsType, UnusedReferences,
15 UrlBehavior, WorkerConfigurationOptions,
16 availability_info::AvailabilityInfo,
17 chunk_group::{MakeChunkGroupResult, make_chunk_group},
18 chunk_id_strategy::ModuleIdStrategy,
19 },
20 environment::Environment,
21 ident::AssetIdent,
22 module::Module,
23 module_graph::{
24 ModuleGraph,
25 binding_usage_info::{BindingUsageInfo, ModuleExportUsage},
26 chunk_group_info::ChunkGroup,
27 },
28 output::{OutputAsset, OutputAssets},
29};
30use turbopack_ecmascript::{
31 async_chunk::module::AsyncLoaderModule,
32 chunk::EcmascriptChunk,
33 manifest::{chunk_asset::ManifestAsyncModule, loader_module::ManifestLoaderModule},
34};
35use turbopack_ecmascript_runtime::RuntimeType;
36
37use crate::ecmascript::node::{
38 chunk::EcmascriptBuildNodeChunk, entry::chunk::EcmascriptBuildNodeEntryChunk,
39};
40
41pub struct NodeJsChunkingContextBuilder {
43 chunking_context: NodeJsChunkingContext,
44}
45
46impl NodeJsChunkingContextBuilder {
47 pub fn asset_prefix(mut self, asset_prefix: Option<RcStr>) -> Self {
48 self.chunking_context.asset_prefix = asset_prefix;
49 self
50 }
51
52 pub fn asset_prefix_override(mut self, tag: RcStr, prefix: RcStr) -> Self {
53 self.chunking_context.asset_prefixes.insert(tag, prefix);
54 self
55 }
56
57 pub fn asset_root_path_override(mut self, tag: RcStr, path: FileSystemPath) -> Self {
58 self.chunking_context.asset_root_paths.insert(tag, path);
59 self
60 }
61
62 pub fn client_roots_override(mut self, tag: RcStr, path: FileSystemPath) -> Self {
63 self.chunking_context.client_roots.insert(tag, path);
64 self
65 }
66
67 pub fn url_behavior_override(mut self, tag: RcStr, behavior: UrlBehavior) -> Self {
68 self.chunking_context.url_behaviors.insert(tag, behavior);
69 self
70 }
71
72 pub fn default_url_behavior(mut self, behavior: UrlBehavior) -> Self {
73 self.chunking_context.default_url_behavior = Some(behavior);
74 self
75 }
76
77 pub fn minify_type(mut self, minify_type: MinifyType) -> Self {
78 self.chunking_context.minify_type = minify_type;
79 self
80 }
81
82 pub fn source_maps(mut self, source_maps: SourceMapsType) -> Self {
83 self.chunking_context.source_maps_type = source_maps;
84 self
85 }
86
87 pub fn nested_async_availability(mut self, enable_nested_async_availability: bool) -> Self {
88 self.chunking_context.enable_nested_async_availability = enable_nested_async_availability;
89 self
90 }
91
92 pub fn module_merging(mut self, enable_module_merging: bool) -> Self {
93 self.chunking_context.enable_module_merging = enable_module_merging;
94 self
95 }
96
97 pub fn dynamic_chunk_content_loading(
98 mut self,
99 enable_dynamic_chunk_content_loading: bool,
100 ) -> Self {
101 self.chunking_context.enable_dynamic_chunk_content_loading =
102 enable_dynamic_chunk_content_loading;
103 self
104 }
105
106 pub fn runtime_type(mut self, runtime_type: RuntimeType) -> Self {
107 self.chunking_context.runtime_type = runtime_type;
108 self
109 }
110
111 pub fn manifest_chunks(mut self, manifest_chunks: bool) -> Self {
112 self.chunking_context.manifest_chunks = manifest_chunks;
113 self
114 }
115
116 pub fn source_map_source_type(mut self, source_map_source_type: SourceMapSourceType) -> Self {
117 self.chunking_context.source_map_source_type = source_map_source_type;
118 self
119 }
120
121 pub fn module_id_strategy(mut self, module_id_strategy: ResolvedVc<ModuleIdStrategy>) -> Self {
122 self.chunking_context.module_id_strategy = Some(module_id_strategy);
123 self
124 }
125
126 pub fn export_usage(mut self, export_usage: Option<ResolvedVc<BindingUsageInfo>>) -> Self {
127 self.chunking_context.export_usage = export_usage;
128 self
129 }
130
131 pub fn unused_references(mut self, unused_references: ResolvedVc<UnusedReferences>) -> Self {
132 self.chunking_context.unused_references = Some(unused_references);
133 self
134 }
135
136 pub fn chunking_config<T>(mut self, ty: ResolvedVc<T>, chunking_config: ChunkingConfig) -> Self
137 where
138 T: Upcast<Box<dyn ChunkType>>,
139 {
140 self.chunking_context
141 .chunking_configs
142 .push((ResolvedVc::upcast_non_strict(ty), chunking_config));
143 self
144 }
145
146 pub fn debug_ids(mut self, debug_ids: bool) -> Self {
147 self.chunking_context.debug_ids = debug_ids;
148 self
149 }
150
151 pub fn worker_forwarded_globals(mut self, globals: Vec<RcStr>) -> Self {
152 self.chunking_context
153 .worker_forwarded_globals
154 .extend(globals);
155 self
156 }
157
158 pub fn asset_content_hashing(mut self, content_hashing: ContentHashing) -> Self {
159 self.chunking_context.asset_content_hashing = content_hashing;
160 self
161 }
162
163 pub fn hash_salt(mut self, salt: ResolvedVc<RcStr>) -> Self {
164 self.chunking_context.hash_salt = salt;
165 self
166 }
167
168 pub fn build(self) -> Vc<NodeJsChunkingContext> {
170 NodeJsChunkingContext::cell(self.chunking_context)
171 }
172}
173
174#[turbo_tasks::value]
176#[derive(Debug, Clone)]
177pub struct NodeJsChunkingContext {
178 root_path: FileSystemPath,
180 output_root: FileSystemPath,
182 output_root_to_root_path: RcStr,
184 client_root: FileSystemPath,
186 #[bincode(with = "turbo_bincode::indexmap")]
188 client_roots: FxIndexMap<RcStr, FileSystemPath>,
189 chunk_root_path: FileSystemPath,
191 asset_root_path: FileSystemPath,
193 #[bincode(with = "turbo_bincode::indexmap")]
195 asset_root_paths: FxIndexMap<RcStr, FileSystemPath>,
196 asset_prefix: Option<RcStr>,
198 #[bincode(with = "turbo_bincode::indexmap")]
200 asset_prefixes: FxIndexMap<RcStr, RcStr>,
201 #[bincode(with = "turbo_bincode::indexmap")]
203 url_behaviors: FxIndexMap<RcStr, UrlBehavior>,
204 default_url_behavior: Option<UrlBehavior>,
206 environment: ResolvedVc<Environment>,
208 runtime_type: RuntimeType,
210 enable_nested_async_availability: bool,
212 enable_module_merging: bool,
214 enable_dynamic_chunk_content_loading: bool,
216 minify_type: MinifyType,
218 source_maps_type: SourceMapsType,
220 manifest_chunks: bool,
222 module_id_strategy: Option<ResolvedVc<ModuleIdStrategy>>,
224 export_usage: Option<ResolvedVc<BindingUsageInfo>>,
226 unused_references: Option<ResolvedVc<UnusedReferences>>,
228 source_map_source_type: SourceMapSourceType,
230 chunking_configs: Vec<(ResolvedVc<Box<dyn ChunkType>>, ChunkingConfig)>,
232 debug_ids: bool,
234 worker_forwarded_globals: Vec<RcStr>,
236 asset_content_hashing: ContentHashing,
238 hash_salt: ResolvedVc<RcStr>,
240}
241
242impl NodeJsChunkingContext {
243 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 ) -> NodeJsChunkingContextBuilder {
254 NodeJsChunkingContextBuilder {
255 chunking_context: NodeJsChunkingContext {
256 root_path,
257 output_root,
258 output_root_to_root_path,
259 client_root,
260 client_roots: Default::default(),
261 chunk_root_path,
262 asset_root_path,
263 asset_root_paths: Default::default(),
264 asset_prefix: None,
265 asset_prefixes: Default::default(),
266 url_behaviors: Default::default(),
267 default_url_behavior: None,
268 enable_nested_async_availability: false,
269 enable_module_merging: false,
270 enable_dynamic_chunk_content_loading: false,
271 environment,
272 runtime_type,
273 minify_type: MinifyType::NoMinify,
274 source_maps_type: SourceMapsType::Full,
275 manifest_chunks: false,
276 source_map_source_type: SourceMapSourceType::TurbopackUri,
277 module_id_strategy: None,
278 export_usage: None,
279 unused_references: None,
280 chunking_configs: Default::default(),
281 debug_ids: false,
282 worker_forwarded_globals: vec![],
283 asset_content_hashing: ContentHashing::Direct { length: 13 },
284 hash_salt: ResolvedVc::cell(RcStr::default()),
285 },
286 }
287 }
288}
289
290#[turbo_tasks::value_impl]
291impl NodeJsChunkingContext {
292 #[turbo_tasks::function]
297 pub fn runtime_type(&self) -> Vc<RuntimeType> {
298 self.runtime_type.cell()
299 }
300
301 #[turbo_tasks::function]
303 pub fn minify_type(&self) -> Vc<MinifyType> {
304 self.minify_type.cell()
305 }
306
307 #[turbo_tasks::function]
308 pub fn hash_salt(&self) -> Vc<RcStr> {
309 *self.hash_salt
310 }
311
312 #[turbo_tasks::function]
313 pub fn asset_prefix(&self) -> Vc<Option<RcStr>> {
314 Vc::cell(self.asset_prefix.clone())
315 }
316}
317
318impl NodeJsChunkingContext {
319 async fn generate_chunk(
320 self: Vc<Self>,
321 chunk: ResolvedVc<Box<dyn Chunk>>,
322 ) -> Result<ResolvedVc<Box<dyn OutputAsset>>> {
323 Ok(
324 if let Some(ecmascript_chunk) = ResolvedVc::try_downcast_type::<EcmascriptChunk>(chunk)
325 {
326 ResolvedVc::upcast(
327 EcmascriptBuildNodeChunk::new(self, *ecmascript_chunk)
328 .to_resolved()
329 .await?,
330 )
331 } else if let Some(output_asset) =
332 ResolvedVc::try_sidecast::<Box<dyn OutputAsset>>(chunk)
333 {
334 output_asset
335 } else {
336 bail!("Unable to generate output asset for chunk");
337 },
338 )
339 }
340}
341
342#[turbo_tasks::value_impl]
343impl ChunkingContext for NodeJsChunkingContext {
344 #[turbo_tasks::function]
345 fn name(&self) -> Vc<RcStr> {
346 Vc::cell(rcstr!("unknown"))
347 }
348
349 #[turbo_tasks::function]
350 fn root_path(&self) -> Vc<FileSystemPath> {
351 self.root_path.clone().cell()
352 }
353
354 #[turbo_tasks::function]
355 fn output_root(&self) -> Vc<FileSystemPath> {
356 self.output_root.clone().cell()
357 }
358
359 #[turbo_tasks::function]
360 fn output_root_to_root_path(&self) -> Vc<RcStr> {
361 Vc::cell(self.output_root_to_root_path.clone())
362 }
363
364 #[turbo_tasks::function]
365 fn environment(&self) -> Vc<Environment> {
366 *self.environment
367 }
368
369 #[turbo_tasks::function]
370 fn is_nested_async_availability_enabled(&self) -> Vc<bool> {
371 Vc::cell(self.enable_nested_async_availability)
372 }
373
374 #[turbo_tasks::function]
375 fn is_module_merging_enabled(&self) -> Vc<bool> {
376 Vc::cell(self.enable_module_merging)
377 }
378
379 #[turbo_tasks::function]
380 fn is_dynamic_chunk_content_loading_enabled(&self) -> Vc<bool> {
381 Vc::cell(self.enable_dynamic_chunk_content_loading)
382 }
383
384 #[turbo_tasks::function]
385 pub fn minify_type(&self) -> Vc<MinifyType> {
386 self.minify_type.cell()
387 }
388
389 #[turbo_tasks::function]
390 async fn asset_url(&self, ident: FileSystemPath, tag: Option<RcStr>) -> Result<Vc<RcStr>> {
391 let asset_path = ident.to_string();
392
393 let client_root = tag
394 .as_ref()
395 .and_then(|tag| self.client_roots.get(tag))
396 .unwrap_or(&self.client_root);
397
398 let asset_prefix = tag
399 .as_ref()
400 .and_then(|tag| self.asset_prefixes.get(tag))
401 .or(self.asset_prefix.as_ref());
402
403 let asset_path = asset_path
404 .strip_prefix(&format!("{}/", client_root.path))
405 .context("expected client root to contain asset path")?;
406
407 Ok(Vc::cell(
408 format!(
409 "{}{}",
410 asset_prefix.map(|s| s.as_str()).unwrap_or("/"),
411 asset_path
412 )
413 .into(),
414 ))
415 }
416
417 #[turbo_tasks::function]
418 fn chunk_root_path(&self) -> Vc<FileSystemPath> {
419 self.chunk_root_path.clone().cell()
420 }
421
422 #[turbo_tasks::function]
423 async fn chunk_path(
424 &self,
425 _asset: Option<Vc<Box<dyn Asset>>>,
426 ident: Vc<AssetIdent>,
427 prefix: Option<RcStr>,
428 extension: RcStr,
429 ) -> Result<Vc<FileSystemPath>> {
430 let root_path = self.chunk_root_path.clone();
431 let name = ident
432 .output_name(self.root_path.clone(), prefix, extension)
433 .owned()
434 .await?;
435 Ok(root_path.join(&name)?.cell())
436 }
437
438 #[turbo_tasks::function]
439 fn reference_chunk_source_maps(&self, _chunk: Vc<Box<dyn OutputAsset>>) -> Vc<bool> {
440 Vc::cell(match self.source_maps_type {
441 SourceMapsType::Full => true,
442 SourceMapsType::Partial => true,
443 SourceMapsType::None => false,
444 })
445 }
446
447 #[turbo_tasks::function]
448 fn reference_module_source_maps(&self, _module: Vc<Box<dyn Module>>) -> Vc<bool> {
449 Vc::cell(match self.source_maps_type {
450 SourceMapsType::Full => true,
451 SourceMapsType::Partial => true,
452 SourceMapsType::None => false,
453 })
454 }
455
456 #[turbo_tasks::function]
457 fn source_map_source_type(&self) -> Vc<SourceMapSourceType> {
458 self.source_map_source_type.cell()
459 }
460
461 #[turbo_tasks::function]
462 fn chunking_configs(&self) -> Result<Vc<ChunkingConfigs>> {
463 Ok(Vc::cell(self.chunking_configs.iter().cloned().collect()))
464 }
465
466 #[turbo_tasks::function]
467 async fn asset_path(
468 self: Vc<Self>,
469 content: Vc<AssetContent>,
470 original_asset_ident: Vc<AssetIdent>,
471 tag: Option<RcStr>,
472 ) -> Result<Vc<FileSystemPath>> {
473 let this = self.await?;
474 let source_path = original_asset_ident.await?.path.clone();
475 let basename = source_path.file_name();
476 let ContentHashing::Direct { length } = this.asset_content_hashing;
477 let hash = content
478 .content_hash(self.hash_salt(), HashAlgorithm::Xxh3Hash128Base38)
479 .await?;
480 let hash = hash
481 .as_ref()
482 .context("Missing content when trying to generate the content hash for static asset")?;
483 let short_hash = &hash[..length as usize];
484 let asset_path = match source_path.extension() {
485 Some(ext) => format!(
486 "{basename}.{short_hash}.{ext}",
487 basename = &basename[..basename.len() - ext.len() - 1],
488 ),
489 None => format!("{basename}.{short_hash}"),
490 };
491
492 let asset_root_path = tag
493 .as_ref()
494 .and_then(|tag| this.asset_root_paths.get(tag))
495 .unwrap_or(&this.asset_root_path);
496
497 Ok(asset_root_path.join(&asset_path)?.cell())
498 }
499
500 #[turbo_tasks::function]
501 fn url_behavior(&self, tag: Option<RcStr>) -> Vc<UrlBehavior> {
502 tag.as_ref()
503 .and_then(|tag| self.url_behaviors.get(tag))
504 .cloned()
505 .or_else(|| self.default_url_behavior.clone())
506 .unwrap_or(UrlBehavior {
507 suffix: AssetSuffix::Inferred,
508 static_suffix: ResolvedVc::cell(None),
509 })
510 .cell()
511 }
512
513 #[turbo_tasks::function]
514 async fn chunk_group(
515 self: ResolvedVc<Self>,
516 ident: Vc<AssetIdent>,
517 chunk_group: ChunkGroup,
518 module_graph: ResolvedVc<ModuleGraph>,
519 availability_info: AvailabilityInfo,
520 ) -> Result<Vc<ChunkGroupResult>> {
521 let span = tracing::info_span!("chunking", name = display(ident.to_string().await?));
522 async move {
523 let MakeChunkGroupResult {
524 chunks,
525 references,
526 availability_info,
527 } = make_chunk_group(
528 chunk_group,
529 module_graph,
530 ResolvedVc::upcast(self),
531 availability_info,
532 )
533 .await?;
534
535 let chunks = chunks.await?;
536
537 let assets = chunks
538 .iter()
539 .map(|chunk| self.generate_chunk(*chunk))
540 .try_join()
541 .await?;
542
543 Ok(ChunkGroupResult {
544 assets: ResolvedVc::cell(assets),
545 referenced_assets: OutputAssets::empty_resolved(),
546 references: ResolvedVc::cell(references),
547 availability_info,
548 }
549 .cell())
550 }
551 .instrument(span)
552 .await
553 }
554
555 #[turbo_tasks::function]
556 pub async fn entry_chunk_group(
557 self: ResolvedVc<Self>,
558 path: FileSystemPath,
559 chunk_group: ChunkGroup,
560 module_graph: ResolvedVc<ModuleGraph>,
561 extra_chunks: Vc<OutputAssets>,
562 extra_referenced_assets: Vc<OutputAssets>,
563 availability_info: AvailabilityInfo,
564 ) -> Result<Vc<EntryChunkGroupResult>> {
565 let span = tracing::info_span!(
566 "chunking",
567 name = display(path.to_string_ref().await?),
568 chunking_type = "entry",
569 );
570 async move {
571 let MakeChunkGroupResult {
572 chunks,
573 references,
574 availability_info,
575 } = make_chunk_group(
576 chunk_group.clone(),
577 module_graph,
578 ResolvedVc::upcast(self),
579 availability_info,
580 )
581 .await?;
582
583 let chunks = chunks.await?;
584
585 let extra_chunks = extra_chunks.await?;
586 let mut other_chunks = chunks
587 .iter()
588 .map(|chunk| self.generate_chunk(*chunk))
589 .try_join()
590 .await?;
591 other_chunks.extend(extra_chunks.iter().copied());
592
593 let Some(module) = ResolvedVc::try_sidecast(chunk_group.entries().last().unwrap())
594 else {
595 bail!("module must be placeable in an ecmascript chunk");
596 };
597
598 let evaluatable_assets = chunk_group
599 .entries()
600 .map(|entry| {
601 ResolvedVc::try_sidecast::<Box<dyn EvaluatableAsset>>(entry)
602 .context("entry_chunk_group entries must be evaluatable")
603 })
604 .collect::<Result<Vec<_>>>()?;
605
606 let asset = ResolvedVc::upcast(
607 EcmascriptBuildNodeEntryChunk::new(
608 path,
609 Vc::cell(other_chunks),
610 Vc::cell(evaluatable_assets),
611 *module,
612 extra_referenced_assets,
613 Vc::cell(references),
614 *module_graph,
615 *self,
616 )
617 .to_resolved()
618 .await?,
619 );
620
621 Ok(EntryChunkGroupResult {
622 asset,
623 availability_info,
624 }
625 .cell())
626 }
627 .instrument(span)
628 .await
629 }
630
631 #[turbo_tasks::function]
632 fn evaluated_chunk_group(
633 self: Vc<Self>,
634 _ident: Vc<AssetIdent>,
635 _chunk_group: ChunkGroup,
636 _module_graph: Vc<ModuleGraph>,
637 _extra_chunks: Vc<OutputAssets>,
638 _availability_info: AvailabilityInfo,
639 ) -> Result<Vc<ChunkGroupResult>> {
640 bail!("the Node.js chunking context does not support evaluated chunk groups")
641 }
642
643 #[turbo_tasks::function]
644 fn chunk_item_id_strategy(&self) -> Vc<ModuleIdStrategy> {
645 *self
646 .module_id_strategy
647 .unwrap_or_else(|| ModuleIdStrategy::default().resolved_cell())
648 }
649
650 #[turbo_tasks::function]
651 async fn async_loader_chunk_item(
652 self: Vc<Self>,
653 module: Vc<Box<dyn ChunkableModule>>,
654 module_graph: Vc<ModuleGraph>,
655 availability_info: AvailabilityInfo,
656 ) -> Result<Vc<Box<dyn ChunkItem>>> {
657 let chunking_context: ResolvedVc<Box<dyn ChunkingContext>> =
658 Vc::upcast::<Box<dyn ChunkingContext>>(self)
659 .to_resolved()
660 .await?;
661 Ok(if self.await?.manifest_chunks {
662 let manifest_asset = ManifestAsyncModule::new(
663 module,
664 module_graph,
665 *chunking_context,
666 availability_info,
667 )
668 .to_resolved()
669 .await?;
670 let loader_module = ManifestLoaderModule::new(*manifest_asset);
671 loader_module.as_chunk_item(module_graph, *chunking_context)
672 } else {
673 let module = AsyncLoaderModule::new(module, *chunking_context, availability_info);
674 module.as_chunk_item(module_graph, *chunking_context)
675 })
676 }
677
678 #[turbo_tasks::function]
679 async fn async_loader_chunk_item_ident(
680 self: Vc<Self>,
681 module: Vc<Box<dyn ChunkableModule>>,
682 ) -> Result<Vc<AssetIdent>> {
683 Ok(if self.await?.manifest_chunks {
684 ManifestLoaderModule::asset_ident_for(module)
685 } else {
686 AsyncLoaderModule::asset_ident_for(module)
687 })
688 }
689
690 #[turbo_tasks::function]
691 async fn module_export_usage(
692 &self,
693 module: ResolvedVc<Box<dyn Module>>,
694 ) -> Result<Vc<ModuleExportUsage>> {
695 if let Some(export_usage) = self.export_usage {
696 Ok(export_usage.await?.used_exports(module).await?)
697 } else {
698 Ok(ModuleExportUsage::all())
699 }
700 }
701
702 #[turbo_tasks::function]
703 fn unused_references(&self) -> Vc<UnusedReferences> {
704 if let Some(unused_references) = self.unused_references {
705 *unused_references
706 } else {
707 Vc::cell(Default::default())
708 }
709 }
710
711 #[turbo_tasks::function]
712 fn debug_ids_enabled(&self) -> Vc<bool> {
713 Vc::cell(self.debug_ids)
714 }
715
716 #[turbo_tasks::function]
717 fn worker_configuration_options(&self) -> Vc<WorkerConfigurationOptions> {
718 WorkerConfigurationOptions {
719 asset_prefix: None,
720 forwarded_globals: self.worker_forwarded_globals.clone(),
721 }
722 .cell()
723 }
724}