ev_lib_gen 0.1.4

Generates the TS uikit class tables from ev_lib_classes (Rust is the source of truth).
#![feature(default_field_values)]
//! Emits one TS file per uikit component under `ts/uikit/src/generated/` from the
//! Rust styling source of truth (`ev_lib_classes`). Run via `cargo run -p
//! ev_lib_gen`; the output is committed and CI fails if it drifts
//! (`git diff --exit-code ts/uikit/src/generated`).

use std::{fmt::Write as _, fs, path::Path};

use strum::IntoEnumIterator;
use tailwind_fuse::AsTailwindClass;

mod manifest;

fn main() {
	let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
	let dir = root.join("ts/uikit/src/generated");
	fs::create_dir_all(&dir).expect("create generated dir");

	for (component, exports) in manifest::manifest() {
		let mut out = String::from("// AUTO-GENERATED by `cargo run -p ev_lib_gen` — do not edit.\n\n");
		for (i, ts) in exports.iter().enumerate() {
			if i > 0 {
				out.push('\n');
			}
			ts.render(&mut out);
		}
		let path = dir.join(format!("{component}.ts"));
		fs::write(&path, out).expect("write generated component file");
		println!("wrote {}", path.display());
	}
}
/// One TS export. The serializer turns each into a single statement; `manifest`
/// lists them per component so adding a class string is one flat line there.
enum Ts {
	Const {
		name: &'static str,
		value: &'static str,
	},
	Table {
		name: &'static str,
		ty: &'static str,
		entries: Vec<(String, String)>,
	},
}

impl Ts {
	fn render(&self, out: &mut String) {
		match self {
			// The `{:?}` Debug formatting doubles as a TS string-literal quoter.
			Ts::Const { name, value } => writeln!(out, "export const {name} = {value:?};").unwrap(),
			Ts::Table { name, ty, entries } => {
				writeln!(out, "export const {name} = {{").unwrap();
				for (key, class) in entries {
					writeln!(out, "  {key:?}: {class:?},").unwrap();
				}
				out.push_str("} as const;\n");
				writeln!(out, "export type {ty} = keyof typeof {name};").unwrap();
			}
		}
	}
}

/// Any `TwVariant` enum → a `{ kebab-key: "class" }` table. Base classes are split
/// into a `*_BASE` const, so `as_class()` yields the per-variant string only.
fn table<T>(name: &'static str, ty: &'static str) -> Ts
where
	T: IntoEnumIterator + AsRef<str> + AsTailwindClass, {
	Ts::Table {
		name,
		ty,
		entries: T::iter().map(|v| (v.as_ref().to_string(), v.as_class().to_string())).collect(),
	}
}