1use std::{fmt::Write, sync::LazyLock};
2
3use anyhow::Result;
4use bincode::{Decode, Encode};
5use regex::Regex;
6use turbo_rcstr::RcStr;
7use turbo_tasks::{ReadRef, ResolvedVc, ValueToString, ValueToStringRef, Vc, turbofmt};
8use turbo_tasks_fs::FileSystemPath;
9use turbo_tasks_hash::{DeterministicHash, Xxh3Hash64Hasher, encode_base38, hash_xxh3_hash64};
10
11use crate::resolve::ModulePart;
12
13#[turbo_tasks::task_input]
15#[derive(Clone, Hash, Debug, DeterministicHash, Eq, PartialEq, Encode, Decode)]
16pub struct Layer {
17 name: RcStr,
18 user_friendly_name: Option<RcStr>,
19}
20
21impl Layer {
22 pub fn new(name: RcStr) -> Self {
23 debug_assert!(!name.is_empty());
24 Self {
25 name,
26 user_friendly_name: None,
27 }
28 }
29 pub fn new_with_user_friendly_name(name: RcStr, user_friendly_name: RcStr) -> Self {
30 debug_assert!(!name.is_empty());
31 debug_assert!(!user_friendly_name.is_empty());
32 Self {
33 name,
34 user_friendly_name: Some(user_friendly_name),
35 }
36 }
37
38 pub fn user_friendly_name(&self) -> &RcStr {
40 self.user_friendly_name.as_ref().unwrap_or(&self.name)
41 }
42
43 pub fn name(&self) -> &RcStr {
44 &self.name
45 }
46}
47
48#[turbo_tasks::value(task_input)]
49#[derive(Clone, Debug, Hash)]
50pub struct AssetIdent {
51 pub path: FileSystemPath,
53 pub query: RcStr,
56 pub fragment: RcStr,
59 pub assets: Vec<(RcStr, ResolvedVc<AssetIdent>)>,
61 pub modifiers: Vec<RcStr>,
63 pub parts: Vec<ModulePart>,
65 pub layer: Option<Layer>,
67 pub content_type: Option<RcStr>,
69}
70
71impl AssetIdent {
72 pub fn from_path(path: FileSystemPath) -> Self {
77 AssetIdent {
78 path,
79 query: RcStr::default(),
80 fragment: RcStr::default(),
81 assets: Vec::new(),
82 modifiers: Vec::new(),
83 parts: Vec::new(),
84 layer: None,
85 content_type: None,
86 }
87 }
88
89 pub fn into_vc(self) -> Vc<Self> {
91 AssetIdent::new_inner(ReadRef::new_owned(self))
93 }
94
95 pub fn with_query(mut self, query: RcStr) -> Self {
96 self.query = query;
97 self
98 }
99
100 pub fn with_fragment(mut self, fragment: RcStr) -> Self {
101 self.fragment = fragment;
102 self
103 }
104
105 pub fn with_modifier(mut self, modifier: RcStr) -> Self {
106 debug_assert!(!modifier.is_empty(), "modifiers cannot be empty.");
107 self.modifiers.push(modifier);
108 self
109 }
110
111 pub fn with_part(mut self, part: ModulePart) -> Self {
112 self.parts.push(part);
113 self
114 }
115
116 pub fn with_path(mut self, path: FileSystemPath) -> Self {
117 self.path = path;
118 self
119 }
120
121 pub fn with_layer(mut self, layer: Layer) -> Self {
122 self.layer = Some(layer);
123 self
124 }
125
126 pub fn with_content_type(mut self, content_type: RcStr) -> Self {
127 self.content_type = Some(content_type);
128 self
129 }
130
131 pub fn with_asset(mut self, key: RcStr, asset: ResolvedVc<AssetIdent>) -> Self {
132 self.assets.push((key, asset));
133 self
134 }
135
136 pub fn rename_as(mut self, pattern: &str) -> Self {
137 self.path = FileSystemPath::new_normalized_unchecked(
138 self.path.fs,
139 pattern.replace('*', &self.path.path).into(),
140 );
141 self.content_type = None;
142 self
143 }
144}
145
146#[turbo_tasks::value_impl]
147impl AssetIdent {
148 #[turbo_tasks::function]
149 fn new_inner(ident: ReadRef<AssetIdent>) -> Vc<Self> {
150 debug_assert!(
151 ident.query.is_empty() || ident.query.starts_with("?"),
152 "query should be empty or start with a `?`"
153 );
154 debug_assert!(
155 ident.fragment.is_empty() || ident.fragment.starts_with("#"),
156 "query should be empty or start with a `?`"
157 );
158 ReadRef::cell(ident)
159 }
160
161 #[turbo_tasks::function]
166 pub async fn output_name(
167 &self,
168 context_path: FileSystemPath,
169 prefix: Option<RcStr>,
170 expected_extension: RcStr,
171 ) -> Result<Vc<RcStr>> {
172 debug_assert!(
173 expected_extension.starts_with("."),
174 "the extension should include the leading '.', got '{expected_extension}'"
175 );
176 let path = &self.path;
181 let mut name = if let Some(inner) = context_path.get_path_to(path) {
182 escape_file_path(inner)
183 } else {
184 escape_file_path(&self.path.to_string_ref().await?)
185 };
186 let removed_extension = name.ends_with(&*expected_extension);
187 if removed_extension {
188 name.truncate(name.len() - expected_extension.len());
189 }
190 let mut name = clean_additional_extensions(&name);
194 if let Some(prefix) = prefix {
195 name = format!("{prefix}-{name}");
196 }
197
198 let default_modifier = match expected_extension.as_str() {
199 ".js" => Some("ecmascript"),
200 ".css" => Some("css"),
201 _ => None,
202 };
203
204 let mut hasher = Xxh3Hash64Hasher::new();
205 let mut has_hash = false;
206 let AssetIdent {
207 path: _,
208 query,
209 fragment,
210 assets,
211 modifiers,
212 parts,
213 layer,
214 content_type,
215 } = self;
216 if !query.is_empty() {
217 0_u8.deterministic_hash(&mut hasher);
218 query.deterministic_hash(&mut hasher);
219 has_hash = true;
220 }
221 if !fragment.is_empty() {
222 1_u8.deterministic_hash(&mut hasher);
223 fragment.deterministic_hash(&mut hasher);
224 has_hash = true;
225 }
226 for (key, ident) in assets.iter() {
227 2_u8.deterministic_hash(&mut hasher);
228 key.deterministic_hash(&mut hasher);
229 ident.to_string().await?.deterministic_hash(&mut hasher);
230 has_hash = true;
231 }
232 for modifier in modifiers.iter() {
233 if let Some(default_modifier) = default_modifier
234 && *modifier == default_modifier
235 {
236 continue;
237 }
238 3_u8.deterministic_hash(&mut hasher);
239 modifier.deterministic_hash(&mut hasher);
240 has_hash = true;
241 }
242 for part in parts.iter() {
243 4_u8.deterministic_hash(&mut hasher);
244 match part {
245 ModulePart::Evaluation => {
246 1_u8.deterministic_hash(&mut hasher);
247 }
248 ModulePart::Export(export) => {
249 2_u8.deterministic_hash(&mut hasher);
250 export.deterministic_hash(&mut hasher);
251 }
252 ModulePart::PartialExport { export, member } => {
253 3_u8.deterministic_hash(&mut hasher);
254 export.deterministic_hash(&mut hasher);
255 member.deterministic_hash(&mut hasher);
256 }
257 ModulePart::RenamedExport {
258 original_export,
259 export,
260 } => {
261 4_u8.deterministic_hash(&mut hasher);
262 original_export.deterministic_hash(&mut hasher);
263 export.deterministic_hash(&mut hasher);
264 }
265 ModulePart::RenamedNamespace { export } => {
266 5_u8.deterministic_hash(&mut hasher);
267 export.deterministic_hash(&mut hasher);
268 }
269 ModulePart::RenamedPartialNamespace { export, member } => {
270 6_u8.deterministic_hash(&mut hasher);
271 export.deterministic_hash(&mut hasher);
272 member.deterministic_hash(&mut hasher);
273 }
274 ModulePart::Internal(id) => {
275 7_u8.deterministic_hash(&mut hasher);
276 id.deterministic_hash(&mut hasher);
277 }
278 ModulePart::Locals => {
279 8_u8.deterministic_hash(&mut hasher);
280 }
281 ModulePart::Exports => {
282 9_u8.deterministic_hash(&mut hasher);
283 }
284 ModulePart::Facade => {
285 10_u8.deterministic_hash(&mut hasher);
286 }
287 }
288
289 has_hash = true;
290 }
291 if let Some(layer) = layer {
292 5_u8.deterministic_hash(&mut hasher);
293 layer.deterministic_hash(&mut hasher);
294 has_hash = true;
295 }
296 if let Some(content_type) = content_type {
297 6_u8.deterministic_hash(&mut hasher);
298 content_type.deterministic_hash(&mut hasher);
299 has_hash = true;
300 }
301
302 if has_hash {
303 let hash = encode_base38(hasher.finish());
304 write!(name, "_{hash}")?;
313 }
314
315 let mut i = 0;
318 static NODE_MODULES: &str = "_node_modules_";
319 if let Some(j) = name.rfind(NODE_MODULES) {
320 i = j + NODE_MODULES.len();
321 }
322 const MAX_FILENAME: usize = 80;
323 if name.len() - i > MAX_FILENAME {
324 i = name.len() - MAX_FILENAME;
325 if let Some(j) = name[i..].find('_')
326 && j < 20
327 {
328 i += j + 1;
329 }
330 }
331 if i > 0 {
332 let hash = encode_base38(hash_xxh3_hash64(&name.as_bytes()[..i]));
333 let truncated_hash = &hash[..4];
335 name = format!("{}_{}", truncated_hash, &name[i..]);
336 }
337 if !removed_extension {
341 name += "._";
342 }
343 name += &expected_extension;
344 Ok(Vc::cell(name.into()))
345 }
346}
347
348#[turbo_tasks::value_impl]
349impl ValueToString for AssetIdent {
350 #[turbo_tasks::function]
351 async fn to_string(&self) -> Result<Vc<RcStr>> {
352 let mut s = turbofmt!("{}{}{}", self.path, self.query, self.fragment)
355 .await?
356 .into_owned();
357
358 if !self.assets.is_empty() {
359 s.push_str(" {");
360
361 for (i, (key, asset)) in self.assets.iter().enumerate() {
362 if i > 0 {
363 s.push(',');
364 }
365
366 let asset_str = asset.to_string().await?;
367 write!(s, " {key} => {asset_str:?}")?;
368 }
369
370 s.push_str(" }");
371 }
372
373 if let Some(layer) = &self.layer {
374 write!(s, " [{}]", layer.name)?;
375 }
376
377 if !self.modifiers.is_empty() {
378 s.push_str(" (");
379
380 for (i, modifier) in self.modifiers.iter().enumerate() {
381 if i > 0 {
382 s.push_str(", ");
383 }
384
385 s.push_str(modifier);
386 }
387
388 s.push(')');
389 }
390
391 if let Some(content_type) = &self.content_type {
392 write!(s, " <{content_type}>")?;
393 }
394
395 if !self.parts.is_empty() {
396 for part in self.parts.iter() {
397 if !matches!(part, ModulePart::Facade) {
398 write!(s, " <{part}>")?;
401 }
402 }
403 }
404
405 Ok(Vc::cell(s.into()))
406 }
407}
408
409fn escape_file_path(s: &str) -> String {
410 static SEPARATOR_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[/#?:]").unwrap());
411 SEPARATOR_REGEX.replace_all(s, "_").to_string()
412}
413
414fn clean_additional_extensions(s: &str) -> String {
415 s.replace('.', "_")
416}
417
418#[cfg(test)]
419pub mod tests {
420 use turbo_rcstr::{RcStr, rcstr};
421 use turbo_tasks::Vc;
422 use turbo_tasks_backend::{BackendOptions, TurboTasksBackend, noop_backing_storage};
423 use turbo_tasks_fs::{FileSystem, VirtualFileSystem};
424
425 use crate::ident::AssetIdent;
426
427 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
428 async fn test_output_name_escaping() {
429 let tt = turbo_tasks::TurboTasks::new(TurboTasksBackend::new(
430 BackendOptions::default(),
431 noop_backing_storage(),
432 ));
433 tt.run_once(async move {
434 #[turbo_tasks::function(operation, root)]
435 async fn output_name_operation() -> anyhow::Result<Vc<RcStr>> {
436 let fs = VirtualFileSystem::new_with_name(rcstr!("test"));
437 let root = fs.root().owned().await?;
438
439 let asset_ident = AssetIdent::from_path(root.join("a:b?c#d.js")?).into_vc();
440 let output_name = asset_ident
441 .output_name(root, Some(rcstr!("prefix")), rcstr!(".js"))
442 .await?;
443 Ok(Vc::cell((*output_name).clone()))
444 }
445
446 let output_name = output_name_operation().read_strongly_consistent().await?;
447 assert_eq!(&*output_name, "prefix-a_b_c_d.js");
448
449 Ok(())
450 })
451 .await
452 .unwrap();
453 }
454}