next.js/turbopack/crates/turbopack-core/src/resolve/remap.rs
remap.rs576 lines20.7 KB
use std::{borrow::Cow, collections::BTreeMap, fmt::Display, ops::Deref};

use anyhow::{Result, bail};
use bincode::{Decode, Encode};
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use turbo_rcstr::{RcStr, rcstr};
use turbo_tasks::FxIndexMap;

use crate::resolve::{
    alias_map::{AliasKey, AliasMap, AliasMapIter, AliasPattern, AliasTemplate},
    options::ConditionValue,
    pattern::Pattern,
};

/// A small helper type to differentiate parsing exports and imports fields.
#[derive(Copy, Clone)]
enum ExportImport {
    Export,
    Import,
}

impl Display for ExportImport {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Export => f.write_str("export"),
            Self::Import => f.write_str("import"),
        }
    }
}

/// The result an "exports"/"imports" field describes. Can represent multiple
/// alternatives, conditional result, ignored result (null mapping) and a plain
/// result.
#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
pub enum SubpathValue {
    /// Alternative subpaths, defined with `"path": ["other1", "other2"]`,
    /// allows for specifying multiple possible remappings to be tried. This
    /// may be that conditions didn't match, or that a particular path
    /// wasn't found.
    Alternatives(Vec<SubpathValue>),

    /// Conditional subpaths, defined with `"path": { "condition": "other"}`,
    /// allow remapping based on certain predefined conditions. Eg, if using
    /// ESM import syntax, the `import` condition allows you to remap to a
    /// file that uses ESM syntax.
    ///
    /// Node defines several conditions in
    /// <https://nodejs.org/api/packages.html#conditional-exports>
    /// TODO: Should this use an enum of predefined keys?
    Conditional(Vec<(RcStr, SubpathValue)>),

    /// A result subpath, defined with `"path": "other"`, remaps imports of
    /// `path` to `other`.
    Result(RcStr),

    /// An excluded subpath, defined with `"path": null`, prevents importing
    /// this subpath.
    Excluded,
}

/// A `SubpathValue` that was applied to a pattern. See `SubpathValue` for
/// more details on the variants.
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub enum ReplacedSubpathValue {
    Alternatives(Vec<ReplacedSubpathValue>),
    Conditional(Vec<(RcStr, ReplacedSubpathValue)>),
    Result(Pattern),
    Excluded,
}

impl AliasTemplate for SubpathValue {
    type Output<'a>
        = ReplacedSubpathValue
    where
        Self: 'a;

    fn convert(&self) -> ReplacedSubpathValue {
        match self {
            SubpathValue::Alternatives(list) => ReplacedSubpathValue::Alternatives(
                list.iter()
                    .map(|value: &SubpathValue| value.convert())
                    .collect::<Vec<_>>(),
            ),
            SubpathValue::Conditional(list) => ReplacedSubpathValue::Conditional(
                list.iter()
                    .map(|(condition, value)| (condition.clone(), value.convert()))
                    .collect::<Vec<_>>(),
            ),
            SubpathValue::Result(value) => ReplacedSubpathValue::Result(value.clone().into()),
            SubpathValue::Excluded => ReplacedSubpathValue::Excluded,
        }
    }

    fn replace(&self, capture: &Pattern) -> ReplacedSubpathValue {
        match self {
            SubpathValue::Alternatives(list) => ReplacedSubpathValue::Alternatives(
                list.iter()
                    .map(|value: &SubpathValue| value.replace(capture))
                    .collect::<Vec<_>>(),
            ),
            SubpathValue::Conditional(list) => ReplacedSubpathValue::Conditional(
                list.iter()
                    .map(|(condition, value)| (condition.clone(), value.replace(capture)))
                    .collect::<Vec<_>>(),
            ),
            SubpathValue::Result(value) => {
                ReplacedSubpathValue::Result(capture.spread_into_star(value))
            }
            SubpathValue::Excluded => ReplacedSubpathValue::Excluded,
        }
    }
}

