Skip to main content

turbopack_nodejs/ecmascript/node/entry/
chunk_list_content.rs

1use anyhow::{Result, bail};
2use turbo_tasks::{FxIndexMap, ResolvedVc, TryJoinIterExt, Vc};
3use turbo_tasks_fs::FileSystemPath;
4use turbopack_core::{
5    asset::{Asset, AssetContent},
6    chunk::ChunkingContext,
7    output::{
8        ExpandOutputAssetsInput, OutputAsset, OutputAssets, OutputAssetsReferences,
9        expand_output_assets,
10    },
11    version::{Update, Version, VersionedContent},
12};
13use turbopack_ecmascript::chunk_list::{
14    update::update_chunk_list,
15    version::{ChunkListVersion, compute_chunk_list_version},
16};
17
18use crate::NodeJsChunkingContext;
19
20/// Maps each chunk to its `output_root`-relative path and versioned content.
21async fn collect_chunks_contents(
22    output_root: &FileSystemPath,
23    chunks: impl Iterator<Item = ResolvedVc<Box<dyn OutputAsset>>>,
24) -> Result<FxIndexMap<String, ResolvedVc<Box<dyn VersionedContent>>>> {
25    chunks
26        .map(async |chunk| {
27            let chunk_path = chunk.path().await?;
28            let Some(path) = output_root.get_path_to(&chunk_path) else {
29                bail!("chunk {chunk_path} is not inside the output root {output_root}");
30            };
31            Ok((
32                path.to_string(),
33                chunk.versioned_content().to_resolved().await?,
34            ))
35        })
36        .try_join()
37        .await
38        .map(FxIndexMap::from_iter)
39}
40
41/// Never emitted as an asset — the entry chunk already inlines the `R.c(...)`
42/// calls for its shared chunks. This exists purely to give the entry a
43/// *stable* [`VersionedContent`] identity keyed off a chunk-list ident, so
44/// adding/removing/renaming a shared chunk doesn't force a `TotalUpdate`.
45///
46/// Tracks both synchronous chunks and chunks reachable via async-loader
47/// references (dynamic `import()`), so an edit inside a lazy-loaded module
48/// still rides the merged `ChunkListUpdate` instead of being missed.
49#[turbo_tasks::value]
50pub struct EcmascriptBuildNodeChunkListContent {
51    #[bincode(with = "turbo_bincode::indexmap")]
52    pub(super) chunks_contents: FxIndexMap<String, ResolvedVc<Box<dyn VersionedContent>>>,
53}
54
55#[turbo_tasks::value_impl]
56impl EcmascriptBuildNodeChunkListContent {
57    #[turbo_tasks::function]
58    pub async fn new(
59        chunking_context: ResolvedVc<NodeJsChunkingContext>,
60        chunks: ResolvedVc<OutputAssets>,
61        references: ResolvedVc<OutputAssetsReferences>,
62    ) -> Result<Vc<Self>> {
63        let output_root = chunking_context.output_root().owned().await?;
64
65        // Expand async-loader references transitively to reach dynamically
66        // imported chunks. `inner=false`: only follow Reference edges (async
67        // loaders), not Asset-adjacent files like source maps that aren't part
68        // of the module graph and can't be hot-reloaded.
69        let async_chunks = expand_output_assets(
70            references
71                .await?
72                .iter()
73                .copied()
74                .map(ExpandOutputAssetsInput::Reference),
75            false,
76        )
77        .await?;
78
79        let chunks_contents = collect_chunks_contents(
80            &output_root,
81            chunks.await?.iter().copied().chain(async_chunks),
82        )
83        .await?;
84
85        Ok(EcmascriptBuildNodeChunkListContent { chunks_contents }.cell())
86    }
87
88    /// Builds a chunk list content directly from a fixed set of `chunks`,
89    /// without expanding async-loader references. Used by
90    /// [`super::chunk_list::EcmascriptBuildNodeChunkList`] to track chunks
91    /// (e.g. client-component SSR chunks) that are already fully enumerated by
92    /// the caller.
93    #[turbo_tasks::function]
94    pub async fn new_from_chunks(
95        chunking_context: ResolvedVc<NodeJsChunkingContext>,
96        chunks: Vc<OutputAssets>,
97    ) -> Result<Vc<Self>> {
98        let output_root = chunking_context.output_root().owned().await?;
99        let chunks_contents =
100            collect_chunks_contents(&output_root, chunks.await?.iter().copied()).await?;
101
102        Ok(EcmascriptBuildNodeChunkListContent { chunks_contents }.cell())
103    }
104
105    #[turbo_tasks::function]
106    pub async fn version(&self) -> Result<Vc<ChunkListVersion>> {
107        compute_chunk_list_version(&self.chunks_contents).await
108    }
109}
110
111#[turbo_tasks::value_impl]
112impl VersionedContent for EcmascriptBuildNodeChunkListContent {
113    #[turbo_tasks::function]
114    fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
115        bail!("EcmascriptBuildNodeChunkListContent does not have content")
116    }
117
118    #[turbo_tasks::function]
119    fn version(self: Vc<Self>) -> Vc<Box<dyn Version>> {
120        Vc::upcast(self.version())
121    }
122
123    #[turbo_tasks::function]
124    async fn update(
125        self: ResolvedVc<Self>,
126        from_version: ResolvedVc<Box<dyn Version>>,
127    ) -> Result<Vc<Update>> {
128        let this = self.await?;
129        let to_version = self.version();
130        update_chunk_list(&this.chunks_contents, to_version, from_version).await
131    }
132}