use {
crate::{
cli::Cli,
config::Config,
dependency_type::{DependencyType, Strategy},
instance::InstanceDescriptor,
package_json::PackageJson,
rcfile::Rcfile,
specifier::{basic_semver::BasicSemver, Specifier},
},
glob::glob,
log::debug,
serde::Deserialize,
serde_json::Value,
std::{
cell::RefCell,
collections::HashMap,
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, BasicSemver> {
self
.all
.iter()
.filter_map(|package| -> Option<(String, BasicSemver)> {
let name = package.borrow().get_prop("/name");
let version = package.borrow().get_prop("/version");
if let (Some(Value::String(name)), Some(Value::String(version))) = (name, version) {
BasicSemver::new(&version.to_string()).map(|semver| (name.to_string(), semver))
} 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, local_versions.get(&name)),
});
}
}
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, local_versions.get(name)),
});
}
}
}
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, local_versions.get(&dependency_type.name)),
});
}
}
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, local_versions.get(&name)),
});
}
}
}
}
Strategy::InvalidConfig => {
panic!("unrecognised strategy");
}
};
}
}
}
}
fn get_file_paths(config: &Config) -> Vec<PathBuf> {
get_source_patterns(config)
.iter()
.map(|pattern| {
if PathBuf::from(pattern).is_absolute() {
pattern.clone()
} else {
config.cli.cwd.join(pattern).to_str().unwrap().to_string()
}
})
.flat_map(|pattern| glob(&pattern).ok())
.flat_map(|paths| {
paths.filter_map(Result::ok).fold(vec![], |mut paths, path| {
paths.push(path.clone());
paths
})
})
.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(|pattern| {
if pattern.contains("package.json") {
pattern
} else {
format!("{pattern}/package.json")
}
})
.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")])
}