impl SubpathValue {
    /// Returns an iterator over all leaf results.
    fn results_mut(&mut self) -> ResultsIterMut<'_> {
        ResultsIterMut { stack: vec![self] }
    }

    /// Walks the [SubpathValue] and adds results to the `target` vector. It
    /// uses the `conditions` to skip or enter conditional results.
    /// The state of conditions is stored within `condition_overrides`, which is
    /// also exposed to the consumer.
    pub fn add_results<'a>(
        &'a self,
        conditions: &BTreeMap<RcStr, ConditionValue>,
        unspecified_condition: &ConditionValue,
        condition_overrides: &mut FxHashMap<&'a str, ConditionValue>,
        target: &mut Vec<(&'a str, Vec<(&'a str, bool)>)>,
    ) -> bool {
        match self {
            SubpathValue::Alternatives(list) => {
                for value in list {
                    if value.add_results(
                        conditions,
                        unspecified_condition,
                        condition_overrides,
                        target,
                    ) {
                        return true;
                    }
                }
                false
            }
            SubpathValue::Conditional(list) => {
                for (condition, value) in list {
                    let condition_value = if condition == "default" {
                        &ConditionValue::Set
                    } else {
                        condition_overrides
                            .get(condition.as_str())
                            .or_else(|| conditions.get(condition))
                            .unwrap_or(unspecified_condition)
                    };
                    match condition_value {
                        ConditionValue::Set => {
                            if value.add_results(
                                conditions,
                                unspecified_condition,
                                condition_overrides,
                                target,
                            ) {
                                return true;
                            }
                        }
                        ConditionValue::Unset => {}
                        ConditionValue::Unknown => {
                            condition_overrides.insert(condition, ConditionValue::Set);
                            if value.add_results(
                                conditions,
                                unspecified_condition,
                                condition_overrides,
                                target,
                            ) {
                                condition_overrides.insert(condition, ConditionValue::Unset);
                            } else {
                                condition_overrides.remove(condition.as_str());
                            }
                        }
                    }
                }
                false
            }
            SubpathValue::Result(r) => {
                target.push((
                    r,
                    condition_overrides
                        .iter()
                        .filter_map(|(k, v)| match v {
                            ConditionValue::Set => Some((*k, true)),
                            ConditionValue::Unset => Some((*k, false)),
                            ConditionValue::Unknown => None,
                        })
                        .collect(),
                ));
                true
            }
            SubpathValue::Excluded => true,
        }
    }

    fn try_new(value: &Value, ty: ExportImport) -> Result<Self> {
        match value {
            Value::Null => Ok(SubpathValue::Excluded),
            Value::String(s) => Ok(SubpathValue::Result(s.as_str().into())),
            Value::Number(_) => bail!("numeric values are invalid in {ty}s field entries"),
            Value::Bool(_) => bail!("boolean values are invalid in {ty}s field entries"),
            Value::Object(object) => Ok(SubpathValue::Conditional(
                object
                    .iter()
                    .map(|(key, value)| {
                        if key.starts_with('.') {
                            bail!(
                                "invalid key \"{}\" in an {ty} field conditions object. Did you \
                                 mean to place this request at a higher level?",
                                key
                            );
                        }

                        Ok((key.as_str().into(), SubpathValue::try_new(value, ty)?))
                    })
                    .collect::<Result<Vec<_>>>()?,
            )),
            Value::Array(array) => Ok(SubpathValue::Alternatives(
                array
                    .iter()
                    .map(|value| SubpathValue::try_new(value, ty))
                    .collect::<Result<Vec<_>>>()?,
            )),
        }
    }
}

pub struct ReplacedSubpathValueResult<'a, 'b> {
    pub result_path: Pattern,
    pub conditions: Vec<(RcStr, bool)>,
    pub map_prefix: Cow<'a, str>,
    pub map_key: &'b AliasKey,
}

