use crate::config::PackageToolsConfig;
use crate::error::{ChangesError, ChangesResult};
use crate::types::PackageInfo;
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::{
MonorepoDescriptor, MonorepoDetector, MonorepoDetectorTrait,
};
#[derive(Debug)]
pub struct PackageMapper<F = FileSystemManager>
where
F: AsyncFileSystem + Clone + Send + Sync + 'static,
{
workspace_root: PathBuf,
fs: F,
monorepo_detector: MonorepoDetector<F>,
pub(crate) cached_monorepo: Option<Option<MonorepoDescriptor>>,
pub(crate) file_cache: HashMap<PathBuf, Option<String>>,
}
impl PackageMapper<FileSystemManager> {
#[must_use]
pub fn new(workspace_root: PathBuf, fs: FileSystemManager) -> Self {
let monorepo_detector = MonorepoDetector::with_filesystem(fs.clone());
Self {
workspace_root,
fs,
monorepo_detector,
cached_monorepo: None,
file_cache: HashMap::new(),
}
}
#[must_use]
pub fn new_with_config(
workspace_root: PathBuf,
fs: FileSystemManager,
config: &PackageToolsConfig,
) -> Self {
let monorepo_config = Self::build_monorepo_config(config);
let monorepo_detector =
MonorepoDetector::with_filesystem_and_config(fs.clone(), monorepo_config);
Self {
workspace_root,
fs,
monorepo_detector,
cached_monorepo: None,
file_cache: HashMap::new(),
}
}
#[must_use]
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
}
}
impl<F> PackageMapper<F>
where
F: AsyncFileSystem + Clone + Send + Sync + 'static,
{
#[must_use]
pub fn with_filesystem(workspace_root: PathBuf, fs: F) -> Self {
let monorepo_detector = MonorepoDetector::with_filesystem(fs.clone());
Self {
workspace_root,
fs,
monorepo_detector,
cached_monorepo: None,
file_cache: HashMap::new(),
}
}
#[must_use]
pub fn with_filesystem_and_config(
workspace_root: PathBuf,
fs: F,
config: &PackageToolsConfig,
) -> Self {
let monorepo_config = Self::build_monorepo_config_generic(config);
let monorepo_detector =
MonorepoDetector::with_filesystem_and_config(fs.clone(), monorepo_config);
Self {
workspace_root,
fs,
monorepo_detector,
cached_monorepo: None,
file_cache: HashMap::new(),
}
}
#[must_use]
fn build_monorepo_config_generic(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
}
pub async fn map_files_to_packages(
&mut self,
files: &[PathBuf],
) -> ChangesResult<HashMap<String, Vec<PathBuf>>> {
self.ensure_monorepo_detected().await?;
let mut package_files: HashMap<String, Vec<PathBuf>> = HashMap::new();
for file in files {
let normalized_path = self.normalize_path(file)?;
if let Some(package_name) = self.find_package_for_file(&normalized_path).await? {
package_files.entry(package_name).or_default().push(normalized_path);
}
}
Ok(package_files)
}
pub async fn find_package_for_file(&mut self, file: &Path) -> ChangesResult<Option<String>> {
let normalized_path = self.normalize_path(file)?;
if let Some(cached_result) = self.file_cache.get(&normalized_path) {
return Ok(cached_result.clone());
}
self.ensure_monorepo_detected().await?;
let package_name = self.find_package_for_file_impl(&normalized_path).await?;
self.file_cache.insert(normalized_path, package_name.clone());
Ok(package_name)
}
pub async fn get_all_packages(&mut self) -> ChangesResult<Vec<PackageInfo>> {
self.ensure_monorepo_detected().await?;
if let Some(Some(monorepo)) = &self.cached_monorepo {
let mut packages = Vec::new();
for wp in monorepo.packages() {
packages.push(self.workspace_package_to_package_info(wp).await?);
}
if packages.is_empty() {
return Err(ChangesError::NoPackagesFound {
workspace_root: self.workspace_root.clone(),
});
}
Ok(packages)
} else {
let package_info = self.read_root_package().await?;
Ok(vec![package_info])
}
}
pub fn clear_cache(&mut self) {
self.cached_monorepo = None;
self.file_cache.clear();
}
pub async fn is_monorepo(&mut self) -> ChangesResult<bool> {
self.ensure_monorepo_detected().await?;
Ok(self.cached_monorepo.as_ref().is_some_and(|m| m.is_some()))
}
async fn ensure_monorepo_detected(&mut self) -> ChangesResult<()> {
if self.cached_monorepo.is_none() {
let monorepo_result =
self.monorepo_detector.detect_monorepo(&self.workspace_root).await;
match monorepo_result {
Ok(descriptor) => {
self.cached_monorepo = Some(Some(descriptor));
}
Err(_) => {
self.cached_monorepo = Some(None);
}
}
}
Ok(())
}
async fn find_package_for_file_impl(&self, file: &Path) -> ChangesResult<Option<String>> {
if let Some(Some(monorepo)) = &self.cached_monorepo {
let absolute_file = if file.is_absolute() {
file.to_path_buf()
} else {
self.workspace_root.join(file)
};
let canonical_file = absolute_file.canonicalize().unwrap_or(absolute_file);
if let Some(workspace_package) = monorepo.find_package_for_path(&canonical_file) {
return Ok(Some(workspace_package.name.clone()));
}
Ok(None)
} else {
let package_info = self.read_root_package().await?;
Ok(Some(package_info.name().to_string()))
}
}
pub(crate) fn normalize_path(&self, path: &Path) -> ChangesResult<PathBuf> {
if path.is_absolute() {
path.strip_prefix(&self.workspace_root).map(|p| p.to_path_buf()).map_err(|_| {
ChangesError::FileOutsideWorkspace {
path: path.to_path_buf(),
workspace_root: self.workspace_root.clone(),
}
})
} else {
Ok(path.to_path_buf())
}
}
async fn read_root_package(&self) -> ChangesResult<PackageInfo> {
let package_json_path = self.workspace_root.join("package.json");
let content = self.fs.read_file_string(&package_json_path).await.map_err(|e| {
ChangesError::FileSystemError {
path: package_json_path.clone(),
reason: format!("Failed to read package.json: {}", e),
}
})?;
let package_json: PackageJson =
serde_json::from_str(&content).map_err(|e| ChangesError::PackageJsonParseError {
path: package_json_path.clone(),
reason: e.to_string(),
})?;
Ok(PackageInfo::new(package_json, None, self.workspace_root.clone()))
}
async fn workspace_package_to_package_info(
&self,
workspace_package: &sublime_standard_tools::monorepo::WorkspacePackage,
) -> ChangesResult<PackageInfo> {
let package_json_path = workspace_package.absolute_path.join("package.json");
let content = self.fs.read_file_string(&package_json_path).await.map_err(|e| {
ChangesError::FileSystemError {
path: package_json_path.clone(),
reason: format!("Failed to read package.json: {}", e),
}
})?;
let package_json: PackageJson =
serde_json::from_str(&content).map_err(|e| ChangesError::PackageJsonParseError {
path: package_json_path.clone(),
reason: e.to_string(),
})?;
Ok(PackageInfo::new(
package_json,
Some(workspace_package.clone()),
workspace_package.absolute_path.clone(),
))
}
}