#![forbid(unsafe_code)]
pub mod build;
use fieldwork::Fieldwork;
use std::borrow::Cow;
#[cfg(feature = "dev-proxy")]
use std::{
io::ErrorKind,
process::{Child, Command},
sync::Mutex,
time::Duration,
};
#[cfg(feature = "dev-proxy")]
use trillium::Upgrade;
use trillium::{Conn, Handler, Info, Method};
use trillium_client::Client;
#[cfg(feature = "dev-proxy")]
use trillium_proxy::{Connector, Proxy, Url};
use trillium_static_compiled::StaticCompiledHandler;
#[derive(Fieldwork)]
#[fieldwork(opt_in, with, into, option_set_some)]
pub struct FrontendHandler {
assets: Option<StaticCompiledHandler>,
spa_fallback: Option<StaticCompiledHandler>,
#[allow(dead_code)]
project_path: Cow<'static, str>,
#[allow(dead_code)]
detected_dev_command: Option<Cow<'static, str>>,
#[allow(dead_code)]
#[field]
client: Option<Client>,
#[allow(dead_code)]
#[field]
dev_command: Option<Cow<'static, str>>,
#[allow(dead_code)]
#[field(into = false)]
dev_port: Option<u16>,
#[field]
index_file: Option<&'static str>,
#[field(into = false)]
index_predicate: Option<fn(&Conn) -> bool>,
#[cfg(feature = "dev-proxy")]
dev_process: Option<Mutex<Child>>,
#[cfg(feature = "dev-proxy")]
proxy: Option<Proxy<Url>>,
}
impl FrontendHandler {
#[doc(hidden)]
pub fn new(
assets: Option<StaticCompiledHandler>,
spa_fallback: Option<StaticCompiledHandler>,
project_path: &'static str,
detected_dev_command: Option<&'static str>,
) -> Self {
FrontendHandler {
assets,
spa_fallback,
project_path: Cow::Borrowed(project_path),
detected_dev_command: detected_dev_command.map(Cow::Borrowed),
client: None,
dev_command: None,
dev_port: None,
index_file: None,
index_predicate: None,
#[cfg(feature = "dev-proxy")]
dev_process: None,
#[cfg(feature = "dev-proxy")]
proxy: None,
}
}
}
impl FrontendHandler {
fn serves_index(&self, conn: &Conn) -> bool {
matches!(conn.method(), Method::Get | Method::Head)
&& self.index_predicate.is_none_or(|predicate| predicate(conn))
}
}
impl Handler for FrontendHandler {
async fn run(&self, conn: Conn) -> Conn {
if let Some(assets) = self.assets {
let conn = assets.run(conn).await;
if !conn.is_halted()
&& let Some(spa) = self.spa_fallback.as_ref()
&& self.serves_index(&conn)
{
return spa.run(conn).await;
}
return conn;
}
#[cfg(feature = "dev-proxy")]
if let Some(proxy) = &self.proxy {
return proxy.run(conn).await;
}
conn
}
async fn init(&mut self, #[allow(unused)] info: &mut Info) {
if self.assets.is_some()
&& let Some(index) = self.index_file
{
self.assets = self.assets.map(|h| h.with_index_file(index));
}
#[cfg(feature = "dev-proxy")]
{
let client = self.client.take().expect(
"trillium-frontend: in dev-proxy mode, provide a Client via .with_client()",
);
let port = self.dev_port.unwrap_or_else(|| {
portpicker::pick_unused_port()
.expect("trillium-frontend: could not find a free port for the dev server")
});
let dev_command = if let Some(cmd) = self.dev_command.as_deref() {
cmd.to_string()
} else {
let detected = self.detected_dev_command.as_deref().expect(
"trillium-frontend: no dev command detected; configure with \
.with_dev_command()",
);
format!("{detected} --port {port}")
};
let child = Command::new("sh")
.arg("-c")
.arg(&dev_command)
.env("PORT", port.to_string())
.current_dir(self.project_path.as_ref())
.spawn()
.expect("trillium-frontend: failed to spawn dev server");
self.dev_process = Some(Mutex::new(child));
let upstream = format!("http://localhost:{port}").parse().unwrap();
wait_for_port(&upstream, &client).await;
let mut proxy = Proxy::new(client, upstream).with_websocket_upgrades();
proxy.init(info).await;
self.proxy = Some(proxy);
}
}
#[cfg(feature = "dev-proxy")]
fn has_upgrade(&self, upgrade: &Upgrade) -> bool {
self.proxy.as_ref().is_some_and(|p| p.has_upgrade(upgrade))
}
#[cfg(feature = "dev-proxy")]
async fn upgrade(&self, upgrade: Upgrade) {
if let Some(proxy) = &self.proxy {
proxy.upgrade(upgrade).await;
}
}
}
#[cfg(feature = "dev-proxy")]
impl Drop for FrontendHandler {
fn drop(&mut self) {
if let Some(mutex) = self.dev_process.take()
&& let Ok(mut child) = mutex.into_inner()
{
let _ = child.kill();
}
}
}
#[cfg(feature = "dev-proxy")]
async fn wait_for_port(upstream: &Url, client: &Client) {
for _ in 0..100 {
log::trace!("testing connection");
match client.connector().connect(upstream).await {
Ok(_) => {
log::trace!("tcp connected");
return;
}
Err(e) if e.kind() == ErrorKind::ConnectionRefused => {
log::trace!("Could not connect to {upstream} yet, sleeping 10ms");
client
.connector()
.runtime()
.delay(Duration::from_millis(10))
.await;
}
Err(other) => {
panic!("trillium-frontend was unable to connect to the dev server: {other}");
}
}
}
}
#[doc(hidden)]
pub mod __macro_internals {
pub use crate::FrontendHandler;
pub use trillium_frontend_macros::frontend_impl;
pub use trillium_static_compiled::static_compiled;
}
#[macro_export]
macro_rules! frontend {
($($tt:tt)*) => {{
use $crate::__macro_internals::{FrontendHandler, static_compiled};
$crate::__macro_internals::frontend_impl!($($tt)*)
}};
}