impl ReplacedSubpathValue {
    // TODO
    #[allow(clippy::type_complexity)]
    /// Walks the [ReplacedSubpathValue] and adds results to the `target`
    /// vector. It uses the `conditions` to skip or enter conditional
    /// results. The state of conditions is stored within
    /// `condition_overrides`, which is also exposed to the consumer.
    pub fn add_results<'a, 'b>(
        self,
        prefix: Cow<'a, str>,
        key: &'b AliasKey,
        conditions: &BTreeMap<RcStr, ConditionValue>,
        unspecified_condition: &ConditionValue,
        condition_overrides: &mut FxHashMap<RcStr, ConditionValue>,
        target: &mut Vec<ReplacedSubpathValueResult<'a, 'b>>,
    ) -> bool {
        match self {
            ReplacedSubpathValue::Alternatives(list) => {
                for value in list {
                    if value.add_results(
                        prefix.clone(),
                        key,
                        conditions,
                        unspecified_condition,
                        condition_overrides,
                        target,
                    ) {
                        return true;
                    }
                }
                false
            }
            ReplacedSubpathValue::Conditional(list) => {
                for (condition, value) in list {
                    let condition_value = if condition == "default" {
                        &ConditionValue::Set
                    } else {
                        condition_overrides
                            .get(condition.as_str())
                            .or_else(|| conditions.get(&condition))
                            .unwrap_or(unspecified_condition)
                    };
                    match condition_value {
                        ConditionValue::Set => {
                            if value.add_results(
                                prefix.clone(),
                                key,
                                conditions,
                                unspecified_condition,
                                condition_overrides,
                                target,
                            ) {
                                return true;
                            }
                        }
                        ConditionValue::Unset => {}
                        ConditionValue::Unknown => {
                            condition_overrides.insert(condition.clone(), ConditionValue::Set);
                            if value.add_results(
                                prefix.clone(),
                                key,
                                conditions,
                                unspecified_condition,
                                condition_overrides,
                                target,
                            ) {
                                condition_overrides.insert(condition, ConditionValue::Unset);
                            } else {
                                condition_overrides.remove(condition.as_str());
                            }
                        }
                    }
                }
                false
            }
            ReplacedSubpathValue::Result(r) => {
                target.push(ReplacedSubpathValueResult {
                    result_path: r,
                    conditions: condition_overrides
                        .iter()
                        .filter_map(|(k, v)| match v {
                            ConditionValue::Set => Some((k.clone(), true)),
                            ConditionValue::Unset => Some((k.clone(), false)),
                            ConditionValue::Unknown => None,
                        })
                        .collect(),
                    map_prefix: prefix,
                    map_key: key,
                });
                true
            }
            ReplacedSubpathValue::Excluded => true,
        }
    }
}

struct ResultsIterMut<'a> {
    stack: Vec<&'a mut SubpathValue>,
}

impl<'a> Iterator for ResultsIterMut<'a> {
    type Item = &'a mut RcStr;

    fn next(&mut self) -> Option<Self::Item> {
        while let Some(value) = self.stack.pop() {
            match value {
                SubpathValue::Alternatives(list) => {
                    for value in list {
                        self.stack.push(value);
                    }
                }
                SubpathValue::Conditional(list) => {
                    for (_, value) in list {
                        self.stack.push(value);
                    }
                }
                SubpathValue::Result(r) => return Some(r),
                SubpathValue::Excluded => {}
            }
        }
        None
    }
}

/// Content of an "exports" field in a package.json
#[derive(PartialEq, Eq, Encode, Decode)]
pub struct ExportsField(AliasMap<SubpathValue>);

impl TryFrom<&Value> for ExportsField {
    type Error = anyhow::Error;

    fn try_from(value: &Value) -> Result<Self> {
        // The "exports" field can be an object, a string, or an array of strings.
        // https://nodejs.org/api/packages.html#exports
        let map = match value {
            Value::Object(object) => {
                let mut map = AliasMap::new();
                // Conditional exports can also be defined at the top-level of the
                // exports field, where they will apply to the package itself.
                let mut conditions = vec![];

                for (key, value) in object.iter() {
                    // NOTE: Node.js does not allow conditional and non-conditional keys
                    // to be mixed at the top-level, but we do.
                    if key != "." && !key.starts_with("./") {
                        conditions.push((key, value));
                        continue;
                    }

                    let mut value = SubpathValue::try_new(value, ExportImport::Export)?;

                    let pattern = if is_folder_shorthand(key) {
                        expand_folder_shorthand(key, &mut value)?
                    } else {
                        AliasPattern::parse(key.as_str())
                    };

                    map.insert(pattern, value);
                }

                if !conditions.is_empty() {
                    map.insert(
                        AliasPattern::Exact(rcstr!(".")),
                        SubpathValue::Conditional(
                            conditions
                                .into_iter()
                                .map(|(key, value)| {
                                    Ok((
                                        key.as_str().into(),
                                        SubpathValue::try_new(value, ExportImport::Export)?,
                                    ))
                                })
                                .collect::<Result<Vec<_>>>()?,
                        ),
                    );
                }

                map
            }
            Value::String(string) => {
                let mut map = AliasMap::new();
                map.insert(
                    AliasPattern::Exact(rcstr!(".")),
                    SubpathValue::Result(string.as_str().into()),
                );
                map
            }
            Value::Array(array) => {
                let mut map = AliasMap::new();
                map.insert(
                    AliasPattern::Exact(rcstr!(".")),
                    // This allows for more complex patterns than the spec allows, since we accept
                    // the following:
                    // [{ "node": "./node.js", "default": "./index.js" }, "./index.js"]
                    SubpathValue::Alternatives(
                        array
                            .iter()
                            .map(|value| SubpathValue::try_new(value, ExportImport::Export))
                            .collect::<Result<Vec<_>>>()?,
                    ),
                );
                map
            }
            _ => {
                bail!("\"exports\" field must be an object or a string");
            }
        };
        Ok(Self(map))
    }
}

