Skip to main content

turbopack_nodejs/ecmascript/node/entry/
chunk_list_content.rs

1use anyhow::{Result, bail};
2use turbo_tasks::{FxIndexMap, ReadRef, ResolvedVc, TransientInstance, 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 compute_update_from_version(
59        self: Vc<Self>,
60        from: TransientInstance<ReadRef<ChunkListVersion>>,
61    ) -> Result<Vc<Update>> {
62        let to = self.version();
63        update_chunk_list(
64            &self.await?.chunks_contents,
65            to,
66            ResolvedVc::upcast(ReadRef::resolved_cell((*from).clone())),
67        )
68        .await
69    }
70
71    #[turbo_tasks::function]
72    pub async fn new(
73        chunking_context: ResolvedVc<NodeJsChunkingContext>,
74        chunks: ResolvedVc<OutputAssets>,
75        referenced_assets: ResolvedVc<OutputAssets>,
76        references: ResolvedVc<OutputAssetsReferences>,
77    ) -> Result<Vc<Self>> {
78        let output_root = chunking_context.output_root().owned().await?;
79
80        // Expand async-loader references transitively to reach dynamically
81        // imported chunks. `inner=false`: only follow Reference edges (async
82        // loaders), not Asset-adjacent files like source maps that aren't part
83        // of the module graph and can't be hot-reloaded.
84        //
85        // `referenced_assets` covers async chunks that were already expanded by
86        // the caller (e.g. chunks reachable from concatenated chunk groups).
87        // They must be tracked here too, otherwise an edit inside one of them
88        // produces no chunk list update at all.
89        let async_chunks = expand_output_assets(
90            referenced_assets
91                .await?
92                .iter()
93                .copied()
94                .map(ExpandOutputAssetsInput::Asset)
95                .chain(
96                    references
97                        .await?
98                        .iter()
99                        .copied()
100                        .map(ExpandOutputAssetsInput::Reference),
101                ),
102            false,
103        )
104        .await?;
105
106        let chunks_contents = collect_chunks_contents(
107            &output_root,
108            chunks.await?.iter().copied().chain(async_chunks),
109        )
110        .await?;
111
112        Ok(EcmascriptBuildNodeChunkListContent { chunks_contents }.cell())
113    }
114
115    /// Builds a chunk list content directly from a fixed set of `chunks`,
116    /// without expanding async-loader references. Used by
117    /// `super::chunk_list::EcmascriptBuildNodeChunkList` to track chunks
118    /// (e.g. client-component SSR chunks) that are already fully enumerated by
119    /// the caller.
120    #[turbo_tasks::function]
121    pub async fn new_from_chunks(
122        chunking_context: ResolvedVc<NodeJsChunkingContext>,
123        chunks: Vc<OutputAssets>,
124    ) -> Result<Vc<Self>> {
125        let output_root = chunking_context.output_root().owned().await?;
126        let chunks_contents =
127            collect_chunks_contents(&output_root, chunks.await?.iter().copied()).await?;
128
129        Ok(EcmascriptBuildNodeChunkListContent { chunks_contents }.cell())
130    }
131
132    #[turbo_tasks::function]
133    pub async fn version(&self) -> Result<Vc<ChunkListVersion>> {
134        compute_chunk_list_version(&self.chunks_contents).await
135    }
136}
137
138#[turbo_tasks::value_impl]
139impl VersionedContent for EcmascriptBuildNodeChunkListContent {
140    #[turbo_tasks::function]
141    fn content(self: Vc<Self>) -> Result<Vc<AssetContent>> {
142        bail!("EcmascriptBuildNodeChunkListContent does not have content")
143    }
144
145    #[turbo_tasks::function]
146    fn version(self: Vc<Self>) -> Vc<Box<dyn Version>> {
147        Vc::upcast(self.version())
148    }
149
150    #[turbo_tasks::function]
151    async fn update(
152        self: ResolvedVc<Self>,
153        from_version: ResolvedVc<Box<dyn Version>>,
154    ) -> Result<Vc<Update>> {
155        let this = self.await?;
156        let to_version = self.version();
157        update_chunk_list(&this.chunks_contents, to_version, from_version).await
158    }
159}
160
161#[turbo_tasks::function(operation, root)]
162pub fn compute_update_from_version_operation(
163    content: ResolvedVc<EcmascriptBuildNodeChunkListContent>,
164    from: TransientInstance<ReadRef<ChunkListVersion>>,
165) -> Vc<Update> {
166    content.compute_update_from_version(from)
167}