#[must_use]
pub fn is_path_alias(specifier: &str) -> bool {
if specifier.starts_with('#') {
return true;
}
if specifier.starts_with("~/") || specifier.starts_with("~~/") || specifier.starts_with("@@/") {
return true;
}
if specifier.starts_with("@/") {
return true;
}
if specifier.starts_with('@') {
let scope = specifier.split('/').next().unwrap_or(specifier);
if scope.len() > 1 && scope.chars().nth(1).is_some_and(|c| c.is_ascii_uppercase()) {
return true;
}
}
false
}
#[must_use]
pub fn is_bare_specifier(specifier: &str) -> bool {
!specifier.starts_with('.')
&& !specifier.starts_with('/')
&& !specifier.contains("://")
&& !specifier.starts_with("data:")
}
#[must_use]
pub fn is_valid_package_name(name: &str) -> bool {
if name.is_empty() {
return false;
}
let first = name.as_bytes()[0];
if first == b'$' || first == b'!' || first == b'#' {
return false;
}
if name.contains('?') || name.contains('!') || name.starts_with("__") {
return false;
}
if name.bytes().all(|b| b.is_ascii_digit()) {
return false;
}
if !name.bytes().any(|b| b.is_ascii_alphabetic() || b == b'@') {
return false;
}
!name.contains(' ') && !name.contains('\\')
}
#[must_use]
pub fn extract_package_name(specifier: &str) -> String {
if specifier.starts_with('@') {
let parts: Vec<&str> = specifier.splitn(3, '/').collect();
if parts.len() >= 2 {
format!("{}/{}", parts[0], parts[1])
} else {
specifier.to_string()
}
} else {
specifier.split('/').next().unwrap_or(specifier).to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_package_name() {
assert_eq!(extract_package_name("react"), "react");
assert_eq!(extract_package_name("lodash/merge"), "lodash");
assert_eq!(extract_package_name("@scope/pkg"), "@scope/pkg");
assert_eq!(extract_package_name("@scope/pkg/foo"), "@scope/pkg");
}
#[test]
fn test_is_bare_specifier() {
assert!(is_bare_specifier("react"));
assert!(is_bare_specifier("@scope/pkg"));
assert!(is_bare_specifier("#internal/module"));
assert!(!is_bare_specifier("./utils"));
assert!(!is_bare_specifier("../lib"));
assert!(!is_bare_specifier("/absolute"));
}
#[test]
fn test_is_bare_specifier_url_specifiers() {
assert!(!is_bare_specifier("https://cdn.example.com/lib.js"));
assert!(!is_bare_specifier("http://example.com/module"));
assert!(!is_bare_specifier("data:text/javascript,export default 42"));
}
#[test]
fn path_alias_hash_prefix() {
assert!(is_path_alias("#internal/module"));
assert!(is_path_alias("#shared"));
}
#[test]
fn path_alias_tilde_prefix() {
assert!(is_path_alias("~/components/Button"));
assert!(is_path_alias("~~/utils/helpers"));
assert!(is_path_alias("@@/shared/utils"));
}
#[test]
fn path_alias_at_slash_prefix() {
assert!(is_path_alias("@/components/Button"));
assert!(is_path_alias("@/lib"));
}
#[test]
fn path_alias_pascal_case_scope() {
assert!(is_path_alias("@Components/Button"));
assert!(is_path_alias("@Hooks/useApi"));
assert!(is_path_alias("@Services/auth"));
}
#[test]
fn path_alias_lowercase_scope_is_not_alias() {
assert!(!is_path_alias("@babel/core"));
assert!(!is_path_alias("@types/react"));
assert!(!is_path_alias("@scope/pkg"));
}
#[test]
fn path_alias_plain_specifier_is_not_alias() {
assert!(!is_path_alias("react"));
assert!(!is_path_alias("lodash/merge"));
assert!(!is_path_alias("my-utils"));
}
#[test]
fn path_alias_tilde_without_slash_is_not_alias() {
assert!(!is_path_alias("~something"));
}
#[test]
fn valid_package_names() {
assert!(is_valid_package_name("react"));
assert!(is_valid_package_name("@scope/pkg"));
assert!(is_valid_package_name("lodash.get"));
assert!(is_valid_package_name("my-pkg"));
assert!(is_valid_package_name("@babel/core"));
assert!(is_valid_package_name("3d-view")); }
#[test]
fn invalid_package_names() {
assert!(!is_valid_package_name("$DIR"));
assert!(!is_valid_package_name("$ENV_VAR"));
assert!(!is_valid_package_name("1"));
assert!(!is_valid_package_name("123"));
assert!(!is_valid_package_name(""));
assert!(!is_valid_package_name("!important"));
assert!(!is_valid_package_name("has spaces"));
assert!(!is_valid_package_name("back\\slash"));
}
#[test]
fn extract_package_name_bare_scope_only() {
assert_eq!(extract_package_name("@scope"), "@scope");
}
#[test]
fn extract_package_name_deep_subpath() {
assert_eq!(
extract_package_name("@scope/pkg/deep/nested/path"),
"@scope/pkg"
);
}
#[test]
fn extract_package_name_single_name() {
assert_eq!(extract_package_name("react"), "react");
}
mod proptests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn relative_paths_are_not_bare(suffix in "[a-zA-Z0-9_/.-]{0,80}") {
let dot = format!(".{suffix}");
let slash = format!("/{suffix}");
prop_assert!(!is_bare_specifier(&dot), "'.{suffix}' was classified as bare");
prop_assert!(!is_bare_specifier(&slash), "'/{suffix}' was classified as bare");
}
#[test]
fn scoped_package_name_has_two_segments(
scope in "[a-z][a-z0-9-]{0,20}",
pkg in "[a-z][a-z0-9-]{0,20}",
subpath in "(/[a-z0-9-]{1,20}){0,3}",
) {
let specifier = format!("@{scope}/{pkg}{subpath}");
let extracted = extract_package_name(&specifier);
let expected = format!("@{scope}/{pkg}");
prop_assert_eq!(extracted, expected);
}
#[test]
fn unscoped_package_name_is_first_segment(
pkg in "[a-z][a-z0-9-]{0,30}",
subpath in "(/[a-z0-9-]{1,20}){0,3}",
) {
let specifier = format!("{pkg}{subpath}");
let extracted = extract_package_name(&specifier);
prop_assert_eq!(extracted, pkg);
}
#[test]
fn classification_functions_no_panic(s in "[a-zA-Z0-9@#~/._$!\\-]{1,100}") {
let _ = is_bare_specifier(&s);
let _ = is_path_alias(&s);
let _ = is_valid_package_name(&s);
}
#[test]
fn valid_npm_names_accepted(name in "[a-z][a-z0-9._-]{0,30}") {
prop_assert!(is_valid_package_name(&name));
}
#[test]
fn shell_variables_rejected(suffix in "[A-Z_]{1,20}") {
let specifier = format!("${suffix}");
prop_assert!(!is_valid_package_name(&specifier));
}
#[test]
fn pure_numbers_rejected(n in "[0-9]{1,10}") {
prop_assert!(!is_valid_package_name(&n));
}
#[test]
fn at_slash_is_path_alias(suffix in "[a-zA-Z0-9_/.-]{0,80}") {
let specifier = format!("@/{suffix}");
prop_assert!(is_path_alias(&specifier));
}
#[test]
fn tilde_slash_is_path_alias(suffix in "[a-zA-Z0-9_/.-]{0,80}") {
let specifier = format!("~/{suffix}");
prop_assert!(is_path_alias(&specifier));
}
#[test]
fn hash_prefix_is_path_alias(suffix in "[a-zA-Z0-9_/.-]{0,80}") {
let specifier = format!("#{suffix}");
prop_assert!(is_path_alias(&specifier));
}
#[test]
fn node_modules_package_name_never_empty(
pkg in "[a-z][a-z0-9-]{0,20}",
file in "[a-z]{1,10}\\.(js|ts|mjs)",
) {
let path = std::path::PathBuf::from(format!("/project/node_modules/{pkg}/{file}"));
if let Some(name) = crate::resolve::fallbacks::extract_package_name_from_node_modules_path(&path) {
prop_assert!(!name.is_empty());
}
}
}
}
}