turbopack_nodejs/
chunking_context.rs

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