use std::any::TypeId;
use std::collections::{BTreeMap, HashMap};
use std::fmt::Write as _;
use std::io;
use std::path::Path;
mod routes;
pub use routes::{register_runtime_route, registered_routes, RouteEntry};
pub trait InertiaPageProps {
const COMPONENT: &'static str;
}
#[macro_export]
macro_rules! register_page {
($ty:ty, $component:literal) => {
impl $crate::bindings::InertiaPageProps for $ty {
const COMPONENT: &'static str = $component;
}
$crate::__private::inventory::submit! {
$crate::bindings::PageEntry {
component: $component,
ts_name: || <$ty as $crate::__private::ts_rs::TS>::ident(),
collect_decls: |out| $crate::bindings::collect_decls::<$ty>(out),
}
}
};
}
pub struct PageEntry {
pub component: &'static str,
pub ts_name: fn() -> String,
pub collect_decls: fn(&mut HashMap<TypeId, (String, String)>),
}
inventory::collect!(PageEntry);
pub fn collect_decls<T: ts_rs::TS + 'static + ?Sized>(out: &mut HashMap<TypeId, (String, String)>) {
let id = TypeId::of::<T>();
if out.contains_key(&id) {
return;
}
if T::output_path().is_some() {
out.insert(id, (T::ident(), T::decl()));
} else {
out.insert(id, (String::new(), String::new()));
}
struct Visit<'a> {
out: &'a mut HashMap<TypeId, (String, String)>,
}
impl ts_rs::TypeVisitor for Visit<'_> {
fn visit<U: ts_rs::TS + 'static + ?Sized>(&mut self) {
collect_decls::<U>(self.out);
}
}
T::visit_dependencies(&mut Visit { out });
}
#[derive(Debug, thiserror::Error)]
pub enum GenerateError {
#[error("write: {0}")]
Io(#[from] io::Error),
}
pub fn generate(out: impl AsRef<Path>) -> Result<(), GenerateError> {
let out = out.as_ref();
if let Some(parent) = out.parent() {
std::fs::create_dir_all(parent)?;
}
let body = render();
std::fs::write(out, body)?;
Ok(())
}
pub fn generate_split(dir: impl AsRef<Path>) -> Result<(), GenerateError> {
Split::new(dir).generate()
}
pub struct Split {
dir: std::path::PathBuf,
actions_dir: String,
file_prefix: String,
file_suffix: String,
}
impl Split {
pub fn new(dir: impl AsRef<Path>) -> Self {
Self {
dir: dir.as_ref().to_path_buf(),
actions_dir: "actions".into(),
file_prefix: String::new(),
file_suffix: String::new(),
}
}
pub fn actions_dir(mut self, name: impl Into<String>) -> Self {
self.actions_dir = name.into();
self
}
pub fn file_prefix(mut self, prefix: impl Into<String>) -> Self {
self.file_prefix = prefix.into();
self
}
pub fn file_suffix(mut self, suffix: impl Into<String>) -> Self {
self.file_suffix = suffix.into();
self
}
pub fn generate(&self) -> Result<(), GenerateError> {
let actions_path = if self.actions_dir.is_empty() {
self.dir.clone()
} else {
self.dir.join(&self.actions_dir)
};
std::fs::create_dir_all(&actions_path)?;
let controllers = routes::render_per_controller();
for (name, body) in &controllers {
let mut file = String::new();
file.push_str(HEADER);
file.push('\n');
file.push_str(body);
let filename = format!("{}{}{}.ts", self.file_prefix, name, self.file_suffix);
std::fs::write(actions_path.join(filename), file)?;
}
let mut index = String::new();
index.push_str(HEADER);
index.push('\n');
index.push_str(PROTOCOL_TYPES);
index.push('\n');
index.push_str(&render_prop_decls_and_pages());
index.push('\n');
for name in controllers.keys() {
let rel = if self.actions_dir.is_empty() {
format!("./{}{}{}", self.file_prefix, name, self.file_suffix)
} else {
format!(
"./{}/{}{}{}",
self.actions_dir, self.file_prefix, name, self.file_suffix
)
};
let _ = writeln!(index, "export * as {name} from \"{rel}\";");
}
std::fs::write(self.dir.join("index.ts"), index)?;
Ok(())
}
}
pub fn render() -> String {
let mut s = String::new();
s.push_str(HEADER);
s.push('\n');
s.push_str(PROTOCOL_TYPES);
s.push('\n');
s.push_str(&render_prop_decls_and_pages());
s.push('\n');
s.push_str(&routes::render_routes());
s
}
fn render_prop_decls_and_pages() -> String {
let mut s = String::new();
let mut decls: HashMap<TypeId, (String, String)> = HashMap::new();
let mut pages: BTreeMap<&'static str, String> = BTreeMap::new();
for entry in inventory::iter::<PageEntry>() {
(entry.collect_decls)(&mut decls);
pages.insert(entry.component, (entry.ts_name)());
}
let mut ordered: Vec<(String, String)> = decls
.into_values()
.filter(|(name, _)| !name.is_empty())
.collect();
ordered.sort_by(|a, b| a.0.cmp(&b.0));
for (_, decl) in &ordered {
s.push_str("export ");
s.push_str(decl);
s.push('\n');
s.push('\n');
}
s.push_str("export type Pages =\n");
if pages.is_empty() {
s.push_str(" never;\n");
} else {
let mut iter = pages.iter().peekable();
while let Some((component, ts_name)) = iter.next() {
let _ = write!(s, " | {{ component: {component:?}; props: {ts_name} }}");
if iter.peek().is_some() {
s.push('\n');
} else {
s.push_str(";\n");
}
}
}
s
}
const HEADER: &str = "// This file is auto-generated by veer. Do not edit manually.\n\
// Run `cargo run --bin gen-bindings` to regenerate.\n";
const PROTOCOL_TYPES: &str = r#"
export interface PageObject<P = Pages> {
component: P extends { component: infer C } ? C : string;
props: P extends { props: infer Props } ? Props : Record<string, unknown>;
url: string;
version: string;
encryptHistory?: boolean;
clearHistory?: boolean;
mergeProps?: string[];
resetMergeProps?: string[];
deferredProps?: Record<string, string[]>;
}
export type ErrorBag = Record<string, string>;
export interface Flash {
errors: ErrorBag;
bags: Record<string, unknown>;
}
"#;