use {
crate::{
cli::Cli,
config::Config,
dependency_type::{DependencyType, Strategy},
instance::InstanceDescriptor,
package_json::PackageJson,
rcfile::Rcfile,
specifier::Specifier,
},
glob::glob,
log::debug,
serde::Deserialize,
serde_json::Value,
std::{
cell::RefCell,
collections::{HashMap, HashSet},
fs,
path::{Path, PathBuf},
rc::Rc,
},
};
#[derive(Debug)]
pub struct Packages {
pub all: Vec<Rc<RefCell<PackageJson>>>,
}
impl Packages {
pub fn new() -> Self {
Self { all: vec![] }
}
pub fn from_config(config: &Config) -> Self {
let file_paths = get_file_paths(config);
let mut packages = Self::new();
file_paths.iter().for_each(|file_path| {
if let Some(package_json) = PackageJson::from_file(file_path) {
packages.add_package(package_json);
}
});
packages
}
pub fn add_package(&mut self, package_json: PackageJson) -> &mut Self {
self.all.push(Rc::new(RefCell::new(package_json)));
self
}
pub fn get_by_name(&self, name: &str) -> Option<Rc<RefCell<PackageJson>>> {
self.all.iter().find(|package| package.borrow().name == name).map(Rc::clone)
}
pub fn get_local_versions(&self) -> HashMap<String, Rc<Specifier>> {
self
.all
.iter()
.filter_map(|package| -> Option<(String, Rc<Specifier>)> {
let package = package.borrow();
let name = package.get_prop("/name");
let version = package.get_prop("/version");
if let (Some(Value::String(name)), Some(Value::String(version))) = (name, version) {
Some((name, Specifier::new(&version)))
} else {
None
}
})
.collect()
}
pub fn get_all_instances<F>(&self, all_dependency_types: &Vec<DependencyType>, mut on_instance: F)
where
F: FnMut(InstanceDescriptor),
{
let _local_versions = self.get_local_versions();
for package in self.all.iter() {
for dependency_type in all_dependency_types {
match dependency_type.strategy {
Strategy::NameAndVersionProps => {
if let (Some(Value::String(name)), Some(Value::String(raw_specifier))) = (
package.borrow().get_prop(dependency_type.name_path.as_ref().unwrap()),
package.borrow().get_prop(&dependency_type.path).or_else(|| {
if dependency_type.name == "local" {
Some(Value::String("".to_string()))
} else {
None
}
}),
) {
on_instance(InstanceDescriptor {
dependency_type: dependency_type.clone(),
internal_name: name.to_string(),
matches_cli_filter: false,
name: name.to_string(),
package: Rc::clone(package),
specifier: Specifier::new(&raw_specifier),
});
}
}
Strategy::NamedVersionString => {
if let Some(Value::String(specifier)) = package.borrow().get_prop(&dependency_type.path) {
if let Some((name, raw_specifier)) = specifier.split_once('@') {
on_instance(InstanceDescriptor {
dependency_type: dependency_type.clone(),
internal_name: name.to_string(),
matches_cli_filter: false,
name: name.to_string(),
package: Rc::clone(package),
specifier: Specifier::new(raw_specifier),
});
}
}
}
Strategy::UnnamedVersionString => {
if let Some(Value::String(raw_specifier)) = package.borrow().get_prop(&dependency_type.path) {
on_instance(InstanceDescriptor {
dependency_type: dependency_type.clone(),
internal_name: dependency_type.name.clone(),
matches_cli_filter: false,
name: dependency_type.name.clone(),
package: Rc::clone(package),
specifier: Specifier::new(&raw_specifier),
});
}
}
Strategy::VersionsByName => {
if let Some(Value::Object(versions_by_name)) = package.borrow().get_prop(&dependency_type.path) {
for (name, raw_specifier) in versions_by_name {
if let Value::String(raw_specifier) = raw_specifier {
on_instance(InstanceDescriptor {
dependency_type: dependency_type.clone(),
internal_name: name.to_string(),
matches_cli_filter: false,
name: name.to_string(),
package: Rc::clone(package),
specifier: Specifier::new(&raw_specifier),
});
}
}
}
}
Strategy::InvalidConfig => {
panic!("unrecognised strategy");
}
};
}
}
}
}
pub fn normalize_pattern(mut pattern: String) -> String {
let negated = pattern.starts_with('!');
if negated {
pattern.remove(0);
}
let normalized = pattern.replace('\\', "/");
if negated {
if normalized.contains("package.json") {
format!("!{normalized}")
} else {
format!("!{normalized}/package.json")
}
} else if normalized.contains("package.json") {
normalized
} else {
format!("{normalized}/package.json")
}
}
fn get_file_paths(config: &Config) -> Vec<PathBuf> {
let all_patterns = get_source_patterns(config);
let (negatives, positives): (Vec<_>, Vec<_>) = all_patterns.iter().partition(|p| p.starts_with('!'));
let to_absolute = |pattern: &str| -> String {
if PathBuf::from(pattern).is_absolute() {
pattern.to_string()
} else {
config.cli.cwd.join(pattern).to_str().unwrap().to_string()
}
};
let resolve_glob = |pattern: &str| -> Vec<PathBuf> {
glob(pattern)
.map_err(|err| debug!("Invalid glob pattern '{pattern}': {err}"))
.into_iter()
.flat_map(|paths| paths.filter_map(Result::ok))
.collect()
};
let included: Vec<PathBuf> = positives
.iter()
.map(|p| to_absolute(p))
.flat_map(|pattern| resolve_glob(&pattern))
.filter(|path| !path.to_string_lossy().contains("node_modules"))
.collect();
if negatives.is_empty() {
return included;
}
let excluded: HashSet<PathBuf> = negatives
.iter()
.map(|p| to_absolute(p.trim_start_matches('!')))
.flat_map(|pattern| resolve_glob(&pattern))
.collect();
included.into_iter().filter(|path| !excluded.contains(path)).collect()
}
fn get_source_patterns(config: &Config) -> Vec<String> {
get_cli_patterns(&config.cli)
.or_else(|| {
debug!("No --source patterns provided");
None
})
.or_else(|| get_rcfile_patterns(&config.rcfile))
.or_else(|| {
debug!("No .source patterns in Rcfile");
None
})
.or_else(|| {
get_npm_and_yarn_patterns(&config.cli.cwd)
.or_else(|| {
debug!("No .workspaces.packages or workspaces patterns in package.json");
None
})
.or_else(|| get_pnpm_patterns(&config.cli.cwd))
.or_else(|| {
debug!("No .packages patterns in pnpm-workspace.yaml");
None
})
.or_else(|| get_lerna_patterns(&config.cli.cwd))
.or_else(|| {
debug!("No .packages patterns in lerna.json");
None
})
.as_ref()
.map(|patterns| {
let mut patterns = patterns.clone();
patterns.push("package.json".to_string());
patterns
})
})
.map(|patterns| patterns.into_iter().map(normalize_pattern).collect())
.or_else(get_default_patterns)
.unwrap()
}
fn get_cli_patterns(cli: &Cli) -> Option<Vec<String>> {
if cli.source_patterns.is_empty() {
None
} else {
Some(cli.source_patterns.clone())
}
}
fn get_rcfile_patterns(rcfile: &Rcfile) -> Option<Vec<String>> {
if rcfile.source.is_empty() {
None
} else {
Some(rcfile.source.clone())
}
}
fn get_pnpm_patterns(cwd: &Path) -> Option<Vec<String>> {
let file_path = cwd.join("pnpm-workspace.yaml");
let json = fs::read_to_string(&file_path).ok()?;
let pnpm_workspace: SourcesUnderPackages = serde_yaml::from_str(&json).ok()?;
pnpm_workspace.packages
}
#[derive(Debug, Deserialize)]
struct SourcesUnderPackages {
packages: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
struct SourcesUnderWorkspacesDotPackages {
workspaces: SourcesUnderPackages,
}
#[derive(Debug, Deserialize)]
struct SourcesUnderWorkspaces {
workspaces: Option<Vec<String>>,
}
fn get_npm_and_yarn_patterns(cwd: &Path) -> Option<Vec<String>> {
let file_path = cwd.join("package.json");
let json = fs::read_to_string(&file_path).ok()?;
serde_json::from_str::<SourcesUnderWorkspacesDotPackages>(&json)
.ok()
.and_then(|package_json| package_json.workspaces.packages)
.or_else(|| {
serde_json::from_str::<SourcesUnderWorkspaces>(&json)
.ok()
.and_then(|package_json| package_json.workspaces)
})
}
fn get_lerna_patterns(cwd: &Path) -> Option<Vec<String>> {
let file_path = cwd.join("lerna.json");
let json = fs::read_to_string(&file_path).ok()?;
let lerna_json: SourcesUnderPackages = serde_json::from_str(&json).ok()?;
lerna_json.packages
}
fn get_default_patterns() -> Option<Vec<String>> {
debug!("Using default source patterns");
Some(vec![String::from("package.json"), String::from("packages/*/package.json")])
}