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