use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{Context, Result};
use http_types::Url;
use serde::Deserialize;
use structopt::StructOpt;
use crate::common::parse_public_url;
use crate::config::{RtcBuild, RtcClean, RtcServe, RtcWatch};
#[derive(Clone, Debug, Default, Deserialize, StructOpt)]
pub struct ConfigOptsBuild {
#[structopt(parse(from_os_str))]
pub target: Option<PathBuf>,
#[structopt(long)]
#[serde(default)]
pub release: bool,
#[structopt(short, long, parse(from_os_str))]
pub dist: Option<PathBuf>,
#[structopt(long, parse(from_str=parse_public_url))]
pub public_url: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, StructOpt)]
pub struct ConfigOptsWatch {
#[structopt(short, long, parse(from_os_str))]
pub ignore: Option<Vec<PathBuf>>,
}
#[derive(Clone, Debug, Default, Deserialize, StructOpt)]
pub struct ConfigOptsServe {
#[structopt(long)]
pub port: Option<u16>,
#[structopt(long)]
#[serde(default)]
pub open: bool,
#[structopt(long = "proxy-backend")]
#[serde(default)]
pub proxy_backend: Option<Url>,
#[structopt(long = "proxy-rewrite")]
#[serde(default)]
pub proxy_rewrite: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize, StructOpt)]
pub struct ConfigOptsClean {
#[structopt(short, long, parse(from_os_str))]
pub dist: Option<PathBuf>,
#[structopt(long)]
#[serde(default)]
pub cargo: bool,
}
#[derive(Clone, Debug, Deserialize)]
pub struct ConfigOptsProxy {
pub backend: Url,
pub rewrite: Option<String>,
}
#[derive(Clone, Debug, Default, Deserialize)]
pub struct ConfigOpts {
pub build: Option<ConfigOptsBuild>,
pub watch: Option<ConfigOptsWatch>,
pub serve: Option<ConfigOptsServe>,
pub clean: Option<ConfigOptsClean>,
pub proxy: Option<Vec<ConfigOptsProxy>>,
}
impl ConfigOpts {
pub async fn rtc_build(cli_build: ConfigOptsBuild, config: Option<PathBuf>) -> Result<Arc<RtcBuild>> {
let base_layer = Self::file_and_env_layers(config)?;
let build_layer = Self::cli_opts_layer_build(cli_build, base_layer);
let build_opts = build_layer.build.unwrap_or_default();
Ok(Arc::new(RtcBuild::new(build_opts)?))
}
pub async fn rtc_watch(cli_build: ConfigOptsBuild, cli_watch: ConfigOptsWatch, config: Option<PathBuf>) -> Result<Arc<RtcWatch>> {
let base_layer = Self::file_and_env_layers(config)?;
let build_layer = Self::cli_opts_layer_build(cli_build, base_layer);
let watch_layer = Self::cli_opts_layer_watch(cli_watch, build_layer);
let build_opts = watch_layer.build.unwrap_or_default();
let watch_opts = watch_layer.watch.unwrap_or_default();
Ok(Arc::new(RtcWatch::new(build_opts, watch_opts)?))
}
pub async fn rtc_serve(
cli_build: ConfigOptsBuild, cli_watch: ConfigOptsWatch, cli_serve: ConfigOptsServe, config: Option<PathBuf>,
) -> Result<Arc<RtcServe>> {
let base_layer = Self::file_and_env_layers(config)?;
let build_layer = Self::cli_opts_layer_build(cli_build, base_layer);
let watch_layer = Self::cli_opts_layer_watch(cli_watch, build_layer);
let serve_layer = Self::cli_opts_layer_serve(cli_serve, watch_layer);
let build_opts = serve_layer.build.unwrap_or_default();
let watch_opts = serve_layer.watch.unwrap_or_default();
let serve_opts = serve_layer.serve.unwrap_or_default();
Ok(Arc::new(RtcServe::new(build_opts, watch_opts, serve_opts, serve_layer.proxy)?))
}
pub async fn rtc_clean(cli_clean: ConfigOptsClean, config: Option<PathBuf>) -> Result<Arc<RtcClean>> {
let base_layer = Self::file_and_env_layers(config)?;
let clean_layer = Self::cli_opts_layer_clean(cli_clean, base_layer);
let clean_opts = clean_layer.clean.unwrap_or_default();
Ok(Arc::new(RtcClean::new(clean_opts)?))
}
pub async fn full(config: Option<PathBuf>) -> Result<Self> {
Self::file_and_env_layers(config)
}
fn cli_opts_layer_build(cli: ConfigOptsBuild, cfg_base: Self) -> Self {
let opts = ConfigOptsBuild {
target: cli.target,
release: cli.release,
dist: cli.dist,
public_url: cli.public_url,
};
let cfg_build = ConfigOpts {
build: Some(opts),
watch: None,
serve: None,
clean: None,
proxy: None,
};
Self::merge(cfg_base, cfg_build)
}
fn cli_opts_layer_watch(cli: ConfigOptsWatch, cfg_base: Self) -> Self {
let opts = ConfigOptsWatch { ignore: cli.ignore };
let cfg = ConfigOpts {
build: None,
watch: Some(opts),
serve: None,
clean: None,
proxy: None,
};
Self::merge(cfg_base, cfg)
}
fn cli_opts_layer_serve(cli: ConfigOptsServe, cfg_base: Self) -> Self {
let opts = ConfigOptsServe {
port: cli.port,
open: cli.open,
proxy_backend: cli.proxy_backend,
proxy_rewrite: cli.proxy_rewrite,
};
let cfg = ConfigOpts {
build: None,
watch: None,
serve: Some(opts),
clean: None,
proxy: None,
};
Self::merge(cfg_base, cfg)
}
fn cli_opts_layer_clean(cli: ConfigOptsClean, cfg_base: Self) -> Self {
let opts = ConfigOptsClean {
dist: cli.dist,
cargo: cli.cargo,
};
let cfg = ConfigOpts {
build: None,
watch: None,
serve: None,
clean: Some(opts),
proxy: None,
};
Self::merge(cfg_base, cfg)
}
fn file_and_env_layers(path: Option<PathBuf>) -> Result<Self> {
let toml_cfg = Self::from_file(path)?;
let env_cfg = Self::from_env().context("error reading trunk env var config")?;
let cfg = Self::merge(toml_cfg, env_cfg);
Ok(cfg)
}
fn from_file(path: Option<PathBuf>) -> Result<Self> {
let mut path = path.unwrap_or_else(|| "Trunk.toml".into());
if !path.exists() {
return Ok(Default::default());
}
if !path.is_absolute() {
path = path
.canonicalize()
.with_context(|| format!("error getting canonical path to Trunk config file {:?}", &path))?;
}
let cfg_bytes = std::fs::read(&path).context("error reading config file")?;
let mut cfg: Self = toml::from_slice(&cfg_bytes).context("error reading config file contents as TOML data")?;
if let Some(parent) = path.parent() {
cfg.build.iter_mut().for_each(|build| {
build.target.iter_mut().for_each(|target| {
if !target.is_absolute() {
*target = parent.join(&target);
}
});
build.dist.iter_mut().for_each(|dist| {
if !dist.is_absolute() {
*dist = parent.join(&dist);
}
});
});
cfg.watch.iter_mut().for_each(|watch| {
watch.ignore.iter_mut().for_each(|ignores_vec| {
ignores_vec.iter_mut().for_each(|ignore_path| {
if !ignore_path.is_absolute() {
*ignore_path = parent.join(&ignore_path);
}
});
});
});
cfg.clean.iter_mut().for_each(|clean| {
clean.dist.iter_mut().for_each(|dist| {
if !dist.is_absolute() {
*dist = parent.join(&dist);
}
});
});
}
Ok(cfg)
}
fn from_env() -> Result<Self> {
let build: ConfigOptsBuild = envy::prefixed("TRUNK_BUILD_").from_env()?;
let watch: ConfigOptsWatch = envy::prefixed("TRUNK_WATCH_").from_env()?;
let serve: ConfigOptsServe = envy::prefixed("TRUNK_SERVE_").from_env()?;
let clean: ConfigOptsClean = envy::prefixed("TRUNK_CLEAN_").from_env()?;
Ok(ConfigOpts {
build: Some(build),
watch: Some(watch),
serve: Some(serve),
clean: Some(clean),
proxy: None,
})
}
fn merge(mut lesser: Self, mut greater: Self) -> Self {
greater.build = match (lesser.build.take(), greater.build.take()) {
(None, None) => None,
(Some(val), None) | (None, Some(val)) => Some(val),
(Some(l), Some(mut g)) => {
g.target = g.target.or(l.target);
g.dist = g.dist.or(l.dist);
g.public_url = g.public_url.or(l.public_url);
if l.release {
g.release = true
}
Some(g)
}
};
greater.watch = match (lesser.watch.take(), greater.watch.take()) {
(None, None) => None,
(Some(val), None) | (None, Some(val)) => Some(val),
(Some(l), Some(mut g)) => {
g.ignore = g.ignore.or(l.ignore);
Some(g)
}
};
greater.serve = match (lesser.serve.take(), greater.serve.take()) {
(None, None) => None,
(Some(val), None) | (None, Some(val)) => Some(val),
(Some(l), Some(mut g)) => {
g.proxy_backend = g.proxy_backend.or(l.proxy_backend);
g.proxy_rewrite = g.proxy_rewrite.or(l.proxy_rewrite);
g.port = g.port.or(l.port);
if l.open {
g.open = true
}
Some(g)
}
};
greater.clean = match (lesser.clean.take(), greater.clean.take()) {
(None, None) => None,
(Some(val), None) | (None, Some(val)) => Some(val),
(Some(l), Some(mut g)) => {
g.dist = g.dist.or(l.dist);
if l.cargo {
g.cargo = true
}
Some(g)
}
};
greater.proxy = match (lesser.proxy.take(), greater.proxy.take()) {
(None, None) => None,
(Some(val), None) | (None, Some(val)) => Some(val),
(Some(_), Some(g)) => Some(g), };
greater
}
}