Skip to main content

turbopack_core/resolve/
remap.rs

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