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