1use std::fmt::Write;
2
3use anyhow::Result;
4use once_cell::sync::Lazy;
5use regex::Regex;
6use serde::{Deserialize, Serialize};
7use turbo_rcstr::RcStr;
8use turbo_tasks::{NonLocalValue, ResolvedVc, TaskInput, ValueToString, Vc, trace::TraceRawVcs};
9use turbo_tasks_fs::FileSystemPath;
10use turbo_tasks_hash::{DeterministicHash, Xxh3Hash64Hasher, encode_hex, hash_xxh3_hash64};
11
12use crate::resolve::ModulePart;
13
14#[derive(
16 Clone,
17 TaskInput,
18 Hash,
19 Debug,
20 DeterministicHash,
21 Eq,
22 PartialEq,
23 TraceRawVcs,
24 Serialize,
25 Deserialize,
26 NonLocalValue,
27)]
28pub struct Layer {
29 name: RcStr,
30 user_friendly_name: Option<RcStr>,
31}
32
33impl Layer {
34 pub fn new(name: RcStr) -> Self {
35 debug_assert!(!name.is_empty());
36 Self {
37 name,
38 user_friendly_name: None,
39 }
40 }
41 pub fn new_with_user_friendly_name(name: RcStr, user_friendly_name: RcStr) -> Self {
42 debug_assert!(!name.is_empty());
43 debug_assert!(!user_friendly_name.is_empty());
44 Self {
45 name,
46 user_friendly_name: Some(user_friendly_name),
47 }
48 }
49
50 pub fn user_friendly_name(&self) -> &RcStr {
52 self.user_friendly_name.as_ref().unwrap_or(&self.name)
53 }
54
55 pub fn name(&self) -> &RcStr {
56 &self.name
57 }
58}
59
60#[turbo_tasks::value]
61#[derive(Clone, Debug, Hash, TaskInput)]
62pub struct AssetIdent {
63 pub path: FileSystemPath,
65 pub query: RcStr,
68 pub fragment: RcStr,
71 pub assets: Vec<(RcStr, ResolvedVc<AssetIdent>)>,
73 pub modifiers: Vec<RcStr>,
75 pub parts: Vec<ModulePart>,
77 pub layer: Option<Layer>,
79 pub content_type: Option<RcStr>,
81}
82
83impl AssetIdent {
84 pub fn add_modifier(&mut self, modifier: RcStr) {
85 debug_assert!(!modifier.is_empty(), "modifiers cannot be empty.");
86 self.modifiers.push(modifier);
87 }
88
89 pub fn add_asset(&mut self, key: RcStr, asset: ResolvedVc<AssetIdent>) {
90 self.assets.push((key, asset));
91 }
92
93 pub async fn rename_as_ref(&mut self, pattern: &str) -> Result<()> {
94 let root = self.path.root().await?;
95 let path = self.path.clone();
96 self.path = root.join(&pattern.replace('*', &path.path))?;
97 Ok(())
98 }
99}
100
101#[turbo_tasks::value_impl]
102impl ValueToString for AssetIdent {
103 #[turbo_tasks::function]
104 async fn to_string(&self) -> Result<Vc<RcStr>> {
105 let mut s = self.path.value_to_string().owned().await?.into_owned();
106
107 s.push_str(&self.query);
109 s.push_str(&self.fragment);
111
112 if !self.assets.is_empty() {
113 s.push_str(" {");
114
115 for (i, (key, asset)) in self.assets.iter().enumerate() {
116 if i > 0 {
117 s.push(',');
118 }
119
120 let asset_str = asset.to_string().await?;
121 write!(s, " {key} => {asset_str:?}")?;
122 }
123
124 s.push_str(" }");
125 }
126
127 if let Some(layer) = &self.layer {
128 write!(s, " [{}]", layer.name)?;
129 }
130
131 if !self.modifiers.is_empty() {
132 s.push_str(" (");
133
134 for (i, modifier) in self.modifiers.iter().enumerate() {
135 if i > 0 {
136 s.push_str(", ");
137 }
138
139 s.push_str(modifier);
140 }
141
142 s.push(')');
143 }
144
145 if let Some(content_type) = &self.content_type {
146 write!(s, " <{content_type}>")?;
147 }
148
149 if !self.parts.is_empty() {
150 for part in self.parts.iter() {
151 if !matches!(part, ModulePart::Facade) {
152 write!(s, " <{part}>")?;
155 }
156 }
157 }
158
159 Ok(Vc::cell(s.into()))
160 }
161}
162
163#[turbo_tasks::value_impl]
164impl AssetIdent {
165 #[turbo_tasks::function]
166 pub fn new(ident: AssetIdent) -> Vc<Self> {
167 debug_assert!(
168 ident.query.is_empty() || ident.query.starts_with("?"),
169 "query should be empty or start with a `?`"
170 );
171 debug_assert!(
172 ident.fragment.is_empty() || ident.fragment.starts_with("#"),
173 "query should be empty or start with a `?`"
174 );
175 ident.cell()
176 }
177
178 #[turbo_tasks::function]
180 pub fn from_path(path: FileSystemPath) -> Vc<Self> {
181 Self::new(AssetIdent {
182 path,
183 query: RcStr::default(),
184 fragment: RcStr::default(),
185 assets: Vec::new(),
186 modifiers: Vec::new(),
187 parts: Vec::new(),
188 layer: None,
189 content_type: None,
190 })
191 }
192
193 #[turbo_tasks::function]
194 pub fn with_query(&self, query: RcStr) -> Vc<Self> {
195 let mut this = self.clone();
196 this.query = query;
197 Self::new(this)
198 }
199
200 #[turbo_tasks::function]
201 pub fn with_fragment(&self, fragment: RcStr) -> Vc<Self> {
202 let mut this = self.clone();
203 this.fragment = fragment;
204 Self::new(this)
205 }
206
207 #[turbo_tasks::function]
208 pub fn with_modifier(&self, modifier: RcStr) -> Vc<Self> {
209 let mut this = self.clone();
210 this.add_modifier(modifier);
211 Self::new(this)
212 }
213
214 #[turbo_tasks::function]
215 pub fn with_part(&self, part: ModulePart) -> Vc<Self> {
216 let mut this = self.clone();
217 this.parts.push(part);
218 Self::new(this)
219 }
220
221 #[turbo_tasks::function]
222 pub fn with_path(&self, path: FileSystemPath) -> Vc<Self> {
223 let mut this = self.clone();
224 this.path = path;
225 Self::new(this)
226 }
227
228 #[turbo_tasks::function]
229 pub fn with_layer(&self, layer: Layer) -> Vc<Self> {
230 let mut this = self.clone();
231 this.layer = Some(layer);
232 Self::new(this)
233 }
234
235 #[turbo_tasks::function]
236 pub fn with_content_type(&self, content_type: RcStr) -> Vc<Self> {
237 let mut this = self.clone();
238 this.content_type = Some(content_type);
239 Self::new(this)
240 }
241
242 #[turbo_tasks::function]
243 pub fn with_asset(&self, key: RcStr, asset: ResolvedVc<AssetIdent>) -> Vc<Self> {
244 let mut this = self.clone();
245 this.add_asset(key, asset);
246 Self::new(this)
247 }
248
249 #[turbo_tasks::function]
250 pub async fn rename_as(&self, pattern: RcStr) -> Result<Vc<Self>> {
251 let mut this = self.clone();
252 this.rename_as_ref(&pattern).await?;
253 Ok(Self::new(this))
254 }
255
256 #[turbo_tasks::function]
257 pub fn path(&self) -> Vc<FileSystemPath> {
258 self.path.clone().cell()
259 }
260
261 #[turbo_tasks::function]
266 pub async fn output_name(
267 &self,
268 context_path: FileSystemPath,
269 prefix: Option<RcStr>,
270 expected_extension: RcStr,
271 ) -> Result<Vc<RcStr>> {
272 debug_assert!(
273 expected_extension.starts_with("."),
274 "the extension should include the leading '.', got '{expected_extension}'"
275 );
276 let path = &self.path;
281 let mut name = if let Some(inner) = context_path.get_path_to(path) {
282 clean_separators(inner)
283 } else {
284 clean_separators(&self.path.value_to_string().await?)
285 };
286 let removed_extension = name.ends_with(&*expected_extension);
287 if removed_extension {
288 name.truncate(name.len() - expected_extension.len());
289 }
290 let mut name = clean_additional_extensions(&name);
294 if let Some(prefix) = prefix {
295 name = format!("{prefix}-{name}");
296 }
297
298 let default_modifier = match expected_extension.as_str() {
299 ".js" => Some("ecmascript"),
300 ".css" => Some("css"),
301 _ => None,
302 };
303
304 let mut hasher = Xxh3Hash64Hasher::new();
305 let mut has_hash = false;
306 let AssetIdent {
307 path: _,
308 query,
309 fragment,
310 assets,
311 modifiers,
312 parts,
313 layer,
314 content_type,
315 } = self;
316 if !query.is_empty() {
317 0_u8.deterministic_hash(&mut hasher);
318 query.deterministic_hash(&mut hasher);
319 has_hash = true;
320 }
321 if !fragment.is_empty() {
322 1_u8.deterministic_hash(&mut hasher);
323 fragment.deterministic_hash(&mut hasher);
324 has_hash = true;
325 }
326 for (key, ident) in assets.iter() {
327 2_u8.deterministic_hash(&mut hasher);
328 key.deterministic_hash(&mut hasher);
329 ident.to_string().await?.deterministic_hash(&mut hasher);
330 has_hash = true;
331 }
332 for modifier in modifiers.iter() {
333 if let Some(default_modifier) = default_modifier
334 && *modifier == default_modifier
335 {
336 continue;
337 }
338 3_u8.deterministic_hash(&mut hasher);
339 modifier.deterministic_hash(&mut hasher);
340 has_hash = true;
341 }
342 for part in parts.iter() {
343 4_u8.deterministic_hash(&mut hasher);
344 match part {
345 ModulePart::Evaluation => {
346 1_u8.deterministic_hash(&mut hasher);
347 }
348 ModulePart::Export(export) => {
349 2_u8.deterministic_hash(&mut hasher);
350 export.deterministic_hash(&mut hasher);
351 }
352 ModulePart::RenamedExport {
353 original_export,
354 export,
355 } => {
356 3_u8.deterministic_hash(&mut hasher);
357 original_export.deterministic_hash(&mut hasher);
358 export.deterministic_hash(&mut hasher);
359 }
360 ModulePart::RenamedNamespace { export } => {
361 4_u8.deterministic_hash(&mut hasher);
362 export.deterministic_hash(&mut hasher);
363 }
364 ModulePart::Internal(id) => {
365 5_u8.deterministic_hash(&mut hasher);
366 id.deterministic_hash(&mut hasher);
367 }
368 ModulePart::Locals => {
369 6_u8.deterministic_hash(&mut hasher);
370 }
371 ModulePart::Exports => {
372 7_u8.deterministic_hash(&mut hasher);
373 }
374 ModulePart::Facade => {
375 8_u8.deterministic_hash(&mut hasher);
376 }
377 }
378
379 has_hash = true;
380 }
381 if let Some(layer) = layer {
382 5_u8.deterministic_hash(&mut hasher);
383 layer.deterministic_hash(&mut hasher);
384 has_hash = true;
385 }
386 if let Some(content_type) = content_type {
387 6_u8.deterministic_hash(&mut hasher);
388 content_type.deterministic_hash(&mut hasher);
389 has_hash = true;
390 }
391
392 if has_hash {
393 let hash = encode_hex(hasher.finish());
394 let truncated_hash = &hash[..8];
395 write!(name, "_{truncated_hash}")?;
396 }
397
398 let mut i = 0;
401 static NODE_MODULES: &str = "_node_modules_";
402 if let Some(j) = name.rfind(NODE_MODULES) {
403 i = j + NODE_MODULES.len();
404 }
405 const MAX_FILENAME: usize = 80;
406 if name.len() - i > MAX_FILENAME {
407 i = name.len() - MAX_FILENAME;
408 if let Some(j) = name[i..].find('_')
409 && j < 20
410 {
411 i += j + 1;
412 }
413 }
414 if i > 0 {
415 let hash = encode_hex(hash_xxh3_hash64(&name.as_bytes()[..i]));
416 let truncated_hash = &hash[..5];
417 name = format!("{}_{}", truncated_hash, &name[i..]);
418 }
419 if !removed_extension {
423 name += "._";
424 }
425 name += &expected_extension;
426 Ok(Vc::cell(name.into()))
427 }
428}
429
430fn clean_separators(s: &str) -> String {
431 static SEPARATOR_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[/#?]").unwrap());
432 SEPARATOR_REGEX.replace_all(s, "_").to_string()
433}
434
435fn clean_additional_extensions(s: &str) -> String {
436 s.replace('.', "_")
437}