oxc_resolver 11.19.1

ESM / CJS module resolution
Documentation
use std::borrow::Cow;

use crate::error::SpecifierError;

#[derive(Debug)]
pub struct Specifier<'a> {
    path: Cow<'a, str>,
    pub query: Option<&'a str>,
    pub fragment: Option<&'a str>,
}

impl<'a> Specifier<'a> {
    pub fn path(&'a self) -> &'a str {
        self.path.as_ref()
    }

    pub fn parse(specifier: &'a str) -> Result<Self, SpecifierError> {
        if specifier.is_empty() {
            #[cold]
            fn empty_specifier_error(specifier: &str) -> SpecifierError {
                SpecifierError::Empty(specifier.to_string())
            }
            return Err(empty_specifier_error(specifier));
        }
        let offset = match specifier.as_bytes()[0] {
            b'/' | b'.' | b'#' => 1,
            _ => 0,
        };
        let (path, query, fragment) = Self::parse_query_fragment(specifier, offset);
        if path.is_empty() {
            #[cold]
            fn empty_path_error(specifier: &str) -> SpecifierError {
                SpecifierError::Empty(specifier.to_string())
            }
            return Err(empty_path_error(specifier));
        }
        Ok(Self { path, query, fragment })
    }

    fn parse_query_fragment(
        specifier: &'a str,
        skip: usize,
    ) -> (Cow<'a, str>, Option<&'a str>, Option<&'a str>) {
        let mut query_start: Option<usize> = None;
        let mut fragment_start: Option<usize> = None;

        let mut prev = specifier.as_bytes()[0];
        // Optimize for the common case: most specifiers don't have escaped characters
        let mut escaped_indexes: Option<Vec<usize>> = None;
        for (i, &b) in specifier.as_bytes().iter().enumerate().skip(skip) {
            if b == b'?' && query_start.is_none() {
                query_start = Some(i);
            }
            if b == b'#' {
                if prev == b'\0' {
                    escaped_indexes.get_or_insert_with(Vec::new).push(i - 1);
                } else {
                    fragment_start = Some(i);
                    break;
                }
            }
            prev = b;
        }

        let (path, query, fragment) = match (query_start, fragment_start) {
            (Some(i), Some(j)) => {
                debug_assert!(i < j);
                (&specifier[..i], Some(&specifier[i..j]), Some(&specifier[j..]))
            }
            (Some(i), None) => (&specifier[..i], Some(&specifier[i..]), None),
            (None, Some(j)) => (&specifier[..j], None, Some(&specifier[j..])),
            _ => (specifier, None, None),
        };

        let path = escaped_indexes.map_or(Cow::Borrowed(path), |escaped_indexes| {
            let mut s = String::with_capacity(path.len());
            for (i, &b) in path.as_bytes().iter().enumerate() {
                if !escaped_indexes.contains(&i) {
                    s.push(b as char);
                }
            }
            Cow::Owned(s)
        });

        (path, query, fragment)
    }
}

#[cfg(test)]
mod tests {
    use super::{Specifier, SpecifierError};

    #[test]
    fn debug() {
        let specifier = Specifier::parse("/").unwrap();
        assert_eq!(
            format!("{specifier:?}"),
            r#"Specifier { path: "/", query: None, fragment: None }"#
        );
    }

    #[test]
    fn empty() {
        let specifiers = ["", "?"];
        for specifier in specifiers {
            let error = Specifier::parse(specifier).unwrap_err();
            assert_eq!(error, SpecifierError::Empty(specifier.to_string()));
        }
    }

    #[test]
    fn absolute() -> Result<(), SpecifierError> {
        let specifier = "/test?#";
        let parsed = Specifier::parse(specifier)?;
        assert_eq!(parsed.path, "/test");
        assert_eq!(parsed.query, Some("?"));
        assert_eq!(parsed.fragment, Some("#"));
        Ok(())
    }

    #[test]
    fn relative() -> Result<(), SpecifierError> {
        let specifiers = ["./test", "../test", "../../test"];
        for specifier in specifiers {
            let mut r = specifier.to_string();
            r.push_str("?#");
            let parsed = Specifier::parse(&r)?;
            assert_eq!(parsed.path, specifier);
            assert_eq!(parsed.query, Some("?"));
            assert_eq!(parsed.fragment, Some("#"));
        }
        Ok(())
    }

