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