turbopack_nodejs/
chunking_context.rs

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