use crate::config::PackageToolsConfig;
use crate::error::{VersionError, VersionResult};
use crate::types::{Changeset, DependencyType, PackageInfo, VersioningStrategy};
use crate::version::application::ApplyResult;
use crate::version::graph::DependencyGraph;
use crate::version::propagation::DependencyPropagator;
use crate::version::resolution::{PackageUpdate, VersionResolution, resolve_versions};
use package_json::PackageJson;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use sublime_standard_tools::config::MonorepoConfig;
use sublime_standard_tools::filesystem::{AsyncFileSystem, FileSystemManager};
use sublime_standard_tools::monorepo::{MonorepoDetector, MonorepoDetectorTrait, WorkspacePackage};
#[derive(Debug, Clone)]
pub struct VersionResolver<F: AsyncFileSystem = FileSystemManager> {
workspace_root: PathBuf,
strategy: VersioningStrategy,
fs: F,
config: PackageToolsConfig,
is_monorepo: bool,
}
impl VersionResolver<FileSystemManager> {
pub async fn new(workspace_root: PathBuf, config: PackageToolsConfig) -> VersionResult<Self> {
let fs = FileSystemManager::new();
Self::with_filesystem(workspace_root, fs, config).await
}
}
impl<F: AsyncFileSystem + Clone + Send + Sync + 'static> VersionResolver<F> {
pub async fn with_filesystem(
workspace_root: PathBuf,
fs: F,
config: PackageToolsConfig,
) -> VersionResult<Self> {
if !fs.exists(&workspace_root).await {
return Err(VersionError::InvalidWorkspaceRoot {
path: workspace_root,
reason: "Path does not exist".to_string(),
});
}
let monorepo_config = Self::build_monorepo_config(&config);
let is_monorepo = Self::detect_monorepo(&workspace_root, &fs, &monorepo_config).await?;
let strategy = match config.version.strategy {
crate::config::VersioningStrategy::Independent => VersioningStrategy::Independent,
crate::config::VersioningStrategy::Unified => VersioningStrategy::Unified,
};
Ok(Self { workspace_root, strategy, fs, config, is_monorepo })
}
#[must_use]
pub fn is_monorepo(&self) -> bool {
self.is_monorepo
}
#[must_use]
pub fn workspace_root(&self) -> &Path {
&self.workspace_root
}
#[must_use]
pub fn strategy(&self) -> VersioningStrategy {
self.strategy
}
#[must_use]
pub fn filesystem(&self) -> &F {
&self.fs
}
#[must_use]
pub fn config(&self) -> &PackageToolsConfig {
&self.config
}
pub async fn discover_packages(&self) -> VersionResult<Vec<PackageInfo>> {
if self.is_monorepo {
self.discover_monorepo_packages().await
} else {
self.discover_single_package().await
}
}
#[must_use]
pub fn build_monorepo_config(config: &PackageToolsConfig) -> MonorepoConfig {
let mut monorepo_config = config.standard_config.monorepo.clone();
if let Some(ref workspace) = config.workspace
&& !workspace.patterns.is_empty()
{
for pattern in &workspace.patterns {
if !monorepo_config.workspace_patterns.contains(pattern) {
monorepo_config.workspace_patterns.push(pattern.clone());
}
}
}
monorepo_config
}
async fn detect_monorepo(
workspace_root: &Path,
fs: &F,
monorepo_config: &MonorepoConfig,
) -> VersionResult<bool> {
let detector =
MonorepoDetector::with_filesystem_and_config(fs.clone(), monorepo_config.clone());
let monorepo_kind = detector.is_monorepo_root(workspace_root).await.map_err(|e| {
VersionError::FileSystemError {
path: workspace_root.to_path_buf(),
reason: format!("Failed to detect monorepo: {}", e),
}
})?;
Ok(monorepo_kind.is_some())
}
async fn discover_monorepo_packages(&self) -> VersionResult<Vec<PackageInfo>> {
let monorepo_config = Self::build_monorepo_config(&self.config);
let detector =
MonorepoDetector::with_filesystem_and_config(self.fs.clone(), monorepo_config);
let monorepo = detector.detect_monorepo(&self.workspace_root).await.map_err(|e| {
VersionError::FileSystemError {
path: self.workspace_root.clone(),
reason: format!("Failed to detect monorepo: {}", e),
}
})?;
let workspace_packages = monorepo.packages();
if workspace_packages.is_empty() {
return Err(VersionError::PackageNotFound {
name: "any package".to_string(),
workspace_root: self.workspace_root.clone(),
});
}
let mut packages = Vec::with_capacity(workspace_packages.len());
for workspace_package in workspace_packages {
let package_info = self
.load_package_info(
&workspace_package.absolute_path,
Some(workspace_package.clone()),
)
.await?;
packages.push(package_info);
}
Ok(packages)
}
async fn discover_single_package(&self) -> VersionResult<Vec<PackageInfo>> {
let package_info = self.load_package_info(&self.workspace_root, None).await?;
Ok(vec![package_info])
}
async fn load_package_info(
&self,
package_path: &Path,
workspace_package: Option<WorkspacePackage>,
) -> VersionResult<PackageInfo> {
let package_json_path = package_path.join("package.json");
let content = self.fs.read_file_string(&package_json_path).await.map_err(|e| {
VersionError::PackageJsonError {
path: package_json_path.clone(),
reason: format!("Failed to read file: {}", e),
}
})?;
let package_json: package_json::PackageJson =
serde_json::from_str(&content).map_err(|e| VersionError::PackageJsonError {
path: package_json_path.clone(),
reason: format!("Failed to parse JSON: {}", e),
})?;
Ok(PackageInfo::new(package_json, workspace_package, package_path.to_path_buf()))
}
pub async fn resolve_versions(
&self,
changeset: &Changeset,
) -> VersionResult<VersionResolution> {
let package_list = self.discover_packages().await?;
let (graph, circular_deps) = if self.config.dependency.propagation_bump != "none" {
let g = DependencyGraph::from_packages(&package_list)?;
let cycles = g.detect_cycles();
(Some(g), cycles)
} else {
(None, Vec::new())
};
let mut packages = HashMap::new();
for package_info in package_list {
let name = package_info.name().to_string();
packages.insert(name, package_info);
}
let mut resolution = resolve_versions(changeset, &packages, self.strategy).await?;
resolution.circular_dependencies = circular_deps;
if let Some(graph) = graph {
let propagator = DependencyPropagator::new(&graph, &packages, &self.config.dependency);
propagator.propagate(&mut resolution)?;
}
Ok(resolution)
}
pub async fn resolve_versions_with_prerelease(
&self,
changeset: &Changeset,
prerelease_config: Option<&crate::types::prerelease::PrereleaseConfig>,
) -> VersionResult<VersionResolution> {
let package_list = self.discover_packages().await?;
let (graph, circular_deps) = if self.config.dependency.propagation_bump != "none" {
let g = DependencyGraph::from_packages(&package_list)?;
let cycles = g.detect_cycles();
(Some(g), cycles)
} else {
(None, Vec::new())
};
let mut packages = HashMap::new();
for package_info in package_list {
let name = package_info.name().to_string();
packages.insert(name, package_info);
}
let mut resolution = crate::version::resolution::resolve_versions_with_prerelease(
changeset,
&packages,
self.strategy,
prerelease_config,
)
.await?;
resolution.circular_dependencies = circular_deps;
if let Some(graph) = graph {
let propagator = DependencyPropagator::new(&graph, &packages, &self.config.dependency);
propagator.propagate(&mut resolution)?;
}
Ok(resolution)
}
pub async fn resolve_versions_with_prerelease_auto(
&self,
changeset: &Changeset,
prerelease_tag: Option<&str>,
) -> VersionResult<VersionResolution> {
let package_list = self.discover_packages().await?;
let (graph, circular_deps) = if self.config.dependency.propagation_bump != "none" {
let g = DependencyGraph::from_packages(&package_list)?;
let cycles = g.detect_cycles();
(Some(g), cycles)
} else {
(None, Vec::new())
};
let mut packages = HashMap::new();
for package_info in package_list {
let name = package_info.name().to_string();
packages.insert(name, package_info);
}
let mut resolution = crate::version::resolution::resolve_versions_with_prerelease_auto(
changeset,
&packages,
self.strategy,
prerelease_tag,
)
.await?;
resolution.circular_dependencies = circular_deps;
if let Some(graph) = graph {
let propagator = DependencyPropagator::new(&graph, &packages, &self.config.dependency);
propagator.propagate(&mut resolution)?;
}
Ok(resolution)
}
pub async fn apply_versions(
&self,
changeset: &Changeset,
dry_run: bool,
) -> VersionResult<ApplyResult> {
let resolution = self.resolve_versions(changeset).await?;
if dry_run {
return Ok(ApplyResult::new(true, resolution, vec![]));
}
let package_list = self.discover_packages().await?;
let mut packages = HashMap::new();
for package_info in package_list {
let name = package_info.name().to_string();
packages.insert(name, package_info);
}
let mut modified_files = Vec::new();
let mut backups: Vec<(PathBuf, Vec<u8>)> = Vec::new();
let apply_result = self
.apply_updates_to_packages(&resolution, &packages, &mut modified_files, &mut backups)
.await;
if let Err(e) = apply_result {
self.restore_backups(&backups).await?;
return Err(e);
}
Ok(ApplyResult::new(false, resolution, modified_files))
}
pub async fn apply_from_resolution(
&self,
resolution: VersionResolution,
dry_run: bool,
) -> VersionResult<ApplyResult> {
if dry_run {
return Ok(ApplyResult::new(true, resolution, vec![]));
}
let package_list = self.discover_packages().await?;
let mut packages = HashMap::new();
for package_info in package_list {
let name = package_info.name().to_string();
packages.insert(name, package_info);
}
let mut modified_files = Vec::new();
let mut backups: Vec<(PathBuf, Vec<u8>)> = Vec::new();
let apply_result = self
.apply_updates_to_packages(&resolution, &packages, &mut modified_files, &mut backups)
.await;
if let Err(e) = apply_result {
self.restore_backups(&backups).await?;
return Err(e);
}
Ok(ApplyResult::new(false, resolution, modified_files))
}
async fn apply_updates_to_packages(
&self,
resolution: &VersionResolution,
packages: &HashMap<String, PackageInfo>,
modified_files: &mut Vec<PathBuf>,
backups: &mut Vec<(PathBuf, Vec<u8>)>,
) -> VersionResult<()> {
for update in &resolution.updates {
let package_info =
packages.get(&update.name).ok_or_else(|| VersionError::PackageNotFound {
name: update.name.clone(),
workspace_root: self.workspace_root.clone(),
})?;
let package_json_path = self.write_package_json(package_info, update, backups).await?;
modified_files.push(package_json_path);
}
Ok(())
}
async fn write_package_json(
&self,
package: &PackageInfo,
update: &PackageUpdate,
backups: &mut Vec<(PathBuf, Vec<u8>)>,
) -> VersionResult<PathBuf> {
let package_json_path = package.path().join("package.json");
let current_content = self.fs.read_file(&package_json_path).await.map_err(|e| {
VersionError::FileSystemError {
path: package_json_path.clone(),
reason: format!("Failed to read package.json: {}", e),
}
})?;
backups.push((package_json_path.clone(), current_content.clone()));
let mut pkg_json: PackageJson = serde_json::from_slice(¤t_content).map_err(|e| {
VersionError::PackageJsonError {
path: package_json_path.clone(),
reason: format!("Failed to parse JSON: {}", e),
}
})?;
pkg_json.version = update.next_version.to_string();
for dep_update in &update.dependency_updates {
if Self::is_skipped_version_spec(&dep_update.old_version_spec) {
continue;
}
match dep_update.dependency_type {
DependencyType::Regular => {
if let Some(deps) = &mut pkg_json.dependencies {
deps.insert(
dep_update.dependency_name.clone(),
dep_update.new_version_spec.clone(),
);
}
}
DependencyType::Dev => {
if let Some(dev_deps) = &mut pkg_json.dev_dependencies {
dev_deps.insert(
dep_update.dependency_name.clone(),
dep_update.new_version_spec.clone(),
);
}
}
DependencyType::Peer => {
if let Some(peer_deps) = &mut pkg_json.peer_dependencies {
peer_deps.insert(
dep_update.dependency_name.clone(),
dep_update.new_version_spec.clone(),
);
}
}
DependencyType::Optional => {
if let Some(optional_deps) = &mut pkg_json.optional_dependencies {
optional_deps.insert(
dep_update.dependency_name.clone(),
dep_update.new_version_spec.clone(),
);
}
}
}
}
let json_string =
serde_json::to_string_pretty(&pkg_json).map_err(|e| VersionError::ApplyFailed {
path: package_json_path.clone(),
reason: format!("Failed to serialize JSON: {}", e),
})?;
self.fs.write_file_string(&package_json_path, &json_string).await.map_err(|e| {
VersionError::ApplyFailed {
path: package_json_path.clone(),
reason: format!("Failed to write package.json: {}", e),
}
})?;
Ok(package_json_path)
}
pub(crate) fn is_skipped_version_spec(version_spec: &str) -> bool {
version_spec.starts_with("workspace:")
|| version_spec.starts_with("file:")
|| version_spec.starts_with("link:")
|| version_spec.starts_with("portal:")
}
async fn restore_backups(&self, backups: &[(PathBuf, Vec<u8>)]) -> VersionResult<()> {
for (path, content) in backups {
self.fs.write_file(path, content).await.map_err(|e| VersionError::ApplyFailed {
path: path.clone(),
reason: format!("Failed to restore backup: {}", e),
})?;
}
Ok(())
}
}