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<()> {
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 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 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 emit_rebase(*asset, client_relative_path, client_output_path)
172 .as_side_effect()
173 .await?;
174 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#[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 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}