Skip to main content

vexil_codegen_ts/
lib.rs

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