use {
crate::{
cli::UpdateTarget,
semver_range::SemverRange,
specifier::{
alias::Alias, catalog::Catalog, complex_semver::ComplexSemver, exact::Exact, git::Git, latest::Latest, link::Link, major::Major,
minor::Minor, range::Range, range_major::RangeMajor, range_minor::RangeMinor, tag::Tag, workspace_protocol::WorkspaceProtocol,
},
},
std::{cell::RefCell, collections::HashMap, rc::Rc},
};
pub mod alias;
pub mod catalog;
pub mod complex_semver;
pub mod exact;
pub mod file;
pub mod link;
#[cfg(test)]
#[path = "specifier/get_node_range_test.rs"]
mod get_node_range_test;
#[cfg(test)]
#[path = "specifier/get_semver_number_test.rs"]
mod get_semver_number_test;
pub mod git;
#[cfg(test)]
#[path = "specifier/has_same_release_channel_as_test.rs"]
mod has_same_release_channel_as_test;
#[cfg(test)]
#[path = "specifier/has_same_version_number_as_test.rs"]
mod has_same_version_number_as_test;
#[cfg(test)]
#[path = "specifier/is_eligible_update_for_test.rs"]
mod is_eligible_update_for_test;
pub mod latest;
pub mod major;
pub mod minor;
#[cfg(test)]
#[path = "specifier/new_test.rs"]
mod new_test;
#[cfg(test)]
#[path = "specifier/ordering_test.rs"]
mod ordering_test;
pub mod parser;
pub mod range;
pub mod range_major;
pub mod range_minor;
pub mod regexes;
#[cfg(test)]
#[path = "specifier/satisfies_all_test.rs"]
mod satisfies_all_test;
pub mod tag;
pub mod url;
#[cfg(test)]
#[path = "specifier/with_node_version_test.rs"]
mod with_node_version_test;
#[cfg(test)]
#[path = "specifier/with_range_test.rs"]
mod with_range_test;
pub mod workspace_protocol;
pub mod workspace_specifier;
thread_local! {
static SPECIFIER_CACHE: RefCell<HashMap<String, Rc<Specifier>>> = RefCell::new(HashMap::new());
static RANGE_CACHE: RefCell<HashMap<String, Rc<node_semver::Range>>> = RefCell::new(HashMap::new());
static VERSION_CACHE: RefCell<HashMap<String, Rc<node_semver::Version>>> = RefCell::new(HashMap::new());
}
const ALIAS: &str = "alias";
const CATALOG: &str = "catalog";
const RANGE_COMPLEX: &str = "range-complex";
const EXACT: &str = "exact";
const FILE: &str = "file";
const LINK: &str = "link";
const GIT: &str = "git";
const LATEST: &str = "latest";
const MAJOR: &str = "major";
const MINOR: &str = "minor";
const MISSING: &str = "missing";
const RANGE: &str = "range";
const RANGE_MAJOR: &str = "range-major";
const RANGE_MINOR: &str = "range-minor";
const TAG: &str = "tag";
const UNSUPPORTED: &str = "unsupported";
const URL: &str = "url";
const WORKSPACE_PROTOCOL: &str = "workspace-protocol";
pub const HUGE: u64 = 999999;
pub fn strip_semver_range(value: &str) -> &str {
["^", "~", ">=", "<=", ">", "<"]
.into_iter()
.find_map(|prefix| value.strip_prefix(prefix))
.unwrap_or(value)
}
#[derive(Debug, PartialEq)]
pub enum Specifier {
Alias(alias::Alias), Catalog(catalog::Catalog), ComplexSemver(complex_semver::ComplexSemver), Exact(exact::Exact), File(file::File), Link(link::Link), Git(git::Git), Latest(latest::Latest), Major(major::Major), Minor(minor::Minor), None, Range(range::Range), RangeMajor(range_major::RangeMajor), RangeMinor(range_minor::RangeMinor), Tag(tag::Tag), Unsupported(String), Url(url::Url), WorkspaceProtocol(workspace_protocol::WorkspaceProtocol), }
impl Specifier {
pub fn new(value: &str) -> Rc<Self> {
SPECIFIER_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
match cache.get(value) {
Some(rc) => rc.clone(),
None => {
let rc = Rc::new(Self::create(value));
cache.insert(value.to_string(), rc.clone());
rc
}
}
})
}
pub fn new_node_version(value: &str) -> Option<Rc<node_semver::Version>> {
VERSION_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
match cache.get(value) {
Some(rc) => Some(rc.clone()),
None => node_semver::Version::parse(value).ok().map(|range| {
let rc = Rc::new(range);
cache.insert(value.to_string(), rc.clone());
rc
}),
}
})
}
pub fn new_node_range(value: &str) -> Option<Rc<node_semver::Range>> {
RANGE_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
match cache.get(value) {
Some(rc) => Some(rc.clone()),
None => node_semver::Range::parse(value).ok().map(|range| {
let rc = Rc::new(range);
cache.insert(value.to_string(), rc.clone());
rc
}),
}
})
}
pub(crate) fn create(value: &str) -> Self {
if value.is_empty() {
return Self::None;
}
if parser::is_exact(value) {
return Exact::create(value);
}
if parser::is_range(value) {
return Range::create(value);
}
if parser::is_latest(value) {
return Latest::create(value);
}
if parser::is_major(value) {
return Major::create(value);
}
if parser::is_minor(value) {
return Minor::create(value);
}
if parser::is_range_major(value) {
return RangeMajor::create(value);
}
if parser::is_range_minor(value) {
return RangeMinor::create(value);
}
if parser::is_complex_range(value) {
return ComplexSemver::create(value);
}
let first_char = value.chars().next().unwrap_or('\0');
if first_char == 'c' && value.starts_with("catalog:") {
return Catalog::create(value);
}
if first_char == 'w' && value.starts_with("workspace:") {
return WorkspaceProtocol::create(value);
}
if parser::is_tag(value) {
return Tag::create(value);
}
if first_char == 'n' && value.starts_with("npm:") {
return Alias::create(value);
}
if parser::is_git(value) {
return Git::create(value);
}
if first_char == 'f' && value.starts_with("file:") {
return file::File::create(value);
}
if parser::is_link(value) {
return Link::create(value);
}
if first_char == 'h' && (value.starts_with("http://") || value.starts_with("https://")) {
return url::Url::create(value);
}
Self::Unsupported(value.to_string())
}
}
impl Specifier {
pub fn get_config_identifier(&self) -> &'static str {
match self {
Self::Alias(_) => ALIAS,
Self::Catalog(_) => CATALOG,
Self::ComplexSemver(_) => RANGE_COMPLEX,
Self::Exact(_) => EXACT,
Self::File(_) => FILE,
Self::Link(_) => LINK,
Self::Git(_) => GIT,
Self::Latest(_) => LATEST,
Self::Major(_) => MAJOR,
Self::Minor(_) => MINOR,
Self::None => MISSING,
Self::Range(_) => RANGE,
Self::RangeMajor(_) => RANGE_MAJOR,
Self::RangeMinor(_) => RANGE_MINOR,
Self::Tag(_) => TAG,
Self::Unsupported(_) => UNSUPPORTED,
Self::Url(_) => URL,
Self::WorkspaceProtocol(_) => WORKSPACE_PROTOCOL,
}
}
pub fn get_semver_number(&self) -> Option<&str> {
match self {
Self::Alias(s) => s.inner_specifier.get_semver_number(),
Self::Exact(s) => Some(&s.raw),
Self::Major(s) => Some(&s.raw),
Self::Minor(s) => Some(&s.raw),
Self::Range(s) => Some(strip_semver_range(&s.raw)),
Self::RangeMajor(s) => Some(strip_semver_range(&s.raw)),
Self::RangeMinor(s) => Some(strip_semver_range(&s.raw)),
Self::WorkspaceProtocol(s) => s.as_resolved().and_then(|spec| spec.get_semver_number()),
Self::Git(s) => s.semver_number.as_deref(),
Self::Catalog(_)
| Self::ComplexSemver(_)
| Self::File(_)
| Self::Link(_)
| Self::Latest(_)
| Self::None
| Self::Tag(_)
| Self::Unsupported(_)
| Self::Url(_) => None,
}
}
pub fn get_node_version(&self) -> Option<Rc<node_semver::Version>> {
match self {
Self::Alias(s) => s.inner_specifier.get_node_version(),
Self::Git(s) => s.node_version.clone(),
Self::Exact(s) => Some(s.node_version.clone()),
Self::Latest(s) => Some(s.node_version.clone()),
Self::Major(s) => Some(s.node_version.clone()),
Self::Minor(s) => Some(s.node_version.clone()),
Self::Range(s) => Some(s.node_version.clone()),
Self::RangeMajor(s) => Some(s.node_version.clone()),
Self::RangeMinor(s) => Some(s.node_version.clone()),
Self::WorkspaceProtocol(s) => s.as_resolved().and_then(|spec| spec.get_node_version()),
Self::Catalog(_)
| Self::ComplexSemver(_)
| Self::File(_)
| Self::Link(_)
| Self::None
| Self::Tag(_)
| Self::Unsupported(_)
| Self::Url(_) => None,
}
}
pub fn get_node_range(&self) -> Option<Rc<node_semver::Range>> {
match self {
Self::Alias(s) => s.inner_specifier.get_node_range(),
Self::ComplexSemver(s) => Some(s.node_range.clone()),
Self::Git(s) => s.node_range.clone(),
Self::Range(s) => Some(s.node_range.clone()),
Self::RangeMajor(s) => Some(s.node_range.clone()),
Self::RangeMinor(s) => Some(s.node_range.clone()),
Self::WorkspaceProtocol(s) => {
s.as_resolved().and_then(|spec| spec.get_node_range()).or_else(|| {
use crate::{semver_range::SemverRange, specifier::workspace_specifier::WorkspaceSpecifier};
match &s.inner_specifier {
WorkspaceSpecifier::RangeOnly(SemverRange::Any) => {
let huge = HUGE.to_string();
let range_str = format!(">=0.0.0 <={huge}.{huge}.{huge}");
Self::new_node_range(&range_str)
}
_ => None,
}
})
}
Self::Exact(s) => Some(s.node_range.clone()),
Self::Latest(_) => {
let huge = HUGE.to_string();
let range_str = format!(">=0.0.0 <={huge}.{huge}.{huge}");
Self::new_node_range(&range_str)
}
Self::Major(m) => {
let range_str = format!(">={}.0.0 <{}.0.0", m.raw, m.raw.parse::<u64>().ok()? + 1);
Self::new_node_range(&range_str)
}
Self::Minor(m) => {
let parts: Vec<&str> = m.raw.split('.').collect();
if parts.len() != 2 {
return None;
}
let major = parts[0];
let minor = parts[1].parse::<u64>().ok()?;
let range_str = format!(">={}.0 <{}.{}.0", m.raw, major, minor + 1);
Self::new_node_range(&range_str)
}
Self::Catalog(_) | Self::File(_) | Self::Link(_) | Self::None | Self::Tag(_) | Self::Unsupported(_) | Self::Url(_) => None,
}
}
pub fn get_semver_range(&self) -> Option<SemverRange> {
match self {
Self::Alias(s) => s.inner_specifier.get_semver_range(),
Self::Git(s) => s.semver_range.clone(),
Self::Latest(s) => Some(s.semver_range.clone()),
Self::Range(s) => Some(s.semver_range.clone()),
Self::RangeMajor(s) => Some(s.semver_range.clone()),
Self::RangeMinor(s) => Some(s.semver_range.clone()),
Self::WorkspaceProtocol(s) => match &s.inner_specifier {
workspace_specifier::WorkspaceSpecifier::RangeOnly(r) => Some(r.clone()),
workspace_specifier::WorkspaceSpecifier::Resolved(spec) => spec.get_semver_range(),
},
Self::Exact(_) => Some(SemverRange::Exact),
Self::Major(_) => Some(SemverRange::Exact),
Self::Minor(_) => Some(SemverRange::Exact),
Self::Catalog(_)
| Self::ComplexSemver(_)
| Self::File(_)
| Self::Link(_)
| Self::None
| Self::Tag(_)
| Self::Unsupported(_)
| Self::Url(_) => None,
}
}
pub fn get_raw(&self) -> &str {
match self {
Self::Alias(s) => &s.raw,
Self::Catalog(s) => &s.raw,
Self::ComplexSemver(s) => &s.raw,
Self::Exact(s) => &s.raw,
Self::File(s) => &s.raw,
Self::Git(s) => &s.raw,
Self::Latest(s) => &s.raw,
Self::Major(s) => &s.raw,
Self::Minor(s) => &s.raw,
Self::None => "",
Self::Range(s) => &s.raw,
Self::RangeMajor(s) => &s.raw,
Self::RangeMinor(s) => &s.raw,
Self::Tag(s) => &s.raw,
Self::Url(s) => &s.raw,
Self::Link(s) => &s.raw,
Self::WorkspaceProtocol(s) => &s.raw,
Self::Unsupported(s) => s,
}
}
}
impl Specifier {
pub fn with_range(&self, range: &SemverRange) -> Option<Rc<Self>> {
if matches!(range, SemverRange::Any) {
return match self {
Self::Exact(_) | Self::Major(_) | Self::Minor(_) | Self::Range(_) | Self::RangeMajor(_) | Self::RangeMinor(_) => {
Some(Self::new("*"))
}
Self::WorkspaceProtocol(_) => Some(Self::new("workspace:*")),
Self::Alias(s) => {
if s.inner_specifier.get_semver_number().is_some() {
Some(Self::new(&format!("npm:{}", s.name)))
} else {
None
}
}
Self::Git(s) => s.semver_number.as_ref().map(|_| Self::new(&s.origin)),
Self::Catalog(_)
| Self::ComplexSemver(_)
| Self::File(_)
| Self::Link(_)
| Self::Latest(_)
| Self::None
| Self::Tag(_)
| Self::Unsupported(_)
| Self::Url(_) => None,
};
}
let range_str = range.unwrap();
match self {
Self::Exact(s) => {
let new_specifier = format!("{}{}", range_str, s.node_version);
Some(Self::new(&new_specifier))
}
Self::Major(s) => {
let new_specifier = format!("{}{}", range_str, s.raw);
Some(Self::new(&new_specifier))
}
Self::Minor(s) => {
let new_specifier = format!("{}{}", range_str, s.raw);
Some(Self::new(&new_specifier))
}
Self::Range(s) => {
let new_specifier = format!("{}{}", range_str, s.semver_number);
Some(Self::new(&new_specifier))
}
Self::RangeMajor(s) => {
let new_specifier = format!("{}{}", range_str, s.semver_number);
Some(Self::new(&new_specifier))
}
Self::RangeMinor(s) => {
let new_specifier = format!("{}{}", range_str, s.semver_number);
Some(Self::new(&new_specifier))
}
Self::Alias(s) => s
.inner_specifier
.with_range(range)
.map(|new_inner| Self::new(&format!("npm:{}@{}", s.name, new_inner.get_raw()))),
Self::WorkspaceProtocol(s) => {
if let Some(resolved) = s.as_resolved() {
if let Some(semver_number) = resolved.get_semver_number() {
Some(Self::new(&format!("workspace:{range_str}{semver_number}")))
} else {
Some(Self::new(&format!("workspace:{range_str}")))
}
} else {
Some(Self::new(&format!("workspace:{range_str}")))
}
}
Self::Git(s) => s.semver_number.as_ref().map(|semver_number| {
if range_str.is_empty() {
Self::new(&format!("{}#{}", s.origin, semver_number))
} else {
Self::new(&format!("{}#semver:{}{}", s.origin, range_str, semver_number))
}
}),
Self::Catalog(_)
| Self::ComplexSemver(_)
| Self::File(_)
| Self::Link(_)
| Self::Latest(_)
| Self::None
| Self::Tag(_)
| Self::Unsupported(_)
| Self::Url(_) => None,
}
}
pub fn with_node_version(&self, node_version: &node_semver::Version) -> Option<Rc<Self>> {
let version_str = node_version.to_string();
let is_huge_minor = node_version.minor == HUGE;
let is_huge_patch = node_version.patch == HUGE;
match self {
Self::Exact(_) => Some(Self::new(&version_str)),
Self::Major(_) => {
Some(Self::new(&format!("{}", node_version.major)))
}
Self::Minor(_) => {
Some(Self::new(&format!("{}.{}", node_version.major, node_version.minor)))
}
Self::Range(r) => Some(Self::new(&format!("{}{}", r.semver_range.unwrap(), version_str))),
Self::RangeMajor(r) => {
Some(Self::new(&format!("{}{}", r.semver_range.unwrap(), node_version.major)))
}
Self::RangeMinor(r) => {
Some(Self::new(&format!(
"{}{}.{}",
r.semver_range.unwrap(),
node_version.major,
node_version.minor
)))
}
Self::Alias(a) => a
.inner_specifier
.with_node_version(node_version)
.map(|new_inner| Self::new(&format!("npm:{}@{}", a.name, new_inner.get_raw()))),
Self::WorkspaceProtocol(wp) => {
wp.as_resolved().and_then(|resolved| {
resolved.get_semver_number().map(|semver_number| {
let range_str = resolved.get_semver_range().map(|r| r.unwrap()).unwrap_or_default();
let original_parts: Vec<&str> = semver_number.split('.').collect();
let formatted_version = match original_parts.len() {
1 if is_huge_minor && is_huge_patch => node_version.major.to_string(),
2 if is_huge_patch => {
format!("{}.{}", node_version.major, node_version.minor)
}
_ => version_str.clone(),
};
Self::new(&format!("workspace:{range_str}{formatted_version}"))
})
})
}
Self::Git(g) => {
g.semver_number.as_ref().map(|_| {
let range_str = g.semver_range.as_ref().map(|r| r.unwrap()).unwrap_or_else(String::new);
let original_parts: Vec<&str> = g.semver_number.as_ref().unwrap().split('.').collect();
let formatted_version = match original_parts.len() {
1 if is_huge_minor && is_huge_patch => node_version.major.to_string(),
2 if is_huge_patch => {
format!("{}.{}", node_version.major, node_version.minor)
}
_ => version_str.clone(),
};
Self::new(&format!("{}#{}{}", g.origin, range_str, formatted_version))
})
}
Self::Catalog(_)
| Self::ComplexSemver(_)
| Self::File(_)
| Self::Link(_)
| Self::Latest(_)
| Self::None
| Self::Tag(_)
| Self::Unsupported(_)
| Self::Url(_) => None,
}
}
}
impl Specifier {
fn has_comparable_version(&self) -> bool {
match self {
Self::Latest(_) => false,
Self::Alias(a) => !matches!(&*a.inner_specifier, Self::Latest(_)) && a.inner_specifier.get_node_version().is_some(),
_ => self.get_node_version().is_some(),
}
}
pub fn has_same_release_channel_as(&self, other: &Self) -> bool {
if !self.has_comparable_version() || !other.has_comparable_version() {
return false;
}
let (self_version, other_version) = match (self.get_node_version(), other.get_node_version()) {
(Some(self_v), Some(other_v)) => (self_v, other_v),
_ => return false,
};
let self_prerelease = &self_version.pre_release;
let other_prerelease = &other_version.pre_release;
if self_prerelease.is_empty() && other_prerelease.is_empty() {
return true;
}
if self_prerelease.is_empty() || other_prerelease.is_empty() {
return false;
}
match (self_prerelease.first(), other_prerelease.first()) {
(Some(self_first), Some(other_first)) => {
self_first.to_string() == other_first.to_string()
}
_ => false,
}
}
pub fn has_same_version_number_as(&self, other: &Self) -> bool {
if let (Self::WorkspaceProtocol(left), Self::WorkspaceProtocol(right)) = (self, other) {
if left.needs_resolution() && right.needs_resolution() {
return left.raw == right.raw;
}
}
if !self.has_comparable_version() || !other.has_comparable_version() {
return false;
}
match (self.get_node_version(), other.get_node_version()) {
(Some(left), Some(right)) => left == right,
_ => false,
}
}
pub fn is_eligible_update_for(&self, other: &Self, target: &UpdateTarget) -> bool {
if !self.has_comparable_version() || !other.has_comparable_version() {
return false;
}
let (self_version, other_version) = match (self.get_node_version(), other.get_node_version()) {
(Some(self_v), Some(other_v)) => (self_v, other_v),
_ => return false,
};
if self_version <= other_version {
return false;
}
match target {
UpdateTarget::Latest => true,
UpdateTarget::Minor => self_version.major == other_version.major,
UpdateTarget::Patch => {
self_version.major == other_version.major && self_version.minor == other_version.minor
}
}
}
pub fn is_workspace_protocol(&self) -> bool {
matches!(self, Self::WorkspaceProtocol(_))
}
pub fn is_link(&self) -> bool {
matches!(self, Self::Link(_))
}
pub fn is_catalog(&self) -> bool {
matches!(self, Self::Catalog(_))
}
pub fn satisfies_all(&self, others: &[Rc<Specifier>]) -> bool {
match self {
Specifier::None => false,
_ => {
if let Some(self_range) = self.get_node_range() {
return others.iter().all(|other| {
match other.get_node_range() {
Some(other_range) => self_range.allows_any(&other_range),
None => false,
}
});
}
if let Some(self_version) = self.get_node_version() {
return others.iter().all(|other| match other.get_node_range() {
Some(range) => range.satisfies(&self_version),
None => false,
});
}
false
}
}
}
}
impl Ord for Specifier {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
use std::cmp::Ordering;
let self_version = self.get_node_version();
let other_version = other.get_node_version();
match (self_version, other_version) {
(Some(self_ver), Some(other_ver)) => {
match self_ver.cmp(&other_ver) {
Ordering::Equal => {
let self_range = self.get_semver_range();
let other_range = other.get_semver_range();
match (self_range, other_range) {
(Some(self_r), Some(other_r)) => self_r.cmp(&other_r),
(Some(self_r), None) => {
self_r.get_greediness_ranking().cmp(&2)
}
(None, Some(other_r)) => {
2.cmp(&other_r.get_greediness_ranking())
}
(None, None) => Ordering::Equal,
}
}
ordering => ordering,
}
}
(Some(_), None) => Ordering::Greater,
(None, Some(_)) => Ordering::Less,
(None, None) => Ordering::Equal,
}
}
}
impl PartialOrd for Specifier {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Eq for Specifier {}