turbopack_nodejs/
chunking_context.rs

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