vexil_codegen_ts/
backend.rs1use std::collections::{BTreeMap, HashMap, HashSet};
2use std::path::PathBuf;
3
4use vexil_lang::codegen::{CodegenBackend, CodegenError};
5use vexil_lang::ir::{CompiledSchema, ResolvedType, TypeDef, TypeId};
6use vexil_lang::project::ProjectResult;
7
8#[derive(Debug, Clone, Copy)]
13pub struct TypeScriptBackend;
14
15impl CodegenBackend for TypeScriptBackend {
16 fn name(&self) -> &str {
17 "typescript"
18 }
19
20 fn file_extension(&self) -> &str {
21 "ts"
22 }
23
24 fn generate(&self, compiled: &CompiledSchema) -> Result<String, CodegenError> {
25 crate::generate(compiled).map_err(|e| CodegenError::BackendSpecific(Box::new(e)))
26 }
27
28 fn generate_project(
29 &self,
30 result: &ProjectResult,
31 ) -> Result<BTreeMap<PathBuf, String>, CodegenError> {
32 let mut files = BTreeMap::new();
33 let mut index_tree: BTreeMap<String, Vec<String>> = BTreeMap::new();
34
35 let mut global_type_map: HashMap<String, String> = HashMap::new();
37 for (ns, compiled) in &result.schemas {
38 let segments: Vec<&str> = ns.split('.').collect();
39 let ts_module = format!("./{}", segments.join("/"));
40 for &type_id in &compiled.declarations {
41 if let Some(typedef) = compiled.registry.get(type_id) {
42 let name = crate::type_name_of(typedef);
43 global_type_map.insert(name.to_string(), ts_module.clone());
44 }
45 }
46 }
47
48 for (ns, compiled) in &result.schemas {
49 let segments: Vec<&str> = ns.split('.').collect();
50 if segments.is_empty() {
51 continue;
52 }
53 let file_name = segments[segments.len() - 1];
54 let dir_segments = &segments[..segments.len() - 1];
55
56 for i in 0..segments.len() - 1 {
58 let parent_key = segments[..i].join("/");
59 let child = segments[i].to_string();
60 let entry = index_tree.entry(parent_key).or_default();
61 if !entry.contains(&child) {
62 entry.push(child);
63 }
64 }
65 if segments.len() >= 2 {
66 let parent_key = dir_segments.join("/");
67 let child = file_name.to_string();
68 let entry = index_tree.entry(parent_key).or_default();
69 if !entry.contains(&child) {
70 entry.push(child);
71 }
72 } else {
73 let entry = index_tree.entry(String::new()).or_default();
74 let child = file_name.to_string();
75 if !entry.contains(&child) {
76 entry.push(child);
77 }
78 }
79
80 let declared_ids: HashSet<TypeId> = compiled.declarations.iter().copied().collect();
82
83 let mut import_paths: HashMap<String, String> = HashMap::new();
84 for &type_id in &compiled.declarations {
85 if let Some(typedef) = compiled.registry.get(type_id) {
86 collect_named_ids_from_typedef(typedef, &declared_ids, |imported_id| {
87 if let Some(imported_def) = compiled.registry.get(imported_id) {
88 let name = crate::type_name_of(imported_def);
89 if let Some(ts_path) = global_type_map.get(name) {
90 import_paths.insert(name.to_string(), ts_path.clone());
91 }
92 }
93 });
94 }
95 }
96
97 let imports = if import_paths.is_empty() {
99 None
100 } else {
101 Some(&import_paths)
102 };
103 let code = crate::generate_with_imports(compiled, imports)
104 .map_err(|e| CodegenError::BackendSpecific(Box::new(e)))?;
105
106 let mut file_path = PathBuf::new();
107 for seg in dir_segments {
108 file_path.push(seg);
109 }
110 file_path.push(format!("{file_name}.ts"));
111 files.insert(file_path, code);
112 }
113
114 for (dir_key, children) in &index_tree {
116 let mut index_path = PathBuf::new();
117 if !dir_key.is_empty() {
118 for seg in dir_key.split('/') {
119 index_path.push(seg);
120 }
121 }
122 index_path.push("index.ts");
123
124 let mut content = String::from("// Code generated by vexilc. DO NOT EDIT.\n\n");
125 for child in children {
126 content.push_str(&format!("export * from './{child}';\n"));
127 }
128 files.insert(index_path, content);
129 }
130
131 Ok(files)
132 }
133}
134
135fn collect_named_ids_from_typedef(
138 typedef: &TypeDef,
139 declared: &HashSet<TypeId>,
140 mut on_import: impl FnMut(TypeId),
141) {
142 match typedef {
143 TypeDef::Message(msg) => {
144 for f in &msg.fields {
145 collect_named_ids_from_resolved(&f.resolved_type, declared, &mut on_import);
146 }
147 }
148 TypeDef::Union(un) => {
149 for v in &un.variants {
150 for f in &v.fields {
151 collect_named_ids_from_resolved(&f.resolved_type, declared, &mut on_import);
152 }
153 }
154 }
155 TypeDef::Newtype(nt) => {
156 collect_named_ids_from_resolved(&nt.inner_type, declared, &mut on_import);
157 }
158 TypeDef::Config(cfg) => {
159 for f in &cfg.fields {
160 collect_named_ids_from_resolved(&f.resolved_type, declared, &mut on_import);
161 }
162 }
163 _ => {}
164 }
165}
166
167fn collect_named_ids_from_resolved(
168 ty: &ResolvedType,
169 declared: &HashSet<TypeId>,
170 on_import: &mut impl FnMut(TypeId),
171) {
172 match ty {
173 ResolvedType::Named(id) => {
174 if !declared.contains(id) {
175 on_import(*id);
176 }
177 }
178 ResolvedType::Optional(inner) | ResolvedType::Array(inner) => {
179 collect_named_ids_from_resolved(inner, declared, on_import);
180 }
181 ResolvedType::Map(k, v) | ResolvedType::Result(k, v) => {
182 collect_named_ids_from_resolved(k, declared, on_import);
183 collect_named_ids_from_resolved(v, declared, on_import);
184 }
185 _ => {}
186 }
187}