turbopack_core/
ident.rs

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