turbopack_core/
ident.rs

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::{
9    NonLocalValue, ReadRef, ResolvedVc, TaskInput, ValueToString, Vc, trace::TraceRawVcs,
10};
11use turbo_tasks_fs::FileSystemPath;
12use turbo_tasks_hash::{DeterministicHash, Xxh3Hash64Hasher, encode_hex, hash_xxh3_hash64};
13
14use crate::resolve::ModulePart;
15
16/// A layer identifies a distinct part of the module graph.
17#[derive(
18    Clone,
19    TaskInput,
20    Hash,
21    Debug,
22    DeterministicHash,
23    Eq,
24    PartialEq,
25    TraceRawVcs,
26    Serialize,
27    Deserialize,
28    NonLocalValue,
29)]
30pub struct Layer {
31    name: RcStr,
32    user_friendly_name: Option<RcStr>,
33}
34
35impl Layer {
36    pub fn new(name: RcStr) -> Self {
37        debug_assert!(!name.is_empty());
38        Self {
39            name,
40            user_friendly_name: None,
41        }
42    }
43    pub fn new_with_user_friendly_name(name: RcStr, user_friendly_name: RcStr) -> Self {
44        debug_assert!(!name.is_empty());
45        debug_assert!(!user_friendly_name.is_empty());
46        Self {
47            name,
48            user_friendly_name: Some(user_friendly_name),
49        }
50    }
51
52    /// Returns a user friendly name for this layer
53    pub fn user_friendly_name(&self) -> &RcStr {
54        self.user_friendly_name.as_ref().unwrap_or(&self.name)
55    }
56
57    pub fn name(&self) -> &RcStr {
58        &self.name
59    }
60}
61
62#[turbo_tasks::value]
63#[derive(Clone, Debug, Hash, TaskInput)]
64pub struct AssetIdent {
65    /// The primary path of the asset
66    pub path: FileSystemPath,
67    /// The query string of the asset this is either the empty string or a query string that starts
68    /// with a `?` (e.g. `?foo=bar`)
69    pub query: RcStr,
70    /// The fragment of the asset, this is either the empty string or a fragment string that starts
71    /// with a `#` (e.g. `#foo`)
72    pub fragment: RcStr,
73    /// The assets that are nested in this asset
74    pub assets: Vec<(RcStr, ResolvedVc<AssetIdent>)>,
75    /// The modifiers of this asset (e.g. `client chunks`)
76    pub modifiers: Vec<RcStr>,
77    /// The parts of the asset that are (ECMAScript) modules
78    pub parts: Vec<ModulePart>,
79    /// The asset layer the asset was created from.
80    pub layer: Option<Layer>,
81    /// The MIME content type, if this asset was created from a data URL.
82    pub content_type: Option<RcStr>,
83}
84
85impl AssetIdent {
86    pub fn new(ident: AssetIdent) -> Vc<Self> {
87        AssetIdent::new_inner(ReadRef::new_owned(ident))
88    }
89
90    pub fn add_modifier(&mut self, modifier: RcStr) {
91        debug_assert!(!modifier.is_empty(), "modifiers cannot be empty.");
92        self.modifiers.push(modifier);
93    }
94
95    pub fn add_asset(&mut self, key: RcStr, asset: ResolvedVc<AssetIdent>) {
96        self.assets.push((key, asset));
97    }
98
99    pub async fn rename_as_ref(&mut self, pattern: &str) -> Result<()> {
100        let root = self.path.root().await?;
101        let path = self.path.clone();
102        self.path = root.join(&pattern.replace('*', &path.path))?;
103        Ok(())
104    }
105}
106
107#[turbo_tasks::value_impl]
108impl AssetIdent {
109    #[turbo_tasks::function]
110    fn new_inner(ident: ReadRef<AssetIdent>) -> Vc<Self> {
111        debug_assert!(
112            ident.query.is_empty() || ident.query.starts_with("?"),
113            "query should be empty or start with a `?`"
114        );
115        debug_assert!(
116            ident.fragment.is_empty() || ident.fragment.starts_with("#"),
117            "query should be empty or start with a `?`"
118        );
119        ReadRef::cell(ident)
120    }
121
122    /// Creates an [AssetIdent] from a [FileSystemPath]
123    #[turbo_tasks::function]
124    pub fn from_path(path: FileSystemPath) -> Vc<Self> {
125        Self::new(AssetIdent {
126            path,
127            query: RcStr::default(),
128            fragment: RcStr::default(),
129            assets: Vec::new(),
130            modifiers: Vec::new(),
131            parts: Vec::new(),
132            layer: None,
133            content_type: None,
134        })
135    }
136
137    #[turbo_tasks::function]
138    pub fn with_query(&self, query: RcStr) -> Vc<Self> {
139        let mut this = self.clone();
140        this.query = query;
141        Self::new(this)
142    }
143
144    #[turbo_tasks::function]
145    pub fn with_fragment(&self, fragment: RcStr) -> Vc<Self> {
146        let mut this = self.clone();
147        this.fragment = fragment;
148        Self::new(this)
149    }
150
151    #[turbo_tasks::function]
152    pub fn with_modifier(&self, modifier: RcStr) -> Vc<Self> {
153        let mut this = self.clone();
154        this.add_modifier(modifier);
155        Self::new(this)
156    }
157
158    #[turbo_tasks::function]
159    pub fn with_part(&self, part: ModulePart) -> Vc<Self> {
160        let mut this = self.clone();
161        this.parts.push(part);
162        Self::new(this)
163    }
164
165    #[turbo_tasks::function]
166    pub fn with_path(&self, path: FileSystemPath) -> Vc<Self> {
167        let mut this = self.clone();
168        this.path = path;
169        Self::new(this)
170    }
171
172    #[turbo_tasks::function]
173    pub fn with_layer(&self, layer: Layer) -> Vc<Self> {
174        let mut this = self.clone();
175        this.layer = Some(layer);
176        Self::new(this)
177    }
178
179    #[turbo_tasks::function]
180    pub fn with_content_type(&self, content_type: RcStr) -> Vc<Self> {
181        let mut this = self.clone();
182        this.content_type = Some(content_type);
183        Self::new(this)
184    }
185
186    #[turbo_tasks::function]
187    pub fn with_asset(&self, key: RcStr, asset: ResolvedVc<AssetIdent>) -> Vc<Self> {
188        let mut this = self.clone();
189        this.add_asset(key, asset);
190        Self::new(this)
191    }
192
193    #[turbo_tasks::function]
194    pub async fn rename_as(&self, pattern: RcStr) -> Result<Vc<Self>> {
195        let mut this = self.clone();
196        this.rename_as_ref(&pattern).await?;
197        Ok(Self::new(this))
198    }
199
200    #[turbo_tasks::function]
201    pub fn path(&self) -> Vc<FileSystemPath> {
202        self.path.clone().cell()
203    }
204
205    /// Computes a unique output asset name for the given asset identifier.
206    /// TODO(alexkirsz) This is `turbopack-browser` specific, as
207    /// `turbopack-nodejs` would use a content hash instead. But for now
208    /// both are using the same name generation logic.
209    #[turbo_tasks::function]
210    pub async fn output_name(
211        &self,
212        context_path: FileSystemPath,
213        prefix: Option<RcStr>,
214        expected_extension: RcStr,
215    ) -> Result<Vc<RcStr>> {
216        debug_assert!(
217            expected_extension.starts_with("."),
218            "the extension should include the leading '.', got '{expected_extension}'"
219        );
220        // TODO(PACK-2140): restrict character set to A–Za–z0–9-_.~'()
221        // to be compatible with all operating systems + URLs.
222
223        // For clippy -- This explicit deref is necessary
224        let path = &self.path;
225        let mut name = if let Some(inner) = context_path.get_path_to(path) {
226            clean_separators(inner)
227        } else {
228            clean_separators(&self.path.value_to_string().await?)
229        };
230        let removed_extension = name.ends_with(&*expected_extension);
231        if removed_extension {
232            name.truncate(name.len() - expected_extension.len());
233        }
234        // This step ensures that leading dots are not preserved in file names. This is
235        // important as some file servers do not serve files with leading dots (e.g.
236        // Next.js).
237        let mut name = clean_additional_extensions(&name);
238        if let Some(prefix) = prefix {
239            name = format!("{prefix}-{name}");
240        }
241
242        let default_modifier = match expected_extension.as_str() {
243            ".js" => Some("ecmascript"),
244            ".css" => Some("css"),
245            _ => None,
246        };
247
248        let mut hasher = Xxh3Hash64Hasher::new();
249        let mut has_hash = false;
250        let AssetIdent {
251            path: _,
252            query,
253            fragment,
254            assets,
255            modifiers,
256            parts,
257            layer,
258            content_type,
259        } = self;
260        if !query.is_empty() {
261            0_u8.deterministic_hash(&mut hasher);
262            query.deterministic_hash(&mut hasher);
263            has_hash = true;
264        }
265        if !fragment.is_empty() {
266            1_u8.deterministic_hash(&mut hasher);
267            fragment.deterministic_hash(&mut hasher);
268            has_hash = true;
269        }
270        for (key, ident) in assets.iter() {
271            2_u8.deterministic_hash(&mut hasher);
272            key.deterministic_hash(&mut hasher);
273            ident.to_string().await?.deterministic_hash(&mut hasher);
274            has_hash = true;
275        }
276        for modifier in modifiers.iter() {
277            if let Some(default_modifier) = default_modifier
278                && *modifier == default_modifier
279            {
280                continue;
281            }
282            3_u8.deterministic_hash(&mut hasher);
283            modifier.deterministic_hash(&mut hasher);
284            has_hash = true;
285        }
286        for part in parts.iter() {
287            4_u8.deterministic_hash(&mut hasher);
288            match part {
289                ModulePart::Evaluation => {
290                    1_u8.deterministic_hash(&mut hasher);
291                }
292                ModulePart::Export(export) => {
293                    2_u8.deterministic_hash(&mut hasher);
294                    export.deterministic_hash(&mut hasher);
295                }
296                ModulePart::RenamedExport {
297                    original_export,
298                    export,
299                } => {
300                    3_u8.deterministic_hash(&mut hasher);
301                    original_export.deterministic_hash(&mut hasher);
302                    export.deterministic_hash(&mut hasher);
303                }
304                ModulePart::RenamedNamespace { export } => {
305                    4_u8.deterministic_hash(&mut hasher);
306                    export.deterministic_hash(&mut hasher);
307                }
308                ModulePart::Internal(id) => {
309                    5_u8.deterministic_hash(&mut hasher);
310                    id.deterministic_hash(&mut hasher);
311                }
312                ModulePart::Locals => {
313                    6_u8.deterministic_hash(&mut hasher);
314                }
315                ModulePart::Exports => {
316                    7_u8.deterministic_hash(&mut hasher);
317                }
318                ModulePart::Facade => {
319                    8_u8.deterministic_hash(&mut hasher);
320                }
321            }
322
323            has_hash = true;
324        }
325        if let Some(layer) = layer {
326            5_u8.deterministic_hash(&mut hasher);
327            layer.deterministic_hash(&mut hasher);
328            has_hash = true;
329        }
330        if let Some(content_type) = content_type {
331            6_u8.deterministic_hash(&mut hasher);
332            content_type.deterministic_hash(&mut hasher);
333            has_hash = true;
334        }
335
336        if has_hash {
337            let hash = encode_hex(hasher.finish());
338            let truncated_hash = &hash[..8];
339            write!(name, "_{truncated_hash}")?;
340        }
341
342        // Location in "path" where hashed and named parts are split.
343        // Everything before i is hashed and after i named.
344        let mut i = 0;
345        static NODE_MODULES: &str = "_node_modules_";
346        if let Some(j) = name.rfind(NODE_MODULES) {
347            i = j + NODE_MODULES.len();
348        }
349        const MAX_FILENAME: usize = 80;
350        if name.len() - i > MAX_FILENAME {
351            i = name.len() - MAX_FILENAME;
352            if let Some(j) = name[i..].find('_')
353                && j < 20
354            {
355                i += j + 1;
356            }
357        }
358        if i > 0 {
359            let hash = encode_hex(hash_xxh3_hash64(&name.as_bytes()[..i]));
360            let truncated_hash = &hash[..5];
361            name = format!("{}_{}", truncated_hash, &name[i..]);
362        }
363        // We need to make sure that `.json` and `.json.js` doesn't end up with the same
364        // name. So when we add an extra extension when want to mark that with a "._"
365        // suffix.
366        if !removed_extension {
367            name += "._";
368        }
369        name += &expected_extension;
370        Ok(Vc::cell(name.into()))
371    }
372}
373
374#[turbo_tasks::value_impl]
375impl ValueToString for AssetIdent {
376    #[turbo_tasks::function]
377    async fn to_string(&self) -> Result<Vc<RcStr>> {
378        let mut s = self.path.value_to_string().owned().await?.into_owned();
379
380        // The query string is either empty or non-empty starting with `?` so we can just concat
381        s.push_str(&self.query);
382        // ditto for fragment
383        s.push_str(&self.fragment);
384
385        if !self.assets.is_empty() {
386            s.push_str(" {");
387
388            for (i, (key, asset)) in self.assets.iter().enumerate() {
389                if i > 0 {
390                    s.push(',');
391                }
392
393                let asset_str = asset.to_string().await?;
394                write!(s, " {key} => {asset_str:?}")?;
395            }
396
397            s.push_str(" }");
398        }
399
400        if let Some(layer) = &self.layer {
401            write!(s, " [{}]", layer.name)?;
402        }
403
404        if !self.modifiers.is_empty() {
405            s.push_str(" (");
406
407            for (i, modifier) in self.modifiers.iter().enumerate() {
408                if i > 0 {
409                    s.push_str(", ");
410                }
411
412                s.push_str(modifier);
413            }
414
415            s.push(')');
416        }
417
418        if let Some(content_type) = &self.content_type {
419            write!(s, " <{content_type}>")?;
420        }
421
422        if !self.parts.is_empty() {
423            for part in self.parts.iter() {
424                if !matches!(part, ModulePart::Facade) {
425                    // facade is not included in ident as switching between facade and non-facade
426                    // shouldn't change the ident
427                    write!(s, " <{part}>")?;
428                }
429            }
430        }
431
432        Ok(Vc::cell(s.into()))
433    }
434}
435
436fn clean_separators(s: &str) -> String {
437    static SEPARATOR_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"[/#?]").unwrap());
438    SEPARATOR_REGEX.replace_all(s, "_").to_string()
439}
440
441fn clean_additional_extensions(s: &str) -> String {
442    s.replace('.', "_")
443}