use std::path::{Path, PathBuf};
use serde_json::Value;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Mount {
specifier: String,
url: String,
dir: PathBuf,
watch: bool,
}
impl Mount {
pub fn new(prefix: impl AsRef<str>, dir: impl Into<PathBuf>) -> Self {
let p = prefix.as_ref().trim_matches('/');
Self {
specifier: format!("{p}/"),
url: format!("/{p}/"),
dir: dir.into(),
watch: true,
}
}
pub fn root(dir: impl Into<PathBuf>) -> Self {
Self {
specifier: String::new(),
url: "/".to_string(),
dir: dir.into(),
watch: true,
}
}
pub fn from_dir(dir: impl Into<PathBuf>) -> Self {
let dir = dir.into();
let (name, root) = match std::fs::read(dir.join("package.json"))
.ok()
.and_then(|bytes| serde_json::from_slice::<Value>(&bytes).ok())
{
Some(json) => {
let name = json.get("name").and_then(Value::as_str).map(str::to_string);
let root = json
.get("web_modules")
.and_then(|v| v.get("root"))
.and_then(Value::as_str)
.map(|r| dir.join(r.trim_start_matches("./")));
(name, root)
}
None => (None, None),
};
let name = name.unwrap_or_else(|| {
dir.file_name()
.and_then(|n| n.to_str())
.unwrap_or_default()
.to_string()
});
let served = root.unwrap_or_else(|| dir.clone());
Mount::new(&name, served)
}
pub fn specifier(mut self, specifier: impl Into<String>) -> Self {
self.specifier = specifier.into();
self
}
pub fn url(mut self, url: impl Into<String>) -> Self {
self.url = url.into();
self
}
pub fn watched(mut self, watch: bool) -> Self {
self.watch = watch;
self
}
pub fn dir(&self) -> &Path {
&self.dir
}
pub fn url_prefix(&self) -> &str {
&self.url
}
pub fn specifier_prefix(&self) -> &str {
&self.specifier
}
pub fn is_watched(&self) -> bool {
self.watch
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_derives_specifier_and_url_from_prefix() {
let m = Mount::new("/ui/", "ui/src");
assert_eq!(m.specifier_prefix(), "ui/");
assert_eq!(m.url_prefix(), "/ui/");
assert_eq!(m.dir(), Path::new("ui/src"));
assert!(m.is_watched());
}
#[test]
fn decoupled_specifier_url_dir() {
let m = Mount::new("contacts", "modules/contacts/web/src")
.specifier("@module/contacts/")
.url("/modules/contacts/")
.watched(false);
assert_eq!(m.specifier_prefix(), "@module/contacts/");
assert_eq!(m.url_prefix(), "/modules/contacts/");
assert_eq!(m.dir(), Path::new("modules/contacts/web/src"));
assert!(!m.is_watched());
}
#[test]
fn root_mount_has_no_specifier() {
let m = Mount::root("packages/frontend/web/src");
assert_eq!(m.specifier_prefix(), "");
assert_eq!(m.url_prefix(), "/");
}
#[test]
fn from_dir_uses_package_json_name_and_root() {
let tmp = tempfile::tempdir().unwrap();
let comp = tmp.path().join("widgets");
std::fs::create_dir_all(comp.join("src")).unwrap();
std::fs::write(
comp.join("package.json"),
r#"{"name":"@acme/widgets","web_modules":{"root":"./src"}}"#,
)
.unwrap();
let m = Mount::from_dir(&comp);
assert_eq!(m.specifier_prefix(), "@acme/widgets/");
assert_eq!(m.url_prefix(), "/@acme/widgets/");
assert_eq!(m.dir(), comp.join("src"));
}
#[test]
fn from_dir_falls_back_to_basename_and_dir() {
let tmp = tempfile::tempdir().unwrap();
let comp = tmp.path().join("plain");
std::fs::create_dir_all(&comp).unwrap(); let m = Mount::from_dir(&comp);
assert_eq!(m.specifier_prefix(), "plain/");
assert_eq!(m.dir(), comp);
}
#[test]
fn given_name_overrides_package_json_name() {
let tmp = tempfile::tempdir().unwrap();
let comp = tmp.path().join("widgets");
std::fs::create_dir_all(&comp).unwrap();
std::fs::write(comp.join("package.json"), r#"{"name":"@acme/widgets"}"#).unwrap();
let m = Mount::from_dir(&comp)
.specifier("counter/")
.url("/counter/");
assert_eq!(m.specifier_prefix(), "counter/");
assert_eq!(m.url_prefix(), "/counter/");
}
}