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