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(|next| assets_diff(*next, *first, ext.clone(), node_root.clone()).owned())
109            .try_flat_join()
110            .await?;
111        if let Some(detail) = conflicts.into_iter().next() {
112            #[turbo_tasks::function]
113            fn emit_conflict_issue(path: FileSystemPath, detail: RcStr) {
114                EmitConflictIssue {
115                    asset_path: path.clone(),
116                    detail,
117                }
118                .resolved_cell()
119                .emit();
120            }
121            emit_conflict_issue(path.clone(), detail)
122                .as_side_effect()
123                .await?;
124        }
125        Ok(())
126    }
127
128    // Use join! instead of try_join! to collect all errors deterministically
129    // rather than returning whichever branch fails first non-deterministically.
130    let (node_result, client_result) = join!(
131        node_assets_by_path
132            .into_iter()
133            .map(|(path, assets)| {
134                let node_root = node_root.clone();
135
136                async move {
137                    let asset = *assets.first().unwrap();
138                    let span = tracing::info_span!(
139                        "emit asset",
140                        name = %path.to_string_ref().await?
141                    );
142                    async move {
143                        emit(*asset).as_side_effect().await?;
144                        // This need to be after `emit()`, so the asset is emitted even if this
145                        // method crashes due to eventual consistency.
146                        check_duplicates(&path, assets, &node_root).await?;
147                        Ok(())
148                    }
149                    .instrument(span)
150                    .await
151                }
152            })
153            .try_join(),
154        client_assets_by_path
155            .into_iter()
156            .map(|(path, assets)| {
157                let node_root = node_root.clone();
158                let client_relative_path = client_relative_path.clone();
159                let client_output_path = client_output_path.clone();
160
161                async move {
162                    let span = tracing::info_span!(
163                        "emit asset",
164                        name = %path.to_string_ref().await?
165                    );
166                    async move {
167                        let asset = *assets.first().unwrap();
168                        // Client assets are emitted to the client output path, which is
169                        // prefixed with _next. We need to rebase them to
170                        // remove that prefix.
171                        emit_rebase(*asset, client_relative_path, client_output_path)
172                            .as_side_effect()
173                            .await?;
174                        // This need to be after `emit_rebase()`, so the asset is emitted even if
175                        // this method crashes due to eventual consistency.
176                        check_duplicates(&path, assets, &node_root).await?;
177                        Ok(())
178                    }
179                    .instrument(span)
180                    .await
181                }
182            })
183            .try_join(),
184    );
185    node_result?;
186    client_result?;
187    Ok(())
188}
189
190#[turbo_tasks::function]
191async fn emit(asset: Vc<Box<dyn OutputAsset>>) -> Result<()> {
192    asset
193        .content()
194        .to_resolved()
195        .await?
196        .write(asset.path().owned().await?)
197        .as_side_effect()
198        .await?;
199    Ok(())
200}
201
202#[turbo_tasks::function]
203async fn emit_rebase(
204    asset: Vc<Box<dyn OutputAsset>>,
205    from: FileSystemPath,
206    to: FileSystemPath,
207) -> Result<()> {
208    let path = rebase(asset.path().owned().await?, from, to)
209        .owned()
210        .await?;
211    let content = asset.content();
212    content
213        .to_resolved()
214        .await?
215        .write(path)
216        .as_side_effect()
217        .await?;
218    Ok(())
219}
220
221/// Compares two assets that target the same output path. If their content
222/// differs, writes both versions under `node_root` as `<hash>.<ext>` and
223/// returns a description of the difference.
224#[turbo_tasks::function]
225async fn assets_diff(
226    asset1: Vc<Box<dyn OutputAsset>>,
227    asset2: Vc<Box<dyn OutputAsset>>,
228    extension: RcStr,
229    node_root: FileSystemPath,
230) -> Result<Vc<Option<RcStr>>> {
231    let content1 = asset1.content().await?;
232    let content2 = asset2.content().await?;
233
234    let detail = match (&*content1, &*content2) {
235        (AssetContent::File(content1), AssetContent::File(content2)) => {
236            let content1 = content1.await?;
237            let content2 = content2.await?;
238
239            match (&*content1, &*content2) {
240                (FileContent::NotFound, FileContent::NotFound) => None,
241                (FileContent::Content(file1), FileContent::Content(file2)) => {
242                    if file1 == file2 {
243                        None
244                    } else {
245                        // Write both versions under node_root as <hash>.<ext> so the
246                        // user can diff them.
247                        let ext = &*extension;
248                        let hash1 = encode_hex(hash_xxh3_hash64(file1.content().content_hash()));
249                        let hash2 = encode_hex(hash_xxh3_hash64(file2.content().content_hash()));
250                        let name1 = if ext.is_empty() {
251                            hash1
252                        } else {
253                            format!("{hash1}.{ext}")
254                        };
255                        let name2 = if ext.is_empty() {
256                            hash2
257                        } else {
258                            format!("{hash2}.{ext}")
259                        };
260                        let path1 = node_root.join(&name1)?;
261                        let path2 = node_root.join(&name2)?;
262                        path1
263                            .write(FileContent::Content(file1.clone()).cell())
264                            .as_side_effect()
265                            .await?;
266                        path2
267                            .write(FileContent::Content(file2.clone()).cell())
268                            .as_side_effect()
269                            .await?;
270                        Some(format!(
271                            "file content differs, written to:\n  {}\n  {}",
272                            path1.to_string_ref().await?,
273                            path2.to_string_ref().await?,
274                        ))
275                    }
276                }
277                _ => Some(
278                    "assets at the same path have mismatched file content types (one task wants \
279                     to write the file, another wants to delete it)"
280                        .into(),
281                ),
282            }
283        }
284        (AssetContent::Redirect(content1), AssetContent::Redirect(content2)) => {
285            if content1.target == content2.target && content1.target_type == content2.target_type {
286                None
287            } else {
288                Some(format!(
289                    "assets at the same path are both redirects but disagree: {:?} ({:?}) vs {:?} \
290                     ({:?})",
291                    content1.target, content1.target_type, content2.target, content2.target_type,
292                ))
293            }
294        }
295        _ => Some(
296            "assets at the same path have different content types (one is a file, the other is a \
297             redirect)"
298                .into(),
299        ),
300    };
301
302    Ok(Vc::cell(detail.map(|d| d.into())))
303}
304
305#[turbo_tasks::value]
306struct EmitConflictIssue {
307    asset_path: FileSystemPath,
308    detail: RcStr,
309}
310
311#[async_trait]
312#[turbo_tasks::value_impl]
313impl Issue for EmitConflictIssue {
314    async fn file_path(&self) -> Result<FileSystemPath> {
315        Ok(self.asset_path.clone())
316    }
317
318    fn stage(&self) -> IssueStage {
319        IssueStage::Emit
320    }
321
322    fn severity(&self) -> IssueSeverity {
323        IssueSeverity::Error
324    }
325
326    async fn title(&self) -> Result<StyledString> {
327        Ok(StyledString::Text(
328            "Two or more assets with different content were emitted to the same output path".into(),
329        ))
330    }
331
332    async fn description(&self) -> Result<Option<StyledString>> {
333        Ok(Some(StyledString::Text(self.detail.clone())))
334    }
335}