use std::collections::{HashMap, HashSet, VecDeque};
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use indoc::formatdoc;
use serde::{Deserialize, Serialize};
use crate::configuration_file::ConfigurationFile;
use crate::io::read_json_from_file;
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PackageManifestFile {
pub name: String,
pub version: String,
#[serde(flatten)]
pub extra_fields: serde_json::Value,
}
#[derive(Clone, Debug)]
pub struct PackageManifest {
monorepo_root: PathBuf,
directory: PathBuf,
pub contents: PackageManifestFile,
}
pub enum DependencyGroup {
Dependencies,
DevDependencies,
OptionalDependencies,
PeerDependencies,
}
impl DependencyGroup {
pub const VALUES: [Self; 4] = [
Self::Dependencies,
Self::DevDependencies,
Self::OptionalDependencies,
Self::PeerDependencies,
];
}
impl ConfigurationFile<PackageManifest> for PackageManifest {
const FILENAME: &'static str = "package.json";
fn from_directory(monorepo_root: &Path, directory: &Path) -> Result<PackageManifest> {
let filename = monorepo_root.join(directory).join(Self::FILENAME);
let manifest_contents: PackageManifestFile =
read_json_from_file(&filename).with_context(|| {
formatdoc!(
"
Unexpected contents in {:?}
I'm trying to parse the following properties and values out
of this package.json file:
- name: string
- version: string
and if any of the following values are present, I expect
them to be a JSON object with string keys and string values:
- dependencies
- devDependencies
- optionalDependencies
- peerDependencies
",
filename
)
})?;
Ok(PackageManifest {
monorepo_root: monorepo_root.to_owned(),
directory: directory.to_owned(),
contents: manifest_contents,
})
}
fn directory(&self) -> PathBuf {
self.directory.to_owned()
}
fn path(&self) -> PathBuf {
self.directory.join(Self::FILENAME)
}
fn write(&self) -> Result<()> {
let file = File::create(
self.monorepo_root
.join(&self.directory)
.join(Self::FILENAME),
)?;
let mut writer = BufWriter::new(file);
serde_json::to_writer_pretty(&mut writer, &self.contents)?;
writer.write_all(b"\n")?;
writer.flush()?;
Ok(())
}
}
impl AsRef<PackageManifest> for PackageManifest {
fn as_ref(&self) -> &Self {
self
}
}
impl PackageManifest {
pub fn get_dependency_version<S>(&self, dependency: S) -> Option<String>
where
S: AsRef<str>,
{
static DEPENDENCY_GROUPS: &[&str] = &[
"dependencies",
"devDependencies",
"optionalDependencies",
"peerDependencies",
];
DEPENDENCY_GROUPS
.iter()
.filter_map(|dependency_group| {
self.contents
.extra_fields
.get(dependency_group)?
.as_object()
})
.filter_map(|dependency_group_value| {
dependency_group_value
.get(dependency.as_ref())
.and_then(|version_value| version_value.as_str().map(|a| a.to_owned()))
})
.take(1)
.next()
}
pub fn internal_dependencies_iter<'a>(
&'a self,
package_manifests_by_package_name: &'a HashMap<String, PackageManifest>,
) -> impl Iterator<Item = &'a PackageManifest> {
static DEPENDENCY_GROUPS: &[&str] = &[
"dependencies",
"devDependencies",
"optionalDependencies",
"peerDependencies",
];
DEPENDENCY_GROUPS
.iter()
.filter_map(|dependency_group| {
self.contents
.extra_fields
.get(dependency_group)?
.as_object()
})
.flat_map(|dependency_group_value| dependency_group_value.keys())
.filter_map(|package_name| package_manifests_by_package_name.get(package_name))
}
pub fn transitive_internal_dependency_package_names_exclusive<'a>(
&self,
package_manifest_by_package_name: &'a HashMap<String, PackageManifest>,
) -> Vec<&'a PackageManifest> {
let mut seen_package_names = HashSet::new();
let mut internal_dependencies = HashSet::new();
let mut to_visit_package_manifests = VecDeque::new();
to_visit_package_manifests.push_back(self);
while let Some(current_manifest) = to_visit_package_manifests.pop_front() {
seen_package_names.insert(¤t_manifest.contents.name);
for dependency in
current_manifest.internal_dependencies_iter(package_manifest_by_package_name)
{
internal_dependencies.insert(dependency.contents.name.to_owned());
if !seen_package_names.contains(&dependency.contents.name) {
to_visit_package_manifests.push_back(dependency);
}
}
}
internal_dependencies
.iter()
.map(|dependency_package_name| {
package_manifest_by_package_name
.get(dependency_package_name)
.unwrap()
})
.collect()
}
pub fn get_dependency_group_mut(
&mut self,
group: &DependencyGroup,
) -> Option<&mut serde_json::Map<String, serde_json::Value>> {
let group_index = match group {
DependencyGroup::Dependencies => "dependencies",
DependencyGroup::DevDependencies => "devDependencies",
DependencyGroup::OptionalDependencies => "optionalDependencies",
DependencyGroup::PeerDependencies => "peerDependencies",
};
self.contents
.extra_fields
.get_mut(group_index)
.and_then(serde_json::Value::as_object_mut)
}
pub fn npm_pack_file_basename(&self) -> String {
format!(
"{}-{}.tgz",
self.contents.name.trim_start_matches('@').replace('/', "-"),
&self.contents.version,
)
}
pub fn unscoped_package_name(&self) -> &str {
match &self.contents.name.rsplit_once('/') {
Some((_scope, name)) => name,
None => &self.contents.name,
}
}
}