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(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 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 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 emit_rebase(*asset, client_relative_path, client_output_path)
176 .as_side_effect()
177 .await?;
178 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#[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 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}