Skip to main content

next_core/
emit.rs

1use anyhow::{Ok, Result};
2use async_trait::async_trait;
3use futures::join;
4use smallvec::{SmallVec, smallvec};
5use tracing::Instrument;
6use turbo_rcstr::RcStr;
7use turbo_tasks::{
8    FxIndexMap, ResolvedVc, TryFlatJoinIterExt, TryJoinIterExt, ValueToStringRef, Vc,
9};
10use turbo_tasks_fs::{FileContent, FileSystemPath, rebase};
11use turbo_tasks_hash::{encode_hex, hash_xxh3_hash64};
12use turbopack_core::{
13    asset::{Asset, AssetContent},
14    issue::{Issue, IssueExt, IssueSeverity, IssueStage, StyledString},
15    output::{ExpandedOutputAssets, OutputAsset, OutputAssets},
16    reference::all_assets_from_entries,
17};
18
19/// Emits all assets transitively reachable from the given chunks, that are
20/// inside the node root or the client root.
21///
22/// Assets inside the given client root are rebased to the given client output
23/// path.
24#[turbo_tasks::function]
25pub async fn emit_all_assets(
26    assets: Vc<OutputAssets>,
27    node_root: FileSystemPath,
28    client_relative_path: FileSystemPath,
29    client_output_path: FileSystemPath,
30) -> Result<()> {
31    emit_assets(
32        all_assets_from_entries(assets),
33        node_root,
34        client_relative_path,
35        client_output_path,
36    )
37    .as_side_effect()
38    .await?;
39    Ok(())
40}
41
42/// Emits all assets transitively reachable from the given chunks, that are
43/// inside the node root or the client root.
44///
45/// Assets inside the given client root are rebased to the given client output
46/// path.
47#[turbo_tasks::function]
48pub async fn emit_assets(
49    assets: Vc<ExpandedOutputAssets>,
50    node_root: FileSystemPath,
51    client_relative_path: FileSystemPath,
52    client_output_path: FileSystemPath,
53) -> Result<()> {
54    enum Location {
55        Node,
56        Client,
57    }
58    let assets = assets
59        .await?
60        .iter()
61        .copied()
62        .map(async |asset| {
63            let path = asset.path().owned().await?;
64            let location = if path.is_inside_ref(&node_root) {
65                Location::Node
66            } else if path.is_inside_ref(&client_relative_path) {
67                Location::Client
68            } else {
69                return Ok(None);
70            };
71            Ok(Some((location, path, asset)))
72        })
73        .try_flat_join()
74        .await?;
75
76    type AssetVec = SmallVec<[ResolvedVc<Box<dyn OutputAsset>>; 1]>;
77    let mut node_assets_by_path: FxIndexMap<FileSystemPath, AssetVec> = FxIndexMap::default();
78    let mut client_assets_by_path: FxIndexMap<FileSystemPath, AssetVec> = FxIndexMap::default();
79    for (location, path, asset) in assets {
80        match location {
81            Location::Node => {
82                node_assets_by_path
83                    .entry(path)
84                    .or_insert_with(|| smallvec![])
85                    .push(asset);
86            }
87            Location::Client => {
88                client_assets_by_path
89                    .entry(path)
90                    .or_insert_with(|| smallvec![])
91                    .push(asset);
92            }
93        }
94    }
95
96    /// Checks for duplicate assets at the same path. If duplicates with
97    /// different content are found, emits an `EmitConflictIssue` for each
98    /// conflict but still returns the first asset so emission can continue.
99    async fn check_duplicates(
100        path: &FileSystemPath,
101        assets: AssetVec,
102        node_root: &FileSystemPath,
103    ) -> Result<()> {
104        let mut iter = assets.into_iter();
105        let first = iter.next().unwrap();
106        let ext: RcStr = path.extension().unwrap_or_default().into();
107        let conflicts = iter
108            .map(async |next| {
109                assets_diff(*next, *first, ext.clone(), node_root.clone())
110                    .owned()
111                    .await
112            })
113            .try_flat_join()
114            .await?;
115        if let Some(detail) = conflicts.into_iter().next() {
116            #[turbo_tasks::function]
117            fn emit_conflict_issue(path: FileSystemPath, detail: RcStr) {
118                EmitConflictIssue {
119                    asset_path: path.clone(),
120                    detail,
121                }
122                .resolved_cell()
123                .emit();
124            }
125            emit_conflict_issue(path.clone(), detail)
126                .as_side_effect()
127                .await?;
128        }
129        Ok(())
130    }
131
132    // Use join! instead of try_join! to collect all errors deterministically
133    // rather than returning whichever branch fails first non-deterministically.
134    let (node_result, client_result) = join!(
135        node_assets_by_path
136            .into_iter()
137            .map(|(path, assets)| {
138                let node_root = node_root.clone();
139
140                async move {
141                    let asset = *assets.first().unwrap();
142                    let span = tracing::info_span!(
143                        "emit asset",
144                        name = %path.to_string_ref().await?
145                    );
146                    async move {
147                        emit(*asset).as_side_effect().await?;
148                        // This need to be after `emit()`, so the asset is emitted even if this
149                        // method crashes due to eventual consistency.
150                        check_duplicates(&path, assets, &node_root).await?;
151                        Ok(())
152                    }
153                    .instrument(span)
154                    .await
155                }
156            })
157            .try_join(),
158        client_assets_by_path
159            .into_iter()
160            .map(|(path, assets)| {
161                let node_root = node_root.clone();
162                let client_relative_path = client_relative_path.clone();
163                let client_output_path = client_output_path.clone();
164
165                async move {
166                    let span = tracing::info_span!(
167                        "emit asset",
168                        name = %path.to_string_ref().await?
169                    );
170                    async move {
171                        let asset = *assets.first().unwrap();
172                        // Client assets are emitted to the client output path, which is
173                        // prefixed with _next. We need to rebase them to
174                        // remove that prefix.
175                        emit_rebase(*asset, client_relative_path, client_output_path)
176                            .as_side_effect()
177                            .await?;
178                        // This need to be after `emit_rebase()`, so the asset is emitted even if
179                        // this method crashes due to eventual consistency.
180                        check_duplicates(&path, assets, &node_root).await?;
181                        Ok(())
182                    }
183                    .instrument(span)
184                    .await
185                }
186            })
187            .try_join(),
188    );
189    node_result?;
190    client_result?;
191    Ok(())
192}
193
194#[turbo_tasks::function]
195async fn emit(asset: Vc<Box<dyn OutputAsset>>) -> Result<()> {
196    asset
197        .content()
198        .to_resolved()
199        .await?
200        .write(asset.path().owned().await?)
201        .as_side_effect()
202        .await?;
203    Ok(())
204}
205
206#[turbo_tasks::function]
207async fn emit_rebase(
208    asset: Vc<Box<dyn OutputAsset>>,
209    from: FileSystemPath,
210    to: FileSystemPath,
211) -> Result<()> {
212    let path = rebase(asset.path().owned().await?, from, to)
213        .owned()
214        .await?;
215    let content = asset.content();
216    content
217        .to_resolved()
218        .await?
219        .write(path)
220        .as_side_effect()
221        .await?;
222    Ok(())
223}
224
225/// Compares two assets that target the same output path. If their content
226/// differs, writes both versions under `node_root` as `<hash>.<ext>` and
227/// returns a description of the difference.
228#[turbo_tasks::function]
229async fn assets_diff(
230    asset1: Vc<Box<dyn OutputAsset>>,
231    asset2: Vc<Box<dyn OutputAsset>>,
232    extension: RcStr,
233    node_root: FileSystemPath,
234) -> Result<Vc<Option<RcStr>>> {
235    let content1 = asset1.content().await?;
236    let content2 = asset2.content().await?;
237
238    let detail = match (&*content1, &*content2) {
239        (AssetContent::File(content1), AssetContent::File(content2)) => {
240            let content1 = content1.await?;
241            let content2 = content2.await?;
242
243            match (&*content1, &*content2) {
244                (FileContent::NotFound, FileContent::NotFound) => None,
245                (FileContent::Content(file1), FileContent::Content(file2)) => {
246                    if file1 == file2 {
247                        None
248                    } else {
249                        // Write both versions under node_root as <hash>.<ext> so the
250                        // user can diff them.
251                        let ext = &*extension;
252                        let hash1 = encode_hex(hash_xxh3_hash64(file1.content().content_hash()));
253                        let hash2 = encode_hex(hash_xxh3_hash64(file2.content().content_hash()));
254                        let name1 = if ext.is_empty() {
255                            hash1
256                        } else {
257                            format!("{hash1}.{ext}")
258                        };
259                        let name2 = if ext.is_empty() {
260                            hash2
261                        } else {
262                            format!("{hash2}.{ext}")
263                        };
264                        let path1 = node_root.join(&name1)?;
265                        let path2 = node_root.join(&name2)?;
266                        path1
267                            .write(FileContent::Content(file1.clone()).cell())
268                            .as_side_effect()
269                            .await?;
270                        path2
271                            .write(FileContent::Content(file2.clone()).cell())
272                            .as_side_effect()
273                            .await?;
274                        Some(format!(
275                            "file content differs, written to:\n  {}\n  {}",
276                            path1.to_string_ref().await?,
277                            path2.to_string_ref().await?,
278                        ))
279                    }
280                }
281                _ => Some(
282                    "assets at the same path have mismatched file content types (one task wants \
283                     to write the file, another wants to delete it)"
284                        .into(),
285                ),
286            }
287        }
288        (
289            AssetContent::Redirect {
290                target: target1,
291                link_type: link_type1,
292            },
293            AssetContent::Redirect {
294                target: target2,
295                link_type: link_type2,
296            },
297        ) => {
298            if target1 == target2 && link_type1 == link_type2 {
299                None
300            } else {
301                Some(format!(
302                    "assets at the same path are both redirects but point to different targets: \
303                     {target1} vs {target2}"
304                ))
305            }
306        }
307        _ => Some(
308            "assets at the same path have different content types (one is a file, the other is a \
309             redirect)"
310                .into(),
311        ),
312    };
313
314    Ok(Vc::cell(detail.map(|d| d.into())))
315}
316
317#[turbo_tasks::value]
318struct EmitConflictIssue {
319    asset_path: FileSystemPath,
320    detail: RcStr,
321}
322
323#[async_trait]
324#[turbo_tasks::value_impl]
325impl Issue for EmitConflictIssue {
326    async fn file_path(&self) -> Result<FileSystemPath> {
327        Ok(self.asset_path.clone())
328    }
329
330    fn stage(&self) -> IssueStage {
331        IssueStage::Emit
332    }
333
334    fn severity(&self) -> IssueSeverity {
335        IssueSeverity::Error
336    }
337
338    async fn title(&self) -> Result<StyledString> {
339        Ok(StyledString::Text(
340            "Two or more assets with different content were emitted to the same output path".into(),
341        ))
342    }
343
344    async fn description(&self) -> Result<Option<StyledString>> {
345        Ok(Some(StyledString::Text(self.detail.clone())))
346    }
347}