use std::path::Path;
use serde::Serialize;
pub(crate) mod deps;
mod load;
mod workspace;
pub use load::load;
pub(crate) use load::load_with_raw;
pub(crate) use workspace::discover_packages;
#[cfg(test)]
mod tests;
const SCOPE_DIRS: &[&str] = &["crates", "packages", "modules"];
pub fn discover_scopes(repo_root: &Path) -> Vec<String> {
let mut scopes = Vec::new();
for dir in SCOPE_DIRS {
let parent = repo_root.join(dir);
if let Ok(entries) = std::fs::read_dir(&parent) {
for entry in entries.flatten() {
if entry.path().is_dir()
&& let Some(name) = entry.file_name().to_str()
{
scopes.push(name.to_string());
}
}
}
}
scopes.sort();
scopes.dedup();
scopes
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Scheme {
#[default]
Semver,
Calver,
Patch,
}
#[derive(Debug, Clone, Default, PartialEq)]
pub enum ScopesConfig {
#[default]
None,
Auto,
List(Vec<String>),
}
impl Serialize for ScopesConfig {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
ScopesConfig::None => serializer.serialize_none(),
ScopesConfig::Auto => serializer.serialize_str("auto"),
ScopesConfig::List(list) => list.serialize(serializer),
}
}
}
pub const DEFAULT_TAG_TEMPLATE: &str = "{name}@{version}";
#[derive(Debug, Clone, Serialize)]
pub struct VersioningConfig {
pub tag_prefix: String,
pub prerelease_tag: String,
pub calver_format: String,
pub tag_template: String,
}
impl Default for VersioningConfig {
fn default() -> Self {
Self {
tag_prefix: "v".to_string(),
prerelease_tag: "rc".to_string(),
calver_format: standard_version::calver::DEFAULT_FORMAT.to_string(),
tag_template: DEFAULT_TAG_TEMPLATE.to_string(),
}
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct ChangelogConfig {
pub title: Option<String>,
pub sections: Option<Vec<(String, String)>>,
pub hidden: Option<Vec<String>>,
pub bug_url: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct VersionFileConfig {
pub path: String,
pub regex: String,
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct PackageConfig {
pub name: String,
pub path: String,
pub scheme: Option<Scheme>,
pub version_files: Option<Vec<VersionFileConfig>>,
pub changelog: Option<ChangelogConfig>,
}
#[derive(Debug, Default, Serialize)]
pub struct ProjectConfig {
pub types: Vec<String>,
pub scopes: ScopesConfig,
pub strict: bool,
pub scheme: Scheme,
pub changelog: ChangelogConfig,
pub versioning: VersioningConfig,
pub version_files: Vec<VersionFileConfig>,
pub monorepo: bool,
pub packages: Vec<PackageConfig>,
pub release_branch: Option<String>,
pub refs_required: Vec<String>,
}
impl ProjectConfig {
pub fn to_changelog_config(&self) -> standard_changelog::ChangelogConfig {
let default = standard_changelog::ChangelogConfig::default();
standard_changelog::ChangelogConfig {
title: self.changelog.title.clone().unwrap_or(default.title),
sections: self.changelog.sections.clone().unwrap_or(default.sections),
hidden: self.changelog.hidden.clone().unwrap_or(default.hidden),
bug_url: self.changelog.bug_url.clone(),
}
}
pub fn to_package_changelog_config(
&self,
pkg_changelog: Option<&ChangelogConfig>,
) -> standard_changelog::ChangelogConfig {
let global = self.to_changelog_config();
let Some(pkg) = pkg_changelog else {
return global;
};
standard_changelog::ChangelogConfig {
title: pkg.title.clone().unwrap_or(global.title),
sections: pkg.sections.clone().unwrap_or(global.sections),
hidden: pkg.hidden.clone().unwrap_or(global.hidden),
bug_url: pkg.bug_url.clone().or(global.bug_url),
}
}
pub fn resolved_scopes(
&self,
repo_root: &Path,
packages: Option<&[PackageConfig]>,
) -> Vec<String> {
let mut scopes = match &self.scopes {
ScopesConfig::None if self.monorepo => discover_scopes(repo_root),
ScopesConfig::None => return Vec::new(),
ScopesConfig::Auto => discover_scopes(repo_root),
ScopesConfig::List(list) => list.clone(),
};
if self.monorepo {
let owned;
let pkgs = match packages {
Some(p) => p,
None => {
owned = self.resolved_packages(repo_root);
&owned
}
};
for pkg in pkgs {
if !scopes.contains(&pkg.name) {
scopes.push(pkg.name.clone());
}
}
scopes.sort();
scopes.dedup();
}
scopes
}
pub fn resolved_packages(&self, repo_root: &Path) -> Vec<PackageConfig> {
if !self.packages.is_empty() {
return self.packages.clone();
}
if self.monorepo {
discover_packages(repo_root)
} else {
Vec::new()
}
}
pub fn to_lint_config(&self, strict: bool, repo_root: &Path) -> standard_commit::LintConfig {
if self.strict || strict {
let (scopes, require_scope) = if self.monorepo {
let resolved = self.resolved_scopes(repo_root, None);
if resolved.is_empty() {
(None, false)
} else {
(Some(resolved), true)
}
} else {
match &self.scopes {
ScopesConfig::None => (None, false),
ScopesConfig::Auto => {
let discovered = discover_scopes(repo_root);
if discovered.is_empty() {
(None, false)
} else {
(Some(discovered), true)
}
}
ScopesConfig::List(list) => (Some(list.clone()), true),
}
};
let scopes = scopes.map(|mut s| {
if !s.iter().any(|v| v == "release") {
s.push("release".to_string());
}
s
});
standard_commit::LintConfig {
types: Some(self.types.clone()),
scopes,
require_scope,
..Default::default()
}
} else {
standard_commit::LintConfig::default()
}
}
}