use std::borrow::Borrow;
use std::collections::HashMap;
use std::ffi::OsString;
use std::fmt::Display;
use std::path::{Path, PathBuf};
use pathdiff::diff_paths;
use crate::configuration_file::{ConfigurationFile, WriteError};
use crate::io::FromFileError;
use crate::monorepo_manifest::{EnumeratePackageManifestsError, MonorepoManifest};
use crate::out_of_date_project_references::{
AllOutOfDateTypescriptConfig, OutOfDatePackageProjectReferences,
OutOfDateParentProjectReferences, OutOfDateTypescriptConfig,
};
use crate::package_manifest::PackageManifest;
use crate::types::{Directory, PackageName};
use crate::typescript_config::{
TypescriptConfig, TypescriptParentProjectReference, TypescriptProjectReference,
};
#[derive(Debug)]
#[non_exhaustive]
pub struct LinkError {
pub kind: LinkErrorKind,
}
impl Display for LinkError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "error linking TypeScript project references")
}
}
impl std::error::Error for LinkError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.kind {
LinkErrorKind::EnumeratePackageManifests(err) => Some(err),
LinkErrorKind::FromFile(err) => Some(err),
LinkErrorKind::Write(err) => Some(err),
LinkErrorKind::InvalidUtf8(err) => Some(err),
}
}
}
impl From<EnumeratePackageManifestsError> for LinkError {
fn from(err: EnumeratePackageManifestsError) -> Self {
Self {
kind: LinkErrorKind::EnumeratePackageManifests(err),
}
}
}
impl From<FromFileError> for LinkError {
fn from(err: FromFileError) -> Self {
Self {
kind: LinkErrorKind::FromFile(err),
}
}
}
impl From<WriteError> for LinkError {
fn from(err: WriteError) -> Self {
Self {
kind: LinkErrorKind::Write(err),
}
}
}
impl From<InvalidUtf8Error> for LinkError {
fn from(err: InvalidUtf8Error) -> Self {
Self {
kind: LinkErrorKind::InvalidUtf8(err),
}
}
}
#[derive(Debug)]
pub enum LinkErrorKind {
#[non_exhaustive]
EnumeratePackageManifests(EnumeratePackageManifestsError),
#[non_exhaustive]
FromFile(FromFileError),
#[non_exhaustive]
InvalidUtf8(InvalidUtf8Error),
#[non_exhaustive]
Write(WriteError),
}
#[derive(Debug)]
pub struct InvalidUtf8Error(OsString);
impl Display for InvalidUtf8Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "path cannot be expressed as UTF-8: {:?}", self.0)
}
}
impl std::error::Error for InvalidUtf8Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
}
fn key_children_by_parent<M>(
mut accumulator: HashMap<Directory, Vec<String>>,
package_manifest: M,
) -> Result<HashMap<Directory, Vec<String>>, InvalidUtf8Error>
where
M: Borrow<PackageManifest>,
{
let mut path_so_far = PathBuf::new();
for component in package_manifest.borrow().directory().iter() {
let children = accumulator
.entry(Directory::unchecked_from_path(path_so_far.clone()))
.or_default();
let new_child = component
.to_str()
.map(ToOwned::to_owned)
.ok_or_else(|| InvalidUtf8Error(component.to_owned()))?;
if !children.contains(&new_child) {
children.push(new_child);
}
path_so_far.push(component);
}
Ok(accumulator)
}
fn create_project_references(mut children: Vec<String>) -> Vec<TypescriptProjectReference> {
children.sort_unstable();
children
.into_iter()
.map(|path| TypescriptProjectReference { path })
.collect()
}
fn link_children_packages(
root: &Directory,
package_manifests_by_package_name: &HashMap<PackageName, PackageManifest>,
) -> Result<(), LinkError> {
out_of_date_parent_project_references(root, package_manifests_by_package_name)?.try_for_each(
|maybe_parent_project_references| -> Result<(), LinkError> {
let OutOfDateParentProjectReferences {
mut tsconfig,
desired_references,
} = maybe_parent_project_references?;
tsconfig.contents.references = desired_references;
Ok(TypescriptParentProjectReference::write(root, tsconfig)?)
},
)
}
fn link_package_dependencies(
root: &Directory,
package_manifests_by_package_name: &HashMap<PackageName, PackageManifest>,
) -> Result<(), LinkError> {
out_of_date_package_project_references(root, package_manifests_by_package_name)?
.map(
|maybe_package_project_references| -> Result<Option<_>, FromFileError> {
let OutOfDatePackageProjectReferences {
mut tsconfig,
desired_references,
} = maybe_package_project_references?;
let current_project_references = &tsconfig
.contents
.get("references")
.map(|value| {
serde_json::from_value::<Vec<TypescriptProjectReference>>(value.clone())
.expect("value starting as JSON should be deserializable")
})
.unwrap_or_default();
let needs_update = !current_project_references.eq(&desired_references);
if !needs_update {
return Ok(None);
}
tsconfig.contents.insert(
String::from("references"),
serde_json::to_value(desired_references).expect(
"should be able to express desired TypeScript project references as JSON",
),
);
Ok(Some(tsconfig))
},
)
.filter_map(Result::transpose)
.map(|maybe_tsconfig| -> Result<(), LinkError> {
let tsconfig = maybe_tsconfig?;
Ok(TypescriptConfig::write(root, tsconfig)?)
})
.collect::<Result<Vec<_>, _>>()?;
Ok(())
}
pub fn modify<P>(root: P) -> Result<(), LinkError>
where
P: AsRef<Path>,
{
fn inner(root: &Path) -> Result<(), LinkError> {
let lerna_manifest = MonorepoManifest::from_directory(root)?;
let package_manifests_by_package_name =
lerna_manifest.package_manifests_by_package_name()?;
link_children_packages(&lerna_manifest.root, &package_manifests_by_package_name)?;
link_package_dependencies(&lerna_manifest.root, &package_manifests_by_package_name)?;
Ok(())
}
inner(root.as_ref())
}
#[derive(Debug)]
#[non_exhaustive]
pub struct LinkLintError {
pub kind: LinkLintErrorKind,
}
impl Display for LinkLintError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.kind {
LinkLintErrorKind::ProjectReferencesOutOfDate(out_of_date_references) => {
writeln!(f, "TypeScript project references are not up-to-date")?;
writeln!(f, "{}", out_of_date_references)
}
_ => write!(f, "error linking TypeScript project references"),
}
}
}
impl std::error::Error for LinkLintError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.kind {
LinkLintErrorKind::EnumeratePackageManifests(err) => Some(err),
LinkLintErrorKind::FromFile(err) => Some(err),
LinkLintErrorKind::ProjectReferencesOutOfDate(_) => None,
LinkLintErrorKind::InvalidUtf8(err) => Some(err),
}
}
}
impl From<EnumeratePackageManifestsError> for LinkLintError {
fn from(err: EnumeratePackageManifestsError) -> Self {
Self {
kind: LinkLintErrorKind::EnumeratePackageManifests(err),
}
}
}
impl From<FromFileError> for LinkLintError {
fn from(err: FromFileError) -> Self {
Self {
kind: LinkLintErrorKind::FromFile(err),
}
}
}
impl From<InvalidUtf8Error> for LinkLintError {
fn from(err: InvalidUtf8Error) -> Self {
Self {
kind: LinkLintErrorKind::InvalidUtf8(err),
}
}
}
impl From<AllOutOfDateTypescriptConfig> for LinkLintError {
fn from(err: AllOutOfDateTypescriptConfig) -> Self {
Self {
kind: LinkLintErrorKind::ProjectReferencesOutOfDate(err),
}
}
}
#[derive(Debug)]
pub enum LinkLintErrorKind {
#[non_exhaustive]
EnumeratePackageManifests(EnumeratePackageManifestsError),
#[non_exhaustive]
FromFile(FromFileError),
#[non_exhaustive]
InvalidUtf8(InvalidUtf8Error),
#[non_exhaustive]
ProjectReferencesOutOfDate(AllOutOfDateTypescriptConfig),
}
fn out_of_date_parent_project_references<'a>(
root: &'a Directory,
package_manifests_by_package_name: &'a HashMap<PackageName, PackageManifest>,
) -> Result<
impl Iterator<Item = Result<OutOfDateParentProjectReferences, FromFileError>> + 'a,
InvalidUtf8Error,
> {
let iter = package_manifests_by_package_name
.values()
.try_fold(HashMap::default(), key_children_by_parent)?
.into_iter()
.map(move |(directory, children)| {
let desired_references = create_project_references(children);
let tsconfig = TypescriptParentProjectReference::from_directory(&root, directory)?;
let current_project_references = &tsconfig.contents.references;
let needs_update = !current_project_references.eq(&desired_references);
Ok(match needs_update {
true => Some(OutOfDateParentProjectReferences {
tsconfig,
desired_references,
}),
false => None,
})
})
.filter_map(Result::transpose);
Ok(iter)
}
fn out_of_date_package_project_references<'a>(
root: &'a Directory,
package_manifests_by_package_name: &'a HashMap<PackageName, PackageManifest>,
) -> Result<
impl Iterator<Item = Result<OutOfDatePackageProjectReferences, FromFileError>> + 'a,
InvalidUtf8Error,
> {
let iter = package_manifests_by_package_name
.values()
.map(move |package_manifest| {
let package_directory = package_manifest.directory();
let tsconfig = TypescriptConfig::from_directory(&root, package_directory.to_owned())?;
let internal_dependencies =
package_manifest.internal_dependencies_iter(&package_manifests_by_package_name);
let desired_references: Vec<TypescriptProjectReference> = {
let mut typescript_project_references: Vec<String> = internal_dependencies
.into_iter()
.map(|dependency| {
diff_paths(dependency.directory(), package_manifest.directory())
.expect(
"Unable to calculate a relative path to dependency from package",
)
.to_str()
.expect("Path not valid UTF-8 encoded")
.to_string()
})
.collect::<Vec<_>>();
typescript_project_references.sort_unstable();
typescript_project_references
.into_iter()
.map(|path| TypescriptProjectReference { path })
.collect()
};
let current_project_references = &tsconfig
.contents
.get("references")
.map(|value| {
serde_json::from_value::<Vec<TypescriptProjectReference>>(value.clone())
.expect("value starting as JSON should be serializable")
})
.unwrap_or_default();
let needs_update = !current_project_references.eq(&desired_references);
Ok(match needs_update {
true => Some(OutOfDatePackageProjectReferences {
tsconfig,
desired_references,
}),
false => None,
})
})
.filter_map(Result::transpose);
Ok(iter)
}
pub fn lint<P>(root: P) -> Result<(), LinkLintError>
where
P: AsRef<Path>,
{
fn inner(root: &Path) -> Result<(), LinkLintError> {
let monorepo_manifest = MonorepoManifest::from_directory(root)?;
let package_manifests_by_package_name =
monorepo_manifest.package_manifests_by_package_name()?;
let is_children_link_success = out_of_date_parent_project_references(
&monorepo_manifest.root,
&package_manifests_by_package_name,
)?
.map(
|result| -> Result<OutOfDateTypescriptConfig, FromFileError> { result.map(Into::into) },
);
let is_dependencies_link_success = out_of_date_package_project_references(
&monorepo_manifest.root,
&package_manifests_by_package_name,
)?
.map(
|result| -> Result<OutOfDateTypescriptConfig, FromFileError> { result.map(Into::into) },
);
let lint_issues: AllOutOfDateTypescriptConfig = is_children_link_success
.chain(is_dependencies_link_success)
.collect::<Result<_, _>>()?;
match lint_issues.is_empty() {
true => Ok(()),
false => Err(lint_issues)?,
}
}
inner(root.as_ref())
}