rspack_core 0.100.0-rc.3

rspack core
Documentation
use std::borrow::Cow;

use rspack_paths::Utf8Path;
use rspack_util::identifier::absolute_to_request;
use swc_core::ecma::utils::is_valid_prop_ident;

use crate::BoxLoader;

pub fn to_module_export_name(name: &str) -> String {
  if is_valid_prop_ident(name) {
    name.into()
  } else {
    rspack_util::json_stringify_str(name)
  }
}

pub fn contextify(context: impl AsRef<Utf8Path>, request: &str) -> String {
  let context = context.as_ref();
  request
    .split('!')
    .map(|r| absolute_to_request(context.as_str(), r))
    .collect::<Vec<Cow<str>>>()
    .join("!")
}

macro_rules! ident_table {
  ( $( range: $range_start:literal, $range_end:expr ; )* $( unit: $unit:expr ; )* ) => {{
    let mut table = 0u128;

    $(
      assert!($range_start <= 128);
      assert!($range_end <= 128);
      let mut curr = $range_start;
      while curr <= $range_end {
        table |= 1 << curr;
        curr += 1;
      }
    )*

    $(
      assert!($unit <= 128);
      table |= 1 << $unit;
    )*

    table
  }}
}

fn is_ident_first_safe(b: u8) -> bool {
  const TABLE: u128 = ident_table! {
    range: b'A', b'Z';
    range: b'a', b'z';
    unit : b'$';
    unit : b'_';
  };

  b < 128 && (TABLE & (1 << b)) != 0
}

fn is_ident_safe(b: u8) -> bool {
  const TABLE: u128 = ident_table! {
    range: b'0', b'9';
    range: b'A', b'Z';
    range: b'a', b'z';
    unit : b'$';
  };

  b < 128 && (TABLE & (1 << b)) != 0
}

#[inline]
pub fn to_identifier(v: &str) -> Cow<'_, str> {
  let mut buf = if v
    .as_bytes()
    .first()
    .is_none_or(|&b| is_ident_first_safe(b) || !is_ident_safe(b))
  {
    String::new()
  } else {
    "_".into()
  };

  if escape_identifier_impl(v, &mut buf) {
    if buf.is_empty() {
      Cow::Borrowed(v)
    } else {
      buf.push_str(v);
      Cow::Owned(buf)
    }
  } else {
    Cow::Owned(buf)
  }
}

pub fn to_identifier_with_escaped(v: String) -> String {
  if v.is_empty() {
    return v;
  }

  let bytes = v.as_bytes();
  // Fast path: the input is already a valid JS identifier — skip the full
  // escape (see #10760). `_` is a valid continuation char even though
  // `is_ident_safe` excludes it (it's the replacement sentinel for the escape
  // impl), so handle it inline here.
  if is_ident_first_safe(bytes[0]) && bytes.iter().all(|&b| is_ident_safe(b) || b == b'_') {
    return v;
  }

  // Defensive path: invalid characters anywhere in the input (e.g. JSON keys
  // like "!top" or "with space") need the full escape so we never emit bare
  // invalid characters into a JS identifier position.
  to_identifier(&v).into_owned()
}

pub fn escape_identifier(v: &str) -> Cow<'_, str> {
  let mut buf = String::new();
  if escape_identifier_impl(v, &mut buf) {
    Cow::Borrowed(v)
  } else {
    Cow::Owned(buf)
  }
}

fn escape_identifier_impl(v: &str, out: &mut String) -> bool {
  let vstr = v;
  let v = v.as_bytes();
  let mut pos = 0;
  let mut is_safe = true;

  // hot path
  if v.iter().all(|&b| is_ident_safe(b)) {
    return true;
  }

  for i in 0..v.len() {
    match (is_ident_safe(v[i]), is_safe) {
      (true, true) => (),
      (false, true) => {
        // # Safety
        //
        // always ascii
        let s = &vstr[pos..i];
        out.push_str(s);
        out.push('_');
        pos = i + 1;
        is_safe = false;
      }
      (false, false) => pos = i + 1,
      (true, false) => is_safe = true,
    }
  }

  if pos != 0 {
    // # Safety
    //
    // always ascii
    let s = &vstr[pos..];
    out.push_str(s);
  }

  pos == 0
}

pub fn stringify_loaders_and_resource<'a>(
  loaders: &'a [BoxLoader],
  resource: &'a str,
) -> Cow<'a, str> {
  if !loaders.is_empty() {
    let mut s = String::new();
    for loader in loaders {
      let identifier = loader.identifier();
      if let Some((_type, ident)) = identifier.split_once('|') {
        s.push_str(ident);
      } else {
        s.push_str(identifier.as_str());
      }
      s.push('!');
    }
    s.push_str(resource);
    Cow::Owned(s)
  } else {
    Cow::Borrowed(resource)
  }
}

#[test]
fn test_to_identifier() {
  assert_eq!(to_identifier("ident0"), "ident0");
  assert_eq!(to_identifier("0ident"), "_0ident");
  assert_eq!(to_identifier("/ident"), "_ident");
  assert_eq!(
    to_identifier("ident0_stable/src/core/iter//range.rs"),
    "ident0_stable_src_core_iter_range_rs"
  );
  assert_eq!(
    to_identifier("ident0_stable/src/core/iter//range.rs?"),
    "ident0_stable_src_core_iter_range_rs_"
  );
}

#[test]
fn test_to_identifier_with_escaped() {
  // Already-valid identifiers pass through unchanged.
  assert_eq!(to_identifier_with_escaped("ident0".into()), "ident0");
  assert_eq!(to_identifier_with_escaped("_top".into()), "_top");
  assert_eq!(to_identifier_with_escaped("$foo".into()), "$foo");

  // First-char-only fixups still work.
  assert_eq!(to_identifier_with_escaped("0ident".into()), "_0ident");

  // Invalid characters anywhere in the input are escaped — regression coverage
  // for JSON keys like "!top" / "with space" leaking into JS identifier positions.
  assert_eq!(to_identifier_with_escaped("!top".into()), "_top");
  assert_eq!(to_identifier_with_escaped("_!top".into()), "_top");
  assert_eq!(
    to_identifier_with_escaped("with space".into()),
    "with_space"
  );
  assert_eq!(to_identifier_with_escaped("a.b".into()), "a_b");
}