use std::{
fs,
path::{Path, PathBuf},
str::FromStr as _,
};
use anyhow::{bail, Context};
use serde::Deserialize;
use crate::{class_name_pattern::ClassNamePattern, path_utils::normalize};
fn default_extensions() -> Vec<String> {
vec![".module.css".to_owned(), ".module.scss".to_owned()]
}
fn default_folders() -> Vec<PathBuf> {
vec![PathBuf::from_str("./src/").expect("path is valid")]
}
fn default_hash_len() -> usize {
7
}
#[derive(Deserialize, Debug, Default, Clone)]
#[serde(deny_unknown_fields)]
pub struct PartialConfig {
pub output_file: Option<PathBuf>,
pub output_dir: Option<PathBuf>,
pub extensions: Option<Vec<String>>,
pub folders: Option<Vec<PathBuf>>,
pub scss_prelude: Option<String>,
pub hash_len: Option<usize>,
pub class_name_pattern: Option<ClassNamePattern>,
pub hash_root_path: Option<PathBuf>,
#[serde(default)]
pub workspace: bool,
}
pub struct Config {
pub manifest_dir: PathBuf,
pub workspace_dir: Option<PathBuf>,
pub output_file: Option<PathBuf>,
pub output_dir: Option<PathBuf>,
pub extensions: Vec<String>,
pub folders: Vec<PathBuf>,
pub scss_prelude: Option<String>,
pub hash_len: usize,
pub class_name_pattern: ClassNamePattern,
pub hash_root_path: PathBuf,
}
impl Config {
pub fn load(manifest_dir: PathBuf) -> anyhow::Result<Self> {
let cargo_toml_contents = fs::read_to_string(manifest_dir.join("Cargo.toml"))
.context("Failed to read Cargo.toml")?;
let mut cargo_toml: CargoToml = toml::from_str(&cargo_toml_contents)?;
let config = cargo_toml
.package
.as_mut()
.and_then(|p| p.metadata.as_mut())
.and_then(|m| m.stylance.take())
.unwrap_or_default();
let workspace = if config.workspace {
let (workspace_root, mut ws_cargo_toml) =
find_workspace_root(&manifest_dir, cargo_toml)?;
let ws_config = ws_cargo_toml
.workspace
.as_mut()
.and_then(|w| w.metadata.as_mut())
.and_then(|m| m.stylance.take())
.unwrap_or_default();
Some((workspace_root, ws_config))
} else {
None
};
Self::from_partials(manifest_dir, config, workspace)
}
pub fn from_partials(
manifest_dir: PathBuf,
config: PartialConfig,
workspace: Option<(PathBuf, PartialConfig)>,
) -> anyhow::Result<Self> {
let (workspace_dir, ws_config) = match workspace {
Some((workspace_dir, mut ws_config)) => {
ws_config.hash_root_path = Some(
ws_config
.hash_root_path
.map_or_else(|| workspace_dir.clone(), |p| workspace_dir.join(p)),
);
ws_config.output_file = ws_config.output_file.map(|p| workspace_dir.join(p));
ws_config.output_dir = ws_config.output_dir.map(|p| workspace_dir.join(p));
(Some(workspace_dir), ws_config)
}
None => (None, PartialConfig::default()),
};
let config = Self {
output_file: config
.output_file
.or(ws_config.output_file)
.map(|p| manifest_dir.join(p)),
output_dir: config
.output_dir
.or(ws_config.output_dir)
.map(|p| manifest_dir.join(p)),
extensions: config
.extensions
.or(ws_config.extensions)
.unwrap_or_else(default_extensions),
folders: config
.folders
.or(ws_config.folders)
.unwrap_or_else(default_folders)
.into_iter()
.map(|p| manifest_dir.join(p))
.collect(),
scss_prelude: config.scss_prelude.or(ws_config.scss_prelude),
hash_len: config
.hash_len
.or(ws_config.hash_len)
.unwrap_or(default_hash_len()),
class_name_pattern: config
.class_name_pattern
.or(ws_config.class_name_pattern)
.unwrap_or_default(),
hash_root_path: config
.hash_root_path
.or(ws_config.hash_root_path)
.map(|p| manifest_dir.join(p))
.unwrap_or_else(|| manifest_dir.to_path_buf()),
workspace_dir,
manifest_dir,
};
if config.extensions.iter().any(|e| e.is_empty()) {
bail!("Stylance config extensions can't be empty strings");
}
Ok(config)
}
}
#[derive(Deserialize)]
struct CargoToml {
package: Option<CargoTomlPackage>,
workspace: Option<CargoTomlWorkspace>,
}
#[derive(Deserialize)]
struct CargoTomlPackage {
metadata: Option<CargoTomlPackageMetadata>,
#[serde(rename = "workspace")]
workspace_path: Option<toml::Value>,
}
#[derive(Deserialize)]
struct CargoTomlPackageMetadata {
stylance: Option<PartialConfig>,
}
#[derive(Deserialize)]
struct CargoTomlWorkspace {
metadata: Option<CargoTomlWorkspaceMetadata>,
}
#[derive(Deserialize)]
struct CargoTomlWorkspaceMetadata {
stylance: Option<PartialConfig>,
}
fn find_workspace_root(
manifest_dir: &Path,
cargo_toml: CargoToml,
) -> anyhow::Result<(PathBuf, CargoToml)> {
let manifest_dir = normalize(manifest_dir)?;
if cargo_toml.workspace.is_some() {
return Ok((manifest_dir, cargo_toml));
}
if let Some(CargoTomlPackage {
workspace_path: Some(toml::Value::String(workspace_path)),
..
}) = &cargo_toml.package
{
let ws_root = manifest_dir.join(workspace_path);
let contents = fs::read_to_string(ws_root.join("Cargo.toml")).with_context(|| {
format!(
"Failed to read workspace Cargo.toml at {}",
ws_root.display()
)
})?;
let parsed: CargoToml = toml::from_str(&contents)?;
return Ok((ws_root, parsed));
}
let mut current = manifest_dir.to_path_buf();
loop {
if !current.pop() {
bail!(
"Could not find workspace root for {}. \
No parent Cargo.toml with [workspace] was found.",
manifest_dir.display()
);
}
let candidate = current.join("Cargo.toml");
if candidate.exists() {
let contents = fs::read_to_string(&candidate)
.with_context(|| format!("Failed to read {}", candidate.display()))?;
let parsed: CargoToml = toml::from_str(&contents)?;
if parsed.workspace.is_some() {
return Ok((current, parsed));
}
}
}
}