Skip to main content

vexil_codegen_ts/
lib.rs

1pub mod backend;
2pub mod emit;
3pub mod enum_gen;
4pub mod flags;
5pub mod message;
6pub mod newtype;
7pub mod types;
8pub mod union_gen;
9
10pub use backend::TypeScriptBackend;
11
12use vexil_lang::ir::{CompiledSchema, TypeDef};
13
14#[derive(Debug, Clone, PartialEq, thiserror::Error)]
15pub enum CodegenError {
16    #[error("unresolved type {type_id:?} referenced by {referenced_by}")]
17    UnresolvedType {
18        type_id: vexil_lang::ir::TypeId,
19        referenced_by: String,
20    },
21}
22
23/// Generate TypeScript code for a compiled schema (no cross-file imports).
24pub fn generate(compiled: &CompiledSchema) -> Result<String, CodegenError> {
25    generate_with_imports(compiled, None)
26}
27
28/// Generate TypeScript code for a compiled schema, with optional cross-file import statements.
29///
30/// `import_paths` maps type names to their module paths for cross-file imports.
31pub(crate) fn generate_with_imports(
32    compiled: &CompiledSchema,
33    import_paths: Option<&std::collections::HashMap<String, String>>,
34) -> Result<String, CodegenError> {
35    let mut w = emit::CodeWriter::new();
36
37    // Header
38    w.line("// Code generated by vexilc. DO NOT EDIT.");
39    let ns = compiled.namespace.join(".");
40    w.line(&format!("// Source: {ns}"));
41    w.blank();
42
43    // Runtime import
44    w.line("import { BitReader, BitWriter } from '@vexil/runtime';");
45
46    // Cross-file imports
47    if let Some(paths) = import_paths {
48        let mut imports: Vec<(&String, &String)> = paths.iter().collect();
49        imports.sort_by_key(|(name, _)| (*name).clone());
50        for (type_name, module_path) in imports {
51            w.line(&format!(
52                "import {{ {type_name}, encode{type_name}, decode{type_name} }} from '{module_path}';",
53            ));
54        }
55    }
56    w.blank();
57
58    // Schema hash
59    let hash = vexil_lang::canonical::schema_hash(compiled);
60    let hash_str = hash
61        .iter()
62        .map(|b| format!("0x{b:02x}"))
63        .collect::<Vec<_>>()
64        .join(", ");
65    w.line(&format!(
66        "export const SCHEMA_HASH = new Uint8Array([{hash_str}]);"
67    ));
68
69    // Schema version
70    if let Some(ref version) = compiled.annotations.version {
71        w.line(&format!("export const SCHEMA_VERSION = '{version}';"));
72    }
73
74    // Emit each declared type
75    for &type_id in &compiled.declarations {
76        let typedef =
77            compiled
78                .registry
79                .get(type_id)
80                .ok_or_else(|| CodegenError::UnresolvedType {
81                    type_id,
82                    referenced_by: "declarations".to_string(),
83                })?;
84
85        w.blank();
86        let type_name = type_name_of(typedef);
87        w.line(&format!("// \u{2500}\u{2500} {type_name} \u{2500}\u{2500}"));
88
89        match typedef {
90            TypeDef::Message(msg) => {
91                message::emit_message(&mut w, msg, &compiled.registry);
92            }
93            TypeDef::Enum(en) => {
94                enum_gen::emit_enum(&mut w, en, &compiled.registry);
95            }
96            TypeDef::Flags(fl) => {
97                flags::emit_flags(&mut w, fl, &compiled.registry);
98            }
99            TypeDef::Union(un) => {
100                union_gen::emit_union(&mut w, un, &compiled.registry);
101            }
102            TypeDef::Newtype(nt) => {
103                newtype::emit_newtype(&mut w, nt, &compiled.registry);
104            }
105            TypeDef::Config(cfg) => {
106                message::emit_config(&mut w, cfg, &compiled.registry);
107            }
108            _ => {} // non_exhaustive guard
109        }
110    }
111
112    Ok(w.finish())
113}
114
115/// Returns the name of a TypeDef for use in section separator comments.
116pub(crate) fn type_name_of(typedef: &TypeDef) -> &str {
117    match typedef {
118        TypeDef::Message(m) => m.name.as_str(),
119        TypeDef::Enum(e) => e.name.as_str(),
120        TypeDef::Flags(f) => f.name.as_str(),
121        TypeDef::Union(u) => u.name.as_str(),
122        TypeDef::Newtype(n) => n.name.as_str(),
123        TypeDef::Config(c) => c.name.as_str(),
124        _ => "Unknown",
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn generate_produces_output() {
134        let result = vexil_lang::compile("namespace test.gen\nmessage Foo { x @0 : u32 }");
135        let compiled = result.compiled.unwrap();
136        let code = generate(&compiled).unwrap();
137        assert!(code.contains("export interface Foo"));
138        assert!(code.contains("export function encodeFoo"));
139        assert!(code.contains("export function decodeFoo"));
140        assert!(code.contains("import { BitReader, BitWriter } from '@vexil/runtime'"));
141    }
142
143    #[test]
144    fn generate_with_no_imports_matches_generate() {
145        let result = vexil_lang::compile("namespace test.gen\nmessage Foo { x @0 : u32 }");
146        let compiled = result.compiled.unwrap();
147        let without = generate(&compiled).unwrap();
148        let with = generate_with_imports(&compiled, None).unwrap();
149        assert_eq!(without, with);
150    }
151
152    #[test]
153    fn ts_backend_generate_matches_free_function() {
154        use vexil_lang::codegen::CodegenBackend;
155        let result = vexil_lang::compile("namespace test.backend\nmessage Foo { x @0 : u32 }");
156        let compiled = result.compiled.unwrap();
157        let free_fn = generate(&compiled).unwrap();
158        let backend = crate::backend::TypeScriptBackend;
159        let trait_fn = backend.generate(&compiled).unwrap();
160        assert_eq!(free_fn, trait_fn);
161    }
162}