impl Deref for ExportsField {
    type Target = AliasMap<SubpathValue>;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

/// Content of an "imports" field in a package.json
#[derive(PartialEq, Eq, Encode, Decode)]
pub struct ImportsField(AliasMap<SubpathValue>);

impl TryFrom<&Value> for ImportsField {
    type Error = anyhow::Error;

    fn try_from(value: &Value) -> Result<Self> {
        // The "imports" field must be an object.
        // https://nodejs.org/api/packages.html#imports
        let map = match value {
            Value::Object(object) => {
                let mut map = AliasMap::new();

                for (key, value) in object.iter() {
                    if !key.starts_with('#') {
                        bail!("imports key \"{key}\" must begin with a '#'")
                    }
                    let value = SubpathValue::try_new(value, ExportImport::Import)?;
                    map.insert(AliasPattern::parse(key.as_str()), value);
                }

                map
            }
            _ => bail!("\"imports\" field must be an object"),
        };
        Ok(Self(map))
    }
}

impl Deref for ImportsField {
    type Target = AliasMap<SubpathValue>;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

/// Returns true if the given string is a folder path shorthand.
fn is_folder_shorthand(key: &str) -> bool {
    key.ends_with('/') && key.find('*').is_none()
}

/// The exports field supports a shorthand for folders, where:
///   "./folder/": "./other-folder/"
/// is equivalent to
///   "./folder/*": "./other-folder/*"
/// This is not implemented directly by [`AliasMap`] as it is not
/// shared behavior with the tsconfig.json `paths` field. Instead,
/// we do the expansion here.
fn expand_folder_shorthand(key: &str, value: &mut SubpathValue) -> Result<AliasPattern> {
    // Transform folder patterns into wildcard patterns.
    let pattern = AliasPattern::wildcard(key, rcstr!(""));

    // Transform templates into wildcard patterns as well.
    for result in value.results_mut() {
        if result.ends_with('/') {
            if result.find('*').is_none() {
                let mut buf = result.to_string();
                buf.push('*');
                *result = buf.into();
            } else {
                bail!(
                    "invalid exports field value \"{}\" for key \"{}\": \"*\" is not allowed in \
                     folder exports",
                    result,
                    key
                );
            }
        } else {
            bail!(
                "invalid exports field value \"{}\" for key \"{}\": folder exports must end with \
                 \"/\"",
                result,
                key
            );
        }
    }

    Ok(pattern)
}

/// Content of an "alias" configuration
#[turbo_tasks::value(shared)]
#[derive(Default)]
pub struct ResolveAliasMap(#[turbo_tasks(trace_ignore)] AliasMap<SubpathValue>);

impl TryFrom<&FxIndexMap<RcStr, Value>> for ResolveAliasMap {
    type Error = anyhow::Error;

    fn try_from(object: &FxIndexMap<RcStr, Value>) -> Result<Self> {
        let mut map = AliasMap::new();

        for (key, value) in object.iter() {
            let mut value = SubpathValue::try_new(value, ExportImport::Export)?;

            let pattern = if is_folder_shorthand(key) {
                expand_folder_shorthand(key, &mut value)?
            } else {
                AliasPattern::parse(key.as_str())
            };

            map.insert(pattern, value);
        }
        Ok(Self(map))
    }
}

impl<'a> IntoIterator for &'a ResolveAliasMap {
    type Item = (AliasPattern, &'a SubpathValue);
    type IntoIter = AliasMapIter<'a, SubpathValue>;

    fn into_iter(self) -> Self::IntoIter {
        (&self.0).into_iter()
    }
}
Quest for Codev2.0.0
/
SIGN IN