use std::path::{Path, PathBuf};
use crate::path::normalize;
#[derive(Debug, Clone, Copy)]
pub struct CssResolve<'a> {
pub root: Option<&'a Path>,
pub public_dir: Option<&'a Path>,
pub alias: &'a [(String, String)],
pub targets: &'a [String],
pub minify: bool,
pub modules: &'a CssModulesOptions,
}
static DEFAULT_MODULES: CssModulesOptions = CssModulesOptions {
locals_convention: None,
generate_scoped_name: None,
global_scope: false,
global_module_paths: Vec::new(),
};
impl Default for CssResolve<'_> {
fn default() -> Self {
CssResolve {
root: None,
public_dir: None,
alias: &[],
targets: &[],
minify: false,
modules: &DEFAULT_MODULES,
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct CssModulesOptions {
pub locals_convention: Option<String>,
pub generate_scoped_name: Option<String>,
pub global_scope: bool,
pub global_module_paths: Vec<String>,
}
#[derive(Debug, Default, Clone)]
pub struct CssResolveConfig {
pub root: PathBuf,
pub public_dir: PathBuf,
pub alias: Vec<(String, String)>,
pub targets: Vec<String>,
pub minify: bool,
pub modules: CssModulesOptions,
}
impl CssResolveConfig {
pub fn as_ref(&self) -> CssResolve<'_> {
fn set(p: &Path) -> Option<&Path> {
(!p.as_os_str().is_empty()).then_some(p)
}
CssResolve {
root: set(&self.root),
public_dir: set(&self.public_dir),
alias: &self.alias,
targets: &self.targets,
minify: self.minify,
modules: &self.modules,
}
}
}
impl From<CssResolve<'_>> for CssResolveConfig {
fn from(r: CssResolve<'_>) -> Self {
CssResolveConfig {
root: r.root.map(Path::to_path_buf).unwrap_or_default(),
public_dir: r.public_dir.map(Path::to_path_buf).unwrap_or_default(),
alias: r.alias.to_vec(),
targets: r.targets.to_vec(),
minify: r.minify,
modules: r.modules.clone(),
}
}
}
impl CssResolve<'_> {
pub fn alias_spec(&self, spec: &str) -> Option<String> {
self.alias.iter().find_map(|(find, replacement)| {
if find.is_empty() {
return None;
}
let rest = if spec == find {
""
} else {
spec.strip_prefix(find.as_str())
.filter(|rest| rest.starts_with('/'))?
};
let target = match (replacement.starts_with('.'), self.root) {
(true, Some(root)) => normalize(&root.join(replacement))
.to_string_lossy()
.into_owned(),
_ => replacement.clone(),
};
Some(format!("{target}{rest}"))
})
}
pub fn alias_path(&self, spec: &str) -> Option<PathBuf> {
let aliased = self.alias_spec(spec)?;
Path::new(&aliased)
.is_absolute()
.then(|| PathBuf::from(aliased))
}
pub fn public_file(&self, spec: &str) -> Option<PathBuf> {
let p = self.public_dir?.join(root_absolute_rel(spec)?);
p.is_file().then_some(p)
}
pub fn root_path(&self, spec: &str) -> Option<PathBuf> {
Some(self.root?.join(root_absolute_rel(spec)?))
}
pub(crate) fn dev_url(&self, file: &Path) -> String {
match self.root.and_then(|r| file.strip_prefix(r).ok()) {
Some(rel) => format!("/{}", rel.to_string_lossy().replace('\\', "/")),
None => format!("/@fs{}", file.display()),
}
}
}
fn root_absolute_rel(spec: &str) -> Option<&str> {
let rel = spec.strip_prefix('/')?;
(!rel.is_empty() && !rel.starts_with('/')).then_some(rel)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn alias_matches_whole_specifier_or_slash_prefix_like_rollup_alias() {
let alias = vec![
("@".to_string(), "./src".to_string()),
("@components".to_string(), "/abs/components".to_string()),
("react".to_string(), "preact/compat".to_string()),
];
let r = CssResolve {
root: Some(Path::new("/proj")),
public_dir: None,
alias: &alias,
..CssResolve::default()
};
assert_eq!(
r.alias_spec("@/img.png").as_deref(),
Some("/proj/src/img.png")
);
assert_eq!(r.alias_spec("@").as_deref(), Some("/proj/src"));
assert_eq!(
r.alias_spec("@components/btn.css").as_deref(),
Some("/abs/components/btn.css")
);
assert_eq!(
r.alias_spec("@scope/pkg/x.css"),
None,
"not a `find/` prefix match"
);
assert_eq!(
r.alias_spec("react/x.css").as_deref(),
Some("preact/compat/x.css")
);
assert!(r.alias_path("react/x.css").is_none());
assert_eq!(
r.alias_path("@/a.css"),
Some(PathBuf::from("/proj/src/a.css"))
);
}
}