use miette::{Context, IntoDiagnostic};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) enum CatalogSource {
WorkspaceYaml(std::path::PathBuf),
PackageJson {
path: std::path::PathBuf,
namespace: String,
},
}
pub(crate) type CatalogMap =
std::collections::BTreeMap<String, std::collections::BTreeMap<String, String>>;
fn merge_catalog_source(
out: &mut CatalogMap,
default_cat: &std::collections::BTreeMap<String, String>,
named_cats: &CatalogMap,
) {
if !default_cat.is_empty() {
let entry = out.entry("default".to_string()).or_default();
for (k, v) in default_cat {
entry.insert(k.clone(), v.clone());
}
}
for (name, entries) in named_cats {
let bucket = out.entry(name.clone()).or_default();
for (k, v) in entries {
bucket.insert(k.clone(), v.clone());
}
}
}
fn merge_manifest_catalogs(out: &mut CatalogMap, manifest: &aube_manifest::PackageJson) {
if let Some(ws) = &manifest.workspaces {
merge_catalog_source(out, ws.catalog(), ws.catalogs());
}
merge_catalog_source(out, &manifest.pnpm_catalog(), &manifest.pnpm_catalogs());
}
pub(crate) fn discover_catalogs(project_root: &std::path::Path) -> miette::Result<CatalogMap> {
let mut out = CatalogMap::new();
let project_manifest_path = project_root.join("package.json");
let project_manifest = aube_manifest::PackageJson::from_path(&project_manifest_path).ok();
if let Some(m) = &project_manifest {
merge_manifest_catalogs(&mut out, m);
}
let workspace_yaml_dir = crate::dirs::find_workspace_yaml_root(project_root);
let workspace_root_dir = crate::dirs::find_workspace_root(project_root);
if let Some(dir) = &workspace_root_dir
&& dir != project_root
&& let Ok(m) = aube_manifest::PackageJson::from_path(&dir.join("package.json"))
{
merge_manifest_catalogs(&mut out, &m);
}
let yaml_dir = workspace_yaml_dir.as_deref().unwrap_or(project_root);
let (ws_config, _raw) = aube_manifest::workspace::load_both(yaml_dir)
.into_diagnostic()
.wrap_err("failed to load workspace config")?;
merge_catalog_source(&mut out, &ws_config.catalog, &ws_config.catalogs);
out.retain(|_, v| !v.is_empty());
Ok(out)
}
pub(crate) fn load_workspace_catalogs(cwd: &std::path::Path) -> miette::Result<CatalogMap> {
discover_catalogs(cwd)
}
fn manifest_catalog_source(
path: &std::path::Path,
catalog: &str,
package: &str,
) -> Option<CatalogSource> {
let content = std::fs::read_to_string(path).ok()?;
let json: serde_json::Value = serde_json::from_str(&content).ok()?;
let root = json.as_object()?;
let mut source = None;
let id = aube_util::embedder();
let namespaces = std::iter::once("workspaces")
.chain(id.compatible_names.iter().copied())
.chain((!id.manifest_namespace.is_empty()).then_some(id.manifest_namespace));
for namespace in namespaces {
let Some(config) = root.get(namespace).and_then(serde_json::Value::as_object) else {
continue;
};
let entries = if catalog == "default" {
config.get("catalog").and_then(serde_json::Value::as_object)
} else {
config
.get("catalogs")
.and_then(serde_json::Value::as_object)
.and_then(|catalogs| catalogs.get(catalog))
.and_then(serde_json::Value::as_object)
};
if entries.is_some_and(|entries| entries.contains_key(package)) {
source = Some(CatalogSource::PackageJson {
path: path.to_path_buf(),
namespace: namespace.to_string(),
});
}
}
source
}
pub(crate) fn catalog_entry_source(
project_root: &std::path::Path,
catalog: &str,
package: &str,
) -> Option<CatalogSource> {
let project_manifest = project_root.join("package.json");
let mut source = manifest_catalog_source(&project_manifest, catalog, package);
let workspace_root = crate::dirs::find_workspace_root(project_root);
if let Some(root) = &workspace_root
&& root != project_root
&& let Some(root_source) =
manifest_catalog_source(&root.join("package.json"), catalog, package)
{
source = Some(root_source);
}
let yaml_dir = crate::dirs::find_workspace_yaml_root(project_root);
if let Some(dir) = yaml_dir
&& let Some(path) = aube_manifest::workspace::workspace_yaml_existing(&dir)
&& let Ok(config) = aube_manifest::workspace::WorkspaceConfig::load(&dir)
{
let entries = if catalog == "default" {
config.catalog.get(package)
} else {
config
.catalogs
.get(catalog)
.and_then(|entries| entries.get(package))
};
if entries.is_some() {
source = Some(CatalogSource::WorkspaceYaml(path));
}
}
source
}