rspack_resolver 0.7.0

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() {
      return Err(SpecifierError::Empty(specifier.to_string()));
    }
    let offset = match specifier.as_bytes()[0] {
      b'/' | b'.' | b'#' => 1,
      _ => 0,
    };
    let (path, query, fragment) = Self::parse_query_framgment(specifier, offset);
    if path.is_empty() {
      return Err(SpecifierError::Empty(specifier.to_string()));
    }
    Ok(Self {
      path,
      query,
      fragment,
    })
  }

  fn parse_query_framgment(
    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.chars().next().unwrap();
    let mut escaped_indexes = vec![];
    for (i, c) in specifier.char_indices().skip(skip) {
      if c == '?' && query_start.is_none() {
        query_start = Some(i);
      }
      if c == '#' {
        if prev == '\0' {
          escaped_indexes.push(i - 1);
        } else {
          fragment_start = Some(i);
          break;
        }
      }
      prev = c;
    }

    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 = if escaped_indexes.is_empty() {
      Cow::Borrowed(path)
    } else {
      // Remove the `\0` characters for a legal path.
      Cow::Owned(
        path
          .chars()
          .enumerate()
          .filter_map(|(i, c)| (!escaped_indexes.contains(&i)).then_some(c))
          .collect::<String>(),
      )
    };

    (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(())
  }
}