turbopack_core/resolve/
remap.rs

1use std::{collections::BTreeMap, fmt::Display, ops::Deref};
2
3use anyhow::{Result, bail};
4use rustc_hash::FxHashMap;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7use turbo_rcstr::RcStr;
8use turbo_tasks::FxIndexMap;
9
10use super::{
11    alias_map::{AliasMap, AliasMapIter, AliasPattern, AliasTemplate},
12    options::ConditionValue,
13    pattern::Pattern,
14};
15
16/// A small helper type to differentiate parsing exports and imports fields.
17#[derive(Copy, Clone)]
18enum ExportImport {
19    Export,
20    Import,
21}
22
23impl Display for ExportImport {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        match self {
26            Self::Export => f.write_str("export"),
27            Self::Import => f.write_str("import"),
28        }
29    }
30}
31
32/// The result an "exports"/"imports" field describes. Can represent multiple
33/// alternatives, conditional result, ignored result (null mapping) and a plain
34/// result.
35#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
36pub enum SubpathValue {
37    /// Alternative subpaths, defined with `"path": ["other1", "other2"]`,
38    /// allows for specifying multiple possible remappings to be tried. This
39    /// may be that conditions didn't match, or that a particular path
40    /// wasn't found.
41    Alternatives(Vec<SubpathValue>),
42
43    /// Conditional subpaths, defined with `"path": { "condition": "other"}`,
44    /// allow remapping based on certain predefined conditions. Eg, if using
45    /// ESM import syntax, the `import` condition allows you to remap to a
46    /// file that uses ESM syntax.
47    /// Node defines several conditions in https://nodejs.org/api/packages.html#conditional-exports
48    /// TODO: Should this use an enum of predefined keys?
49    Conditional(Vec<(RcStr, SubpathValue)>),
50
51    /// A result subpath, defined with `"path": "other"`, remaps imports of
52    /// `path` to `other`.
53    Result(RcStr),
54
55    /// An excluded subpath, defined with `"path": null`, prevents importing
56    /// this subpath.
57    Excluded,
58}
59
60/// A `SubpathValue` that was applied to a pattern. See `SubpathValue` for
61/// more details on the variants.
62#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
63pub enum ReplacedSubpathValue {
64    Alternatives(Vec<ReplacedSubpathValue>),
65    Conditional(Vec<(RcStr, ReplacedSubpathValue)>),
66    Result(Pattern),
67    Excluded,
68}
69
70impl AliasTemplate for SubpathValue {
71    type Output<'a>
72        = Result<ReplacedSubpathValue>
73    where
74        Self: 'a;
75
76    fn convert(&self) -> Result<ReplacedSubpathValue> {
77        Ok(match self {
78            SubpathValue::Alternatives(list) => ReplacedSubpathValue::Alternatives(
79                list.iter()
80                    .map(|value: &SubpathValue| value.convert())
81                    .collect::<Result<Vec<_>>>()?,
82            ),
83            SubpathValue::Conditional(list) => ReplacedSubpathValue::Conditional(
84                list.iter()
85                    .map(|(condition, value)| Ok((condition.clone(), value.convert()?)))
86                    .collect::<Result<Vec<_>>>()?,
87            ),
88            SubpathValue::Result(value) => ReplacedSubpathValue::Result(value.clone().into()),
89            SubpathValue::Excluded => ReplacedSubpathValue::Excluded,
90        })
91    }
92
93    fn replace(&self, capture: &Pattern) -> Result<ReplacedSubpathValue> {
94        Ok(match self {
95            SubpathValue::Alternatives(list) => ReplacedSubpathValue::Alternatives(
96                list.iter()
97                    .map(|value: &SubpathValue| value.replace(capture))
98                    .collect::<Result<Vec<_>>>()?,
99            ),
100            SubpathValue::Conditional(list) => ReplacedSubpathValue::Conditional(
101                list.iter()
102                    .map(|(condition, value)| Ok((condition.clone(), value.replace(capture)?)))
103                    .collect::<Result<Vec<_>>>()?,
104            ),
105            SubpathValue::Result(value) => {
106                ReplacedSubpathValue::Result(capture.spread_into_star(value))
107            }
108            SubpathValue::Excluded => ReplacedSubpathValue::Excluded,
109        })
110    }
111}
112
113impl SubpathValue {
114    /// Returns an iterator over all leaf results.
115    fn results_mut(&mut self) -> ResultsIterMut<'_> {
116        ResultsIterMut { stack: vec![self] }
117    }
118
119    /// Walks the [SubpathValue] and adds results to the `target` vector. It
120    /// uses the `conditions` to skip or enter conditional results.
121    /// The state of conditions is stored within `condition_overrides`, which is
122    /// also exposed to the consumer.
123    pub fn add_results<'a>(
124        &'a self,
125        conditions: &BTreeMap<RcStr, ConditionValue>,
126        unspecified_condition: &ConditionValue,
127        condition_overrides: &mut FxHashMap<&'a str, ConditionValue>,
128        target: &mut Vec<(&'a str, Vec<(&'a str, bool)>)>,
129    ) -> bool {
130        match self {
131            SubpathValue::Alternatives(list) => {
132                for value in list {
133                    if value.add_results(
134                        conditions,
135                        unspecified_condition,
136                        condition_overrides,
137                        target,
138                    ) {
139                        return true;
140                    }
141                }
142                false
143            }
144            SubpathValue::Conditional(list) => {
145                for (condition, value) in list {
146                    let condition_value = if condition == "default" {
147                        &ConditionValue::Set
148                    } else {
149                        condition_overrides
150                            .get(condition.as_str())
151                            .or_else(|| conditions.get(condition))
152                            .unwrap_or(unspecified_condition)
153                    };
154                    match condition_value {
155                        ConditionValue::Set => {
156                            if value.add_results(
157                                conditions,
158                                unspecified_condition,
159                                condition_overrides,
160                                target,
161                            ) {
162                                return true;
163                            }
164                        }
165                        ConditionValue::Unset => {}
166                        ConditionValue::Unknown => {
167                            condition_overrides.insert(condition, ConditionValue::Set);
168                            if value.add_results(
169                                conditions,
170                                unspecified_condition,
171                                condition_overrides,
172                                target,
173                            ) {
174                                condition_overrides.insert(condition, ConditionValue::Unset);
175                            } else {
176                                condition_overrides.remove(condition.as_str());
177                            }
178                        }
179                    }
180                }
181                false
182            }
183            SubpathValue::Result(r) => {
184                target.push((
185                    r,
186                    condition_overrides
187                        .iter()
188                        .filter_map(|(k, v)| match v {
189                            ConditionValue::Set => Some((*k, true)),
190                            ConditionValue::Unset => Some((*k, false)),
191                            ConditionValue::Unknown => None,
192                        })
193                        .collect(),
194                ));
195                true
196            }
197            SubpathValue::Excluded => true,
198        }
199    }
200
201    fn try_new(value: &Value, ty: ExportImport) -> Result<Self> {
202        match value {
203            Value::Null => Ok(SubpathValue::Excluded),
204            Value::String(s) => Ok(SubpathValue::Result(s.as_str().into())),
205            Value::Number(_) => bail!("numeric values are invalid in {ty}s field entries"),
206            Value::Bool(_) => bail!("boolean values are invalid in {ty}s field entries"),
207            Value::Object(object) => Ok(SubpathValue::Conditional(
208                object
209                    .iter()
210                    .map(|(key, value)| {
211                        if key.starts_with('.') {
212                            bail!(
213                                "invalid key \"{}\" in an {ty} field conditions object. Did you \
214                                 mean to place this request at a higher level?",
215                                key
216                            );
217                        }
218
219                        Ok((key.as_str().into(), SubpathValue::try_new(value, ty)?))
220                    })
221                    .collect::<Result<Vec<_>>>()?,
222            )),
223            Value::Array(array) => Ok(SubpathValue::Alternatives(
224                array
225                    .iter()
226                    .map(|value| SubpathValue::try_new(value, ty))
227                    .collect::<Result<Vec<_>>>()?,
228            )),
229        }
230    }
231}
232
233impl ReplacedSubpathValue {
234    /// Walks the [ReplacedSubpathValue] and adds results to the `target`
235    /// vector. It uses the `conditions` to skip or enter conditional
236    /// results. The state of conditions is stored within
237    /// `condition_overrides`, which is also exposed to the consumer.
238    pub fn add_results<'a>(
239        &'a self,
240        conditions: &BTreeMap<RcStr, ConditionValue>,
241        unspecified_condition: &ConditionValue,
242        condition_overrides: &mut FxHashMap<&'a str, ConditionValue>,
243        target: &mut Vec<(&'a Pattern, Vec<(&'a str, bool)>)>,
244    ) -> bool {
245        match self {
246            ReplacedSubpathValue::Alternatives(list) => {
247                for value in list {
248                    if value.add_results(
249                        conditions,
250                        unspecified_condition,
251                        condition_overrides,
252                        target,
253                    ) {
254                        return true;
255                    }
256                }
257                false
258            }
259            ReplacedSubpathValue::Conditional(list) => {
260                for (condition, value) in list {
261                    let condition_value = if condition == "default" {
262                        &ConditionValue::Set
263                    } else {
264                        condition_overrides
265                            .get(condition.as_str())
266                            .or_else(|| conditions.get(condition))
267                            .unwrap_or(unspecified_condition)
268                    };
269                    match condition_value {
270                        ConditionValue::Set => {
271                            if value.add_results(
272                                conditions,
273                                unspecified_condition,
274                                condition_overrides,
275                                target,
276                            ) {
277                                return true;
278                            }
279                        }
280                        ConditionValue::Unset => {}
281                        ConditionValue::Unknown => {
282                            condition_overrides.insert(condition, ConditionValue::Set);
283                            if value.add_results(
284                                conditions,
285                                unspecified_condition,
286                                condition_overrides,
287                                target,
288                            ) {
289                                condition_overrides.insert(condition, ConditionValue::Unset);
290                            } else {
291                                condition_overrides.remove(condition.as_str());
292                            }
293                        }
294                    }
295                }
296                false
297            }
298            ReplacedSubpathValue::Result(r) => {
299                target.push((
300                    r,
301                    condition_overrides
302                        .iter()
303                        .filter_map(|(k, v)| match v {
304                            ConditionValue::Set => Some((*k, true)),
305                            ConditionValue::Unset => Some((*k, false)),
306                            ConditionValue::Unknown => None,
307                        })
308                        .collect(),
309                ));
310                true
311            }
312            ReplacedSubpathValue::Excluded => true,
313        }
314    }
315}
316
317struct ResultsIterMut<'a> {
318    stack: Vec<&'a mut SubpathValue>,
319}
320
321impl<'a> Iterator for ResultsIterMut<'a> {
322    type Item = &'a mut RcStr;
323
324    fn next(&mut self) -> Option<Self::Item> {
325        while let Some(value) = self.stack.pop() {
326            match value {
327                SubpathValue::Alternatives(list) => {
328                    for value in list {
329                        self.stack.push(value);
330                    }
331                }
332                SubpathValue::Conditional(list) => {
333                    for (_, value) in list {
334                        self.stack.push(value);
335                    }
336                }
337                SubpathValue::Result(r) => return Some(r),
338                SubpathValue::Excluded => {}
339            }
340        }
341        None
342    }
343}
344
345/// Content of an "exports" field in a package.json
346#[derive(PartialEq, Eq, Serialize, Deserialize)]
347pub struct ExportsField(AliasMap<SubpathValue>);
348
349impl TryFrom<&Value> for ExportsField {
350    type Error = anyhow::Error;
351
352    fn try_from(value: &Value) -> Result<Self> {
353        // The "exports" field can be an object, a string, or an array of strings.
354        // https://nodejs.org/api/packages.html#exports
355        let map = match value {
356            Value::Object(object) => {
357                let mut map = AliasMap::new();
358                // Conditional exports can also be defined at the top-level of the
359                // exports field, where they will apply to the package itself.
360                let mut conditions = vec![];
361
362                for (key, value) in object.iter() {
363                    // NOTE: Node.js does not allow conditional and non-conditional keys
364                    // to be mixed at the top-level, but we do.
365                    if key != "." && !key.starts_with("./") {
366                        conditions.push((key, value));
367                        continue;
368                    }
369
370                    let mut value = SubpathValue::try_new(value, ExportImport::Export)?;
371
372                    let pattern = if is_folder_shorthand(key) {
373                        expand_folder_shorthand(key, &mut value)?
374                    } else {
375                        AliasPattern::parse(key.as_str())
376                    };
377
378                    map.insert(pattern, value);
379                }
380
381                if !conditions.is_empty() {
382                    map.insert(
383                        AliasPattern::Exact(".".into()),
384                        SubpathValue::Conditional(
385                            conditions
386                                .into_iter()
387                                .map(|(key, value)| {
388                                    Ok((
389                                        key.as_str().into(),
390                                        SubpathValue::try_new(value, ExportImport::Export)?,
391                                    ))
392                                })
393                                .collect::<Result<Vec<_>>>()?,
394                        ),
395                    );
396                }
397
398                map
399            }
400            Value::String(string) => {
401                let mut map = AliasMap::new();
402                map.insert(
403                    AliasPattern::exact("."),
404                    SubpathValue::Result(string.as_str().into()),
405                );
406                map
407            }
408            Value::Array(array) => {
409                let mut map = AliasMap::new();
410                map.insert(
411                    AliasPattern::exact("."),
412                    // This allows for more complex patterns than the spec allows, since we accept
413                    // the following:
414                    // [{ "node": "./node.js", "default": "./index.js" }, "./index.js"]
415                    SubpathValue::Alternatives(
416                        array
417                            .iter()
418                            .map(|value| SubpathValue::try_new(value, ExportImport::Export))
419                            .collect::<Result<Vec<_>>>()?,
420                    ),
421                );
422                map
423            }
424            _ => {
425                bail!("\"exports\" field must be an object or a string");
426            }
427        };
428        Ok(Self(map))
429    }
430}
431
432impl Deref for ExportsField {
433    type Target = AliasMap<SubpathValue>;
434    fn deref(&self) -> &Self::Target {
435        &self.0
436    }
437}
438
439/// Content of an "imports" field in a package.json
440#[derive(PartialEq, Eq, Serialize, Deserialize)]
441pub struct ImportsField(AliasMap<SubpathValue>);
442
443impl TryFrom<&Value> for ImportsField {
444    type Error = anyhow::Error;
445
446    fn try_from(value: &Value) -> Result<Self> {
447        // The "imports" field must be an object.
448        // https://nodejs.org/api/packages.html#imports
449        let map = match value {
450            Value::Object(object) => {
451                let mut map = AliasMap::new();
452
453                for (key, value) in object.iter() {
454                    if !key.starts_with('#') {
455                        bail!("imports key \"{key}\" must begin with a '#'")
456                    }
457                    let value = SubpathValue::try_new(value, ExportImport::Import)?;
458                    map.insert(AliasPattern::parse(key.as_str()), value);
459                }
460
461                map
462            }
463            _ => bail!("\"imports\" field must be an object"),
464        };
465        Ok(Self(map))
466    }
467}
468
469impl Deref for ImportsField {
470    type Target = AliasMap<SubpathValue>;
471    fn deref(&self) -> &Self::Target {
472        &self.0
473    }
474}
475
476/// Returns true if the given string is a folder path shorthand.
477fn is_folder_shorthand(key: &str) -> bool {
478    key.ends_with('/') && key.find('*').is_none()
479}
480
481/// The exports field supports a shorthand for folders, where:
482///   "./folder/": "./other-folder/"
483/// is equivalent to
484///   "./folder/*": "./other-folder/*"
485/// This is not implemented directly by [`AliasMap`] as it is not
486/// shared behavior with the tsconfig.json `paths` field. Instead,
487/// we do the expansion here.
488fn expand_folder_shorthand(key: &str, value: &mut SubpathValue) -> Result<AliasPattern> {
489    // Transform folder patterns into wildcard patterns.
490    let pattern = AliasPattern::wildcard(key, "");
491
492    // Transform templates into wildcard patterns as well.
493    for result in value.results_mut() {
494        if result.ends_with('/') {
495            if result.find('*').is_none() {
496                let mut buf = result.to_string();
497                buf.push('*');
498                *result = buf.into();
499            } else {
500                bail!(
501                    "invalid exports field value \"{}\" for key \"{}\": \"*\" is not allowed in \
502                     folder exports",
503                    result,
504                    key
505                );
506            }
507        } else {
508            bail!(
509                "invalid exports field value \"{}\" for key \"{}\": folder exports must end with \
510                 \"/\"",
511                result,
512                key
513            );
514        }
515    }
516
517    Ok(pattern)
518}
519
520/// Content of an "alias" configuration
521#[turbo_tasks::value(shared)]
522#[derive(Default)]
523pub struct ResolveAliasMap(#[turbo_tasks(trace_ignore)] AliasMap<SubpathValue>);
524
525impl TryFrom<&FxIndexMap<RcStr, Value>> for ResolveAliasMap {
526    type Error = anyhow::Error;
527
528    fn try_from(object: &FxIndexMap<RcStr, Value>) -> Result<Self> {
529        let mut map = AliasMap::new();
530
531        for (key, value) in object.iter() {
532            let mut value = SubpathValue::try_new(value, ExportImport::Export)?;
533
534            let pattern = if is_folder_shorthand(key) {
535                expand_folder_shorthand(key, &mut value)?
536            } else {
537                AliasPattern::parse(key.as_str())
538            };
539
540            map.insert(pattern, value);
541        }
542        Ok(Self(map))
543    }
544}
545
546impl<'a> IntoIterator for &'a ResolveAliasMap {
547    type Item = (AliasPattern, &'a SubpathValue);
548    type IntoIter = AliasMapIter<'a, SubpathValue>;
549
550    fn into_iter(self) -> Self::IntoIter {
551        (&self.0).into_iter()
552    }
553}