use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde::Deserialize;
use crate::config::{PageBuild, RuntimeBuild};
fn default_out_dir() -> String {
"static".to_owned()
}
fn default_runtime_package() -> String {
"islands-runtime".to_owned()
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Deserialize)]
pub struct IslandsConfig {
#[serde(default = "default_out_dir")]
pub out_dir: String,
#[serde(default)]
pub bundle_prefix: Option<String>,
#[serde(default = "default_runtime_package")]
pub runtime_package: String,
#[serde(default = "default_true")]
pub runtime_nav: bool,
#[serde(default)]
pub css_source_templates: Vec<String>,
#[serde(default)]
pub pages: Vec<String>,
}
#[derive(Deserialize)]
struct IslandsConfigFile {
islands: IslandsConfig,
}
#[derive(Deserialize)]
struct CargoMetadataFile {
package: Option<CargoPackage>,
}
#[derive(Deserialize)]
struct CargoPackage {
metadata: Option<CargoMetadata>,
}
#[derive(Deserialize)]
struct CargoMetadata {
islands: Option<IslandsConfig>,
}
impl IslandsConfig {
pub fn from_toml_file(path: &Path) -> Result<Option<Self>> {
if !path.exists() {
return Ok(None);
}
let text =
std::fs::read_to_string(path).with_context(|| format!("read {}", path.display()))?;
let parsed: IslandsConfigFile =
toml::from_str(&text).with_context(|| format!("parse [islands] in {}", path.display()))?;
Ok(Some(parsed.islands))
}
pub fn from_cargo_metadata(cargo_toml: &Path) -> Result<Option<Self>> {
if !cargo_toml.exists() {
return Ok(None);
}
let text = std::fs::read_to_string(cargo_toml)
.with_context(|| format!("read {}", cargo_toml.display()))?;
let parsed: CargoMetadataFile = toml::from_str(&text)
.with_context(|| format!("parse {}", cargo_toml.display()))?;
Ok(parsed.package.and_then(|package| package.metadata).and_then(|metadata| metadata.islands))
}
pub fn resolve_out_dir(&self, workspace_root: &Path) -> PathBuf {
workspace_root.join(&self.out_dir)
}
pub fn runtime(&self) -> RuntimeBuild {
RuntimeBuild {
package: self.runtime_package.clone(),
nav: self.runtime_nav,
}
}
pub fn page_builds(&self) -> Vec<PageBuild> {
let prefix = self.bundle_prefix.as_deref().unwrap_or("");
self.pages
.iter()
.map(|crate_name| {
let short = crate_name.strip_prefix("page-").unwrap_or(crate_name);
let bundle_key = if prefix.is_empty() {
crate_name.clone()
} else {
format!("{prefix}/{crate_name}")
};
let css_sources = self
.css_source_templates
.iter()
.map(|template| {
template
.replace("{prefix}", prefix)
.replace("{crate}", crate_name)
.replace("{short}", short)
})
.collect();
PageBuild {
crate_name: crate_name.clone(),
bundle_key,
css_sources,
}
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE: &str = r#"
[islands]
bundle_prefix = "counter"
pages = ["page-home", "page-random"]
css_source_templates = [
"../../examples/{prefix}/{crate}/src/**/*.rs",
"../../examples/{prefix}/src/pages/{short}.rs",
]
"#;
fn parse(text: &str) -> IslandsConfig {
toml::from_str::<IslandsConfigFile>(text).unwrap().islands
}
#[test]
fn defaults_apply_when_omitted() {
let config = parse("[islands]\n");
assert_eq!(config.out_dir, "static");
assert_eq!(config.runtime_package, "islands-runtime");
assert!(config.runtime_nav);
assert!(config.pages.is_empty());
}
#[test]
fn page_builds_apply_prefix_and_templates() {
let config = parse(SAMPLE);
let pages = config.page_builds();
assert_eq!(pages.len(), 2);
let home = &pages[0];
assert_eq!(home.crate_name, "page-home");
assert_eq!(home.bundle_key, "counter/page-home");
assert_eq!(home.short_name(), "home");
assert_eq!(
home.css_sources,
vec![
"../../examples/counter/page-home/src/**/*.rs".to_owned(),
"../../examples/counter/src/pages/home.rs".to_owned(),
]
);
}
#[test]
fn no_prefix_yields_bare_bundle_key() {
let config = parse("[islands]\npages = [\"page-home\"]\n");
assert_eq!(config.page_builds()[0].bundle_key, "page-home");
}
}