#[cfg(test)]
#[path = "instance_test.rs"]
mod instance_test;
use {
crate::{
dependency::UpdateUrl,
dependency_type::{DependencyType, Strategy},
instance_state::{
FixableInstance, InstanceState, InvalidInstance, SemverGroupAndVersionConflict, SuspectInstance, UnfixableInstance, ValidInstance,
},
package_json::PackageJson,
semver_range::SemverRange,
specifier::Specifier,
},
log::debug,
serde_json::Value,
std::{
cell::RefCell,
path::{Path, PathBuf},
rc::Rc,
},
};
pub type InstanceId = String;
#[derive(Debug)]
pub struct InstanceDescriptor {
pub dependency_type: DependencyType,
pub internal_name: String,
pub matches_cli_filter: bool,
pub name: String,
pub package: Rc<RefCell<PackageJson>>,
pub specifier: Rc<Specifier>,
}
#[derive(Debug)]
pub struct Instance {
pub descriptor: InstanceDescriptor,
pub expected_specifier: RefCell<Option<Rc<Specifier>>>,
pub id: InstanceId,
pub is_local: bool,
pub preferred_semver_range: Option<SemverRange>,
pub state: RefCell<InstanceState>,
}
impl Instance {
pub fn new(descriptor: InstanceDescriptor, preferred_semver_range: Option<SemverRange>) -> Instance {
let dependency_type_name = &descriptor.dependency_type.path;
let package_name = descriptor.package.borrow().name.clone();
let id = format!("{} in {} of {}", &descriptor.name, dependency_type_name, package_name);
let is_local = dependency_type_name == "/version";
Instance {
descriptor,
expected_specifier: RefCell::new(None),
id,
is_local,
preferred_semver_range,
state: RefCell::new(InstanceState::Unknown),
}
}
pub fn link_resolves_to_local_package(&self, local_instance: &Instance) -> bool {
if let Specifier::Link(link) = &*self.descriptor.specifier {
let consuming_package_path = &self.descriptor.package.borrow().file_path;
let consuming_package_dir = consuming_package_path.parent().unwrap_or_else(|| Path::new(""));
let link_path = link.raw.strip_prefix("link:").unwrap_or(&link.raw);
let resolved_link_path = consuming_package_dir.join(link_path);
let local_package_path = &local_instance.descriptor.package.borrow().file_path;
let local_package_dir = local_package_path.parent().unwrap_or_else(|| Path::new(""));
if let (Ok(resolved_canonical), Ok(local_canonical)) = (resolved_link_path.canonicalize(), local_package_dir.canonicalize()) {
resolved_canonical == local_canonical
} else {
let normalized_resolved = Self::normalize_path(&resolved_link_path);
let normalized_local = Self::normalize_path(local_package_dir);
normalized_resolved == normalized_local
}
} else {
false
}
}
fn normalize_path(path: &Path) -> PathBuf {
let mut components = Vec::new();
for component in path.components() {
match component {
std::path::Component::ParentDir => {
components.pop();
}
std::path::Component::CurDir => {}
_ => components.push(component),
}
}
components.iter().collect()
}
fn set_state(&self, state: InstanceState, expected_specifier: &Rc<Specifier>) -> &Self {
*self.state.borrow_mut() = state;
*self.expected_specifier.borrow_mut() = Some(Rc::clone(expected_specifier));
self
}
pub fn mark_valid(&self, state: ValidInstance, expected_specifier: &Rc<Specifier>) -> &Self {
self.set_state(InstanceState::Valid(state), expected_specifier)
}
pub fn mark_suspect(&self, state: SuspectInstance) -> &Self {
let specifier = Rc::clone(&self.descriptor.specifier);
self.set_state(InstanceState::Suspect(state), &specifier)
}
pub fn mark_fixable(&self, state: FixableInstance, expected_specifier: &Rc<Specifier>) -> &Self {
self.set_state(InstanceState::Invalid(InvalidInstance::Fixable(state)), expected_specifier)
}
pub fn mark_conflict(&self, state: SemverGroupAndVersionConflict) -> &Self {
let specifier = Rc::clone(&self.descriptor.specifier);
self.set_state(InstanceState::Invalid(InvalidInstance::Conflict(state)), &specifier)
}
pub fn mark_unfixable(&self, state: UnfixableInstance) -> &Self {
let specifier = Rc::clone(&self.descriptor.specifier);
self.set_state(InstanceState::Invalid(InvalidInstance::Unfixable(state)), &specifier)
}
pub fn is_valid(&self) -> bool {
self.state.borrow().is_valid()
}
pub fn is_invalid(&self) -> bool {
self.state.borrow().is_invalid()
}
pub fn is_suspect(&self) -> bool {
self.state.borrow().is_suspect()
}
pub fn is_fixable(&self) -> bool {
self.state.borrow().is_fixable()
}
pub fn is_banned(&self) -> bool {
self.state.borrow().is_banned()
}
pub fn is_unfixable(&self) -> bool {
self.state.borrow().is_unfixable()
}
pub fn is_outdated(&self) -> bool {
self.state.borrow().is_outdated()
}
pub fn has_missing_specifier(&self) -> bool {
matches!(&*self.descriptor.specifier, Specifier::None)
}
pub fn already_equals(&self, expected: &Rc<Specifier>) -> bool {
self.descriptor.specifier.get_raw() == expected.get_raw()
}
pub fn must_match_preferred_semver_range(&self) -> bool {
self.preferred_semver_range.is_some()
}
pub fn must_match_preferred_semver_range_which_is_not(&self, needed_range: &SemverRange) -> bool {
self.must_match_preferred_semver_range() && !self.preferred_semver_range_is(needed_range)
}
pub fn must_match_preferred_semver_range_which_differs_to(&self, other_specifier: &Rc<Specifier>) -> bool {
other_specifier
.get_semver_range()
.is_some_and(|range_of_other_specifier| self.must_match_preferred_semver_range_which_is_not(&range_of_other_specifier))
}
pub fn preferred_semver_range_is(&self, range: &SemverRange) -> bool {
self.preferred_semver_range.as_ref().map(|r| r == range).unwrap_or(false)
}
pub fn matches_preferred_semver_range(&self) -> bool {
self
.preferred_semver_range
.as_ref()
.map(|preferred_semver_range| self.descriptor.specifier.get_semver_range().as_ref() == Some(preferred_semver_range))
.unwrap_or(false)
}
pub fn get_specifier_with_preferred_semver_range(&self) -> Option<Rc<Specifier>> {
self
.preferred_semver_range
.as_ref()
.and_then(|preferred_semver_range| self.descriptor.specifier.with_range(preferred_semver_range))
}
pub fn get_update_url(&self) -> Option<UpdateUrl> {
if self.descriptor.matches_cli_filter && !self.is_local {
let internal_name = &self.descriptor.internal_name;
let actual_name = &self.descriptor.name;
let raw = self.descriptor.specifier.get_raw();
match &*self.descriptor.specifier {
Specifier::Alias(alias) => {
let aliased_name = &alias.name;
if !aliased_name.is_empty() {
if aliased_name.starts_with("@jsr/") {
Some(UpdateUrl {
internal_name: internal_name.clone(),
url: format!("https://npm.jsr.io/{aliased_name}"),
})
} else if aliased_name == actual_name {
Some(UpdateUrl {
internal_name: internal_name.clone(),
url: format!("https://registry.npmjs.org/{actual_name}"),
})
} else {
debug!("'{aliased_name}' in '{raw}' does not equal the instance name '{actual_name}', skipping update as this might create mismatches");
None
}
} else {
None
}
}
Specifier::Exact(_) | Specifier::Range(_) | Specifier::Major(_) | Specifier::Minor(_) | Specifier::Latest(_) => {
if actual_name.starts_with("@jsr/") {
Some(UpdateUrl {
internal_name: internal_name.clone(),
url: format!("https://npm.jsr.io/{actual_name}"),
})
} else {
Some(UpdateUrl {
internal_name: internal_name.clone(),
url: format!("https://registry.npmjs.org/{actual_name}"),
})
}
}
_ => None,
}
} else {
None
}
}
pub fn already_satisfies_all(&self, instances: &[Rc<Instance>]) -> bool {
!matches!(&*self.descriptor.specifier, Specifier::None)
&& self
.descriptor
.specifier
.satisfies_all(&instances.iter().map(|i| Rc::clone(&i.descriptor.specifier)).collect::<Vec<_>>())
}
pub fn already_has_same_minor_number_as_all(&self, instances: &[Rc<Instance>]) -> bool {
if matches!(&*self.descriptor.specifier, Specifier::None) {
return false;
}
match self.descriptor.specifier.get_node_version() {
None => false,
Some(a) => instances.iter().all(|other_instance| {
if matches!(&*other_instance.descriptor.specifier, Specifier::None) {
return false;
}
match other_instance.descriptor.specifier.get_node_version() {
None => false,
Some(b) => a.major == b.major && a.minor == b.minor,
}
}),
}
}
pub fn specifier_with_preferred_semver_range_will_satisfy(&self, other: &Rc<Specifier>) -> bool {
self
.get_specifier_with_preferred_semver_range()
.map(|specifier| {
if let (Some(spec_range), Some(other_range)) = (specifier.get_node_range(), other.get_node_range()) {
spec_range.allows_any(&other_range)
} else {
false
}
})
.unwrap_or(false)
}
pub fn remove(&self) {
match self.descriptor.dependency_type.strategy {
Strategy::NameAndVersionProps => {
let path_to_prop = &self.descriptor.dependency_type.path;
if let Some(parent_path) = path_to_prop.rfind('/') {
let parent_path = &path_to_prop[..parent_path];
let prop_name = &path_to_prop[parent_path.len() + 1..];
if let Some(Value::Object(obj)) = self.descriptor.package.borrow_mut().contents.borrow_mut().pointer_mut(parent_path) {
obj.remove(prop_name);
}
} else if path_to_prop == "/" {
debug!("Cannot remove root property for NameAndVersionProps");
}
}
Strategy::NamedVersionString => {
let path_to_prop = &self.descriptor.dependency_type.path;
if let Some(parent_path) = path_to_prop.rfind('/') {
let parent_path = &path_to_prop[..parent_path];
let prop_name = &path_to_prop[parent_path.len() + 1..];
if let Some(Value::Object(obj)) = self.descriptor.package.borrow_mut().contents.borrow_mut().pointer_mut(parent_path) {
obj.remove(prop_name);
}
} else if path_to_prop == "/" {
debug!("Cannot remove root property for NamedVersionString");
}
}
Strategy::UnnamedVersionString => {
let path_to_prop = &self.descriptor.dependency_type.path;
if let Some(parent_path) = path_to_prop.rfind('/') {
let parent_path = &path_to_prop[..parent_path];
let prop_name = &path_to_prop[parent_path.len() + 1..];
if let Some(Value::Object(obj)) = self.descriptor.package.borrow_mut().contents.borrow_mut().pointer_mut(parent_path) {
obj.remove(prop_name);
}
} else if path_to_prop == "/" {
debug!("Cannot remove root property for UnnamedVersionString");
}
}
Strategy::VersionsByName => {
let path_to_obj = &self.descriptor.dependency_type.path;
let name = &self.descriptor.name;
if let Some(Value::Object(obj)) = self.descriptor.package.borrow_mut().contents.borrow_mut().pointer_mut(path_to_obj) {
obj.remove(name);
}
}
Strategy::InvalidConfig => {
panic!("unrecognised strategy");
}
};
}
}