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