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#[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#[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 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 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 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#[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 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}