    #[test]
    fn hash() -> Result<(), SpecifierError> {
        let specifiers = ["#", "#path"];
        for specifier in specifiers {
            let mut r = specifier.to_string();
            r.push_str("?#");
            let parsed = Specifier::parse(&r)?;
            assert_eq!(parsed.path, specifier);
            assert_eq!(parsed.query, Some("?"));
            assert_eq!(parsed.fragment, Some("#"));
        }
        Ok(())
    }

    #[test]
    fn module() -> Result<(), SpecifierError> {
        let specifiers = ["module"];
        for specifier in specifiers {
            let mut r = specifier.to_string();
            r.push_str("?#");
            let parsed = Specifier::parse(&r)?;
            assert_eq!(parsed.path, specifier);
            assert_eq!(parsed.query, Some("?"));
            assert_eq!(parsed.fragment, Some("#"));
        }
        Ok(())
    }

    #[test]
    fn query_fragment() -> Result<(), SpecifierError> {
        let data = [
            ("a?", Some("?"), None),
            ("a?query", Some("?query"), None),
            ("a?query1?query2", Some("?query1?query2"), None),
            ("a?query1?query2?query3", Some("?query1?query2?query3"), None),
            ("a#", None, Some("#")),
            ("a#b#c", None, Some("#b#c")),
            ("a#fragment", None, Some("#fragment")),
            ("a?#", Some("?"), Some("#")),
            ("a?#fragment", Some("?"), Some("#fragment")),
            ("a?query#", Some("?query"), Some("#")),
            ("a?query#fragment", Some("?query"), Some("#fragment")),
            ("a#fragment?", None, Some("#fragment?")),
            ("a#fragment?query", None, Some("#fragment?query")),
        ];

        for (specifier_str, query, fragment) in data {
            let specifier = Specifier::parse(specifier_str)?;
            assert_eq!(specifier.path, "a", "{specifier_str}");
            assert_eq!(specifier.query, query, "{specifier_str}");
            assert_eq!(specifier.fragment, fragment, "{specifier_str}");
        }

        Ok(())
    }

    #[test]
    // https://github.com/webpack/enhanced-resolve/blob/main/test/identifier.test.js
    fn enhanced_resolve_edge_cases() -> Result<(), SpecifierError> {
        let data = [
            ("path/#", "path/", "", "#"),
            ("path/as/?", "path/as/", "?", ""),
            ("path/#/?", "path/", "", "#/?"),
            ("path/#repo#hash", "path/", "", "#repo#hash"),
            ("path/#r#hash", "path/", "", "#r#hash"),
            ("path/#repo/#repo2#hash", "path/", "", "#repo/#repo2#hash"),
            ("path/#r/#r#hash", "path/", "", "#r/#r#hash"),
            ("path/#/not/a/hash?not-a-query", "path/", "", "#/not/a/hash?not-a-query"),
        ];

        for (specifier_str, path, query, fragment) in data {
            let specifier = Specifier::parse(specifier_str)?;
            assert_eq!(specifier.path, path, "{specifier_str}");
            assert_eq!(specifier.query.unwrap_or(""), query, "{specifier_str}");
            assert_eq!(specifier.fragment.unwrap_or(""), fragment, "{specifier_str}");
        }

        Ok(())
    }

    // https://github.com/webpack/enhanced-resolve/blob/main/test/identifier.test.js
    #[test]
    fn enhanced_resolve_windows_like() -> Result<(), SpecifierError> {
        let data = [
            ("path\\#", "path\\", "", "#"),
            ("path\\as\\?", "path\\as\\", "?", ""),
            ("path\\#\\?", "path\\", "", "#\\?"),
            ("path\\#repo#hash", "path\\", "", "#repo#hash"),
            ("path\\#r#hash", "path\\", "", "#r#hash"),
            ("path\\#repo\\#repo2#hash", "path\\", "", "#repo\\#repo2#hash"),
            ("path\\#r\\#r#hash", "path\\", "", "#r\\#r#hash"),
            ("path\\#/not/a/hash?not-a-query", "path\\", "", "#/not/a/hash?not-a-query"),
        ];

        for (specifier_str, path, query, fragment) in data {
            let specifier = Specifier::parse(specifier_str)?;
            assert_eq!(specifier.path, path, "{specifier_str}");
            assert_eq!(specifier.query.unwrap_or(""), query, "{specifier_str}");
            assert_eq!(specifier.fragment.unwrap_or(""), fragment, "{specifier_str}");
        }

        Ok(())
    }
}