use crate::{
config::{
self,
models::Proxy,
rt::{self, RtcBuilder, RtcServe},
types::{AddressFamily, WsProtocol},
Configuration,
},
serve::ServeSystem,
};
use anyhow::{Context, Result};
use axum::http::Uri;
use clap::Args;
use std::{net::IpAddr, path::PathBuf, sync::Arc};
use tokio::{select, sync::broadcast};
#[derive(Clone, Args)]
#[command(name = "serve")]
#[command(next_help_heading = "Serve")]
pub struct Serve {
#[arg(short, long, env = "TRUNK_SERVE_ADDRESS")]
pub address: Option<Vec<IpAddr>>,
#[arg(short = 'A', long, env = "TRUNK_SERVE_PREFER_ADDRESS_FAMILY")]
pub prefer_address_family: Option<AddressFamily>,
#[arg(short, long, env = "TRUNK_SERVE_PORT")]
pub port: Option<u16>,
#[arg(long, env = "TRUNK_SERVE_ALIAS")]
pub alias: Option<Vec<String>>,
#[arg(long, env = "TRUNK_SERVE_DISABLE_ADDRESS_LOOKUP")]
#[arg(default_missing_value="true", num_args=0..=1)]
pub disable_address_lookup: Option<bool>,
#[arg(long, env = "TRUNK_SERVE_OPEN")]
#[arg(default_missing_value="true", num_args=0..=1)]
pub open: Option<bool>,
#[arg(long, env = "TRUNK_SERVE_NO_AUTORELOAD")]
#[arg(default_missing_value="true", num_args=0..=1)]
pub no_autoreload: Option<bool>,
#[arg(long, env = "TRUNK_SERVE_NO_ERROR_REPORTING")]
#[arg(default_missing_value="true", num_args=0..=1)]
pub no_error_reporting: Option<bool>,
#[arg(long, env = "TRUNK_SERVE_NO_SPA")]
#[arg(default_missing_value="true", num_args=0..=1)]
pub no_spa: Option<bool>,
#[arg(long, env = "TRUNK_SERVE_WS_PROTOCOL")]
pub ws_protocol: Option<WsProtocol>,
#[arg(long, env = "TRUNK_SERVE_WS_BASE")]
pub ws_base: Option<String>,
#[arg(long, env = "TRUNK_SERVE_TLS_KEY_PATH")]
pub tls_key_path: Option<PathBuf>,
#[arg(long, env = "TRUNK_SERVE_TLS_CERT_PATH")]
pub tls_cert_path: Option<PathBuf>,
#[arg(long, env = "TRUNK_SERVE_SERVE_BASE")]
pub serve_base: Option<String>,
#[arg(long)]
#[arg(default_missing_value="false", num_args=0..=1)]
pub disable_csp: Option<bool>,
#[command(flatten)]
pub proxy: ProxyArgs,
#[command(flatten)]
pub watch: super::watch::Watch,
}
#[derive(Clone, Debug, Default, Args)]
#[command(next_help_heading = "Backend Proxy")]
pub struct ProxyArgs {
#[arg(long, env = "TRUNK_SERVE_PROXY_BACKEND")]
pub proxy_backend: Option<Uri>,
#[arg(long, env = "TRUNK_SERVE_PROXY_REWRITE", requires = "proxy_backend")]
pub proxy_rewrite: Option<String>,
#[arg(long, env = "TRUNK_SERVE_PROXY_WS", requires = "proxy_backend")]
pub proxy_ws: bool,
#[arg(long, env = "TRUNK_SERVE_PROXY_INSECURE", requires = "proxy_backend")]
pub proxy_insecure: bool,
#[arg(
long,
env = "TRUNK_SERVE_PROXY_NO_SYSTEM_PROXY",
requires = "proxy_backend"
)]
pub proxy_no_system_proxy: bool,
#[arg(
long,
env = "TRUNK_SERVE_PROXY_NO_REDIRECT",
requires = "proxy_backend"
)]
pub proxy_no_redirect: bool,
}
impl Serve {
fn apply_to(self, mut config: Configuration) -> Result<Configuration> {
let Self {
address,
prefer_address_family,
port,
alias,
disable_address_lookup,
open,
proxy:
ProxyArgs {
proxy_backend,
proxy_rewrite,
proxy_ws,
proxy_insecure,
proxy_no_system_proxy,
proxy_no_redirect,
},
no_autoreload,
no_error_reporting,
no_spa,
ws_protocol,
ws_base,
tls_key_path,
tls_cert_path,
serve_base,
watch,
disable_csp,
} = self;
config.serve.addresses = address.unwrap_or(config.serve.addresses);
config.serve.port = port.unwrap_or(config.serve.port);
config.serve.aliases = alias.unwrap_or(config.serve.aliases);
config.serve.disable_address_lookup =
disable_address_lookup.unwrap_or(config.serve.disable_address_lookup);
config.serve.open = open.unwrap_or(config.serve.open);
config.serve.prefer_address_family =
prefer_address_family.or(config.serve.prefer_address_family);
config.serve.serve_base = serve_base.or(config.serve.serve_base);
config.serve.tls_key_path = tls_key_path.or(config.serve.tls_key_path);
config.serve.tls_cert_path = tls_cert_path.or(config.serve.tls_cert_path);
config.serve.no_autoreload = no_autoreload.unwrap_or(config.serve.no_autoreload);
config.serve.no_error_reporting =
no_error_reporting.unwrap_or(config.serve.no_error_reporting);
config.serve.no_spa = no_spa.unwrap_or(config.serve.no_spa);
config.serve.ws_protocol = ws_protocol.or(config.serve.ws_protocol);
config.serve.ws_base = ws_base.or(config.serve.ws_base);
config.serve.disable_csp = disable_csp.unwrap_or(config.serve.disable_csp);
if let Some(backend) = proxy_backend {
config.proxies.0.push(Proxy {
backend: backend.into(),
request_headers: Default::default(),
rewrite: proxy_rewrite,
ws: proxy_ws,
insecure: proxy_insecure,
no_system_proxy: proxy_no_system_proxy,
no_redirect: proxy_no_redirect,
});
}
let config = watch.apply_to(config)?;
Ok(config)
}
#[tracing::instrument(level = "trace", skip(self, config))]
pub async fn run(self, config: Option<PathBuf>) -> Result<()> {
let (cfg, working_directory) = config::load(config).await?;
let cfg = self.clone().apply_to(cfg)?;
let cfg = RtcServe::from_config(cfg, working_directory, |cfg, core| rt::ServeOptions {
watch: rt::WatchOptions {
build: rt::BuildOptions {
core,
inject_autoloader: !cfg.serve.no_autoreload,
},
poll: self.watch.poll.then_some(self.watch.poll_interval.0),
enable_cooldown: self.watch.enable_cooldown,
clear_screen: self.watch.clear_screen,
no_error_reporting: cfg.serve.no_error_reporting,
},
open: self.open.unwrap_or(cfg.serve.open),
})
.await?;
cfg.enforce_version()?;
let (shutdown_tx, _) = broadcast::channel(1);
let system = ServeSystem::new(Arc::new(cfg), shutdown_tx.clone()).await?;
let system_handle = tokio::spawn(system.run());
select! {
_ = tokio::signal::ctrl_c() => {
tracing::debug!("received shutdown signal");
shutdown_tx.send(()).ok();
drop(shutdown_tx);
}
r = system_handle => {
r.context("error awaiting system shutdown")??;
}
}
tracing::debug!("Exiting serve main");
Ok(())
}
}