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