1use anyhow::Result;
2use camino::Utf8Path;
3use weaveffi_core::codegen::Generator;
4use weaveffi_ir::ir::{Api, TypeRef};
5
6pub struct NodeGenerator;
7
8impl Generator for NodeGenerator {
9 fn name(&self) -> &'static str {
10 "node"
11 }
12
13 fn generate(&self, api: &Api, out_dir: &Utf8Path) -> Result<()> {
14 let dir = out_dir.join("node");
15 std::fs::create_dir_all(&dir)?;
16 std::fs::write(
17 dir.join("index.js"),
18 "module.exports = require('./index.node')\n",
19 )?;
20 std::fs::write(dir.join("types.d.ts"), render_node_dts(api))?;
21 std::fs::write(
22 dir.join("package.json"),
23 "{\n \"name\": \"weaveffi\",\n \"version\": \"0.1.0\",\n \"main\": \"index.js\",\n \"types\": \"types.d.ts\"\n}\n",
24 )?;
25 Ok(())
26 }
27}
28
29fn ts_type_for(ty: &TypeRef) -> String {
30 match ty {
31 TypeRef::I32 | TypeRef::U32 | TypeRef::I64 | TypeRef::F64 => "number".into(),
32 TypeRef::Bool => "boolean".into(),
33 TypeRef::StringUtf8 => "string".into(),
34 TypeRef::Bytes => "Buffer".into(),
35 TypeRef::Handle => "bigint".into(),
36 TypeRef::Struct(name) | TypeRef::Enum(name) => name.clone(),
37 TypeRef::Optional(inner) => format!("{} | null", ts_type_for(inner)),
38 TypeRef::List(inner) => {
39 let inner_ts = ts_type_for(inner);
40 if matches!(inner.as_ref(), TypeRef::Optional(_)) {
41 format!("({inner_ts})[]")
42 } else {
43 format!("{inner_ts}[]")
44 }
45 }
46 }
47}
48
49fn render_node_dts(api: &Api) -> String {
50 let mut out = String::from("// Generated types for WeaveFFI functions\n");
51 for m in &api.modules {
52 for s in &m.structs {
53 out.push_str(&format!("export interface {} {{\n", s.name));
54 for field in &s.fields {
55 out.push_str(&format!(" {}: {};\n", field.name, ts_type_for(&field.ty)));
56 }
57 out.push_str("}\n");
58 }
59 for e in &m.enums {
60 out.push_str(&format!("export enum {} {{\n", e.name));
61 for v in &e.variants {
62 out.push_str(&format!(" {} = {},\n", v.name, v.value));
63 }
64 out.push_str("}\n");
65 }
66 out.push_str(&format!("// module {}\n", m.name));
67 for f in &m.functions {
68 let params: Vec<String> = f
69 .params
70 .iter()
71 .map(|p| format!("{}: {}", p.name, ts_type_for(&p.ty)))
72 .collect();
73 let ret = match &f.returns {
74 Some(ty) => ts_type_for(ty),
75 None => "void".into(),
76 };
77 out.push_str(&format!(
78 "export function {}({}): {}\n",
79 f.name,
80 params.join(", "),
81 ret
82 ));
83 }
84 }
85 out
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91 use weaveffi_ir::ir::{EnumDef, EnumVariant, Function, Module, Param, StructDef, StructField};
92
93 fn make_api(modules: Vec<Module>) -> Api {
94 Api {
95 version: "0.1.0".into(),
96 modules,
97 }
98 }
99
100 fn make_module(name: &str) -> Module {
101 Module {
102 name: name.into(),
103 functions: vec![],
104 structs: vec![],
105 enums: vec![],
106 errors: None,
107 }
108 }
109
110 #[test]
111 fn ts_type_for_primitives() {
112 assert_eq!(ts_type_for(&TypeRef::I32), "number");
113 assert_eq!(ts_type_for(&TypeRef::Bool), "boolean");
114 assert_eq!(ts_type_for(&TypeRef::StringUtf8), "string");
115 assert_eq!(ts_type_for(&TypeRef::Bytes), "Buffer");
116 assert_eq!(ts_type_for(&TypeRef::Handle), "bigint");
117 }
118
119 #[test]
120 fn ts_type_for_struct_and_enum() {
121 assert_eq!(ts_type_for(&TypeRef::Struct("Contact".into())), "Contact");
122 assert_eq!(ts_type_for(&TypeRef::Enum("Color".into())), "Color");
123 }
124
125 #[test]
126 fn ts_type_for_optional() {
127 let ty = TypeRef::Optional(Box::new(TypeRef::StringUtf8));
128 assert_eq!(ts_type_for(&ty), "string | null");
129 }
130
131 #[test]
132 fn ts_type_for_list() {
133 let ty = TypeRef::List(Box::new(TypeRef::I32));
134 assert_eq!(ts_type_for(&ty), "number[]");
135 }
136
137 #[test]
138 fn ts_type_for_list_of_optional() {
139 let ty = TypeRef::List(Box::new(TypeRef::Optional(Box::new(TypeRef::I32))));
140 assert_eq!(ts_type_for(&ty), "(number | null)[]");
141 }
142
143 #[test]
144 fn ts_type_for_optional_list() {
145 let ty = TypeRef::Optional(Box::new(TypeRef::List(Box::new(TypeRef::I32))));
146 assert_eq!(ts_type_for(&ty), "number[] | null");
147 }
148
149 #[test]
150 fn generate_node_dts_with_structs() {
151 let mut m = make_module("contacts");
152 m.structs.push(StructDef {
153 name: "Contact".into(),
154 doc: None,
155 fields: vec![
156 StructField {
157 name: "name".into(),
158 ty: TypeRef::StringUtf8,
159 doc: None,
160 },
161 StructField {
162 name: "age".into(),
163 ty: TypeRef::I32,
164 doc: None,
165 },
166 StructField {
167 name: "active".into(),
168 ty: TypeRef::Bool,
169 doc: None,
170 },
171 ],
172 });
173 m.enums.push(EnumDef {
174 name: "Color".into(),
175 doc: None,
176 variants: vec![
177 EnumVariant {
178 name: "Red".into(),
179 value: 0,
180 doc: None,
181 },
182 EnumVariant {
183 name: "Green".into(),
184 value: 1,
185 doc: None,
186 },
187 EnumVariant {
188 name: "Blue".into(),
189 value: 2,
190 doc: None,
191 },
192 ],
193 });
194 m.functions.push(Function {
195 name: "get_contact".into(),
196 params: vec![Param {
197 name: "id".into(),
198 ty: TypeRef::I32,
199 }],
200 returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
201 "Contact".into(),
202 )))),
203 doc: None,
204 r#async: false,
205 });
206 m.functions.push(Function {
207 name: "list_contacts".into(),
208 params: vec![],
209 returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into())))),
210 doc: None,
211 r#async: false,
212 });
213
214 let dts = render_node_dts(&make_api(vec![m]));
215
216 assert!(dts.contains("export interface Contact {"));
217 assert!(dts.contains(" name: string;"));
218 assert!(dts.contains(" age: number;"));
219 assert!(dts.contains(" active: boolean;"));
220 assert!(dts.contains("export enum Color {"));
221 assert!(dts.contains(" Red = 0,"));
222 assert!(dts.contains(" Green = 1,"));
223 assert!(dts.contains(" Blue = 2,"));
224 assert!(dts.contains("export function get_contact(id: number): Contact | null"));
225 assert!(dts.contains("export function list_contacts(): Contact[]"));
226
227 let iface_pos = dts.find("export interface Contact").unwrap();
228 let enum_pos = dts.find("export enum Color").unwrap();
229 let fn_pos = dts.find("export function get_contact").unwrap();
230 assert!(
231 iface_pos < fn_pos,
232 "interface should appear before functions"
233 );
234 assert!(enum_pos < fn_pos, "enum should appear before functions");
235 }
236
237 #[test]
238 fn generate_node_dts_with_structs_and_enums() {
239 let api = make_api(vec![Module {
240 name: "contacts".to_string(),
241 functions: vec![
242 Function {
243 name: "get_contact".to_string(),
244 params: vec![Param {
245 name: "id".to_string(),
246 ty: TypeRef::I32,
247 }],
248 returns: Some(TypeRef::Optional(Box::new(TypeRef::Struct(
249 "Contact".into(),
250 )))),
251 doc: None,
252 r#async: false,
253 },
254 Function {
255 name: "list_contacts".to_string(),
256 params: vec![],
257 returns: Some(TypeRef::List(Box::new(TypeRef::Struct("Contact".into())))),
258 doc: None,
259 r#async: false,
260 },
261 Function {
262 name: "set_favorite_color".to_string(),
263 params: vec![
264 Param {
265 name: "contact_id".to_string(),
266 ty: TypeRef::I32,
267 },
268 Param {
269 name: "color".to_string(),
270 ty: TypeRef::Optional(Box::new(TypeRef::Enum("Color".into()))),
271 },
272 ],
273 returns: None,
274 doc: None,
275 r#async: false,
276 },
277 Function {
278 name: "get_tags".to_string(),
279 params: vec![Param {
280 name: "contact_id".to_string(),
281 ty: TypeRef::I32,
282 }],
283 returns: Some(TypeRef::List(Box::new(TypeRef::StringUtf8))),
284 doc: None,
285 r#async: false,
286 },
287 ],
288 structs: vec![StructDef {
289 name: "Contact".to_string(),
290 doc: None,
291 fields: vec![
292 StructField {
293 name: "name".to_string(),
294 ty: TypeRef::StringUtf8,
295 doc: None,
296 },
297 StructField {
298 name: "email".to_string(),
299 ty: TypeRef::Optional(Box::new(TypeRef::StringUtf8)),
300 doc: None,
301 },
302 StructField {
303 name: "tags".to_string(),
304 ty: TypeRef::List(Box::new(TypeRef::StringUtf8)),
305 doc: None,
306 },
307 ],
308 }],
309 enums: vec![EnumDef {
310 name: "Color".to_string(),
311 doc: None,
312 variants: vec![
313 EnumVariant {
314 name: "Red".to_string(),
315 value: 0,
316 doc: None,
317 },
318 EnumVariant {
319 name: "Green".to_string(),
320 value: 1,
321 doc: None,
322 },
323 EnumVariant {
324 name: "Blue".to_string(),
325 value: 2,
326 doc: None,
327 },
328 ],
329 }],
330 errors: None,
331 }]);
332
333 let tmp = std::env::temp_dir().join("weaveffi_test_node_structs_and_enums");
334 let _ = std::fs::remove_dir_all(&tmp);
335 std::fs::create_dir_all(&tmp).unwrap();
336 let out_dir = Utf8Path::from_path(&tmp).expect("temp dir is valid UTF-8");
337
338 NodeGenerator.generate(&api, out_dir).unwrap();
339
340 let dts = std::fs::read_to_string(tmp.join("node").join("types.d.ts")).unwrap();
341
342 assert!(
343 dts.contains("export interface Contact {"),
344 "missing Contact interface: {dts}"
345 );
346 assert!(dts.contains(" name: string;"), "missing name field: {dts}");
347 assert!(
348 dts.contains(" email: string | null;"),
349 "missing optional email field: {dts}"
350 );
351 assert!(
352 dts.contains(" tags: string[];"),
353 "missing list tags field: {dts}"
354 );
355
356 assert!(
357 dts.contains("export enum Color {"),
358 "missing Color enum: {dts}"
359 );
360 assert!(dts.contains(" Red = 0,"), "missing Red variant: {dts}");
361 assert!(dts.contains(" Green = 1,"), "missing Green variant: {dts}");
362 assert!(dts.contains(" Blue = 2,"), "missing Blue variant: {dts}");
363
364 assert!(
365 dts.contains("export function get_contact(id: number): Contact | null"),
366 "missing get_contact with optional return: {dts}"
367 );
368 assert!(
369 dts.contains("export function list_contacts(): Contact[]"),
370 "missing list_contacts with list return: {dts}"
371 );
372 assert!(
373 dts.contains(
374 "export function set_favorite_color(contact_id: number, color: Color | null): void"
375 ),
376 "missing set_favorite_color with optional enum param: {dts}"
377 );
378 assert!(
379 dts.contains("export function get_tags(contact_id: number): string[]"),
380 "missing get_tags with list return: {dts}"
381 );
382
383 let iface_pos = dts.find("export interface Contact").unwrap();
384 let enum_pos = dts.find("export enum Color").unwrap();
385 let fn_pos = dts.find("export function get_contact").unwrap();
386 assert!(
387 iface_pos < fn_pos,
388 "interface should appear before functions"
389 );
390 assert!(enum_pos < fn_pos, "enum should appear before functions");
391
392 let _ = std::fs::remove_dir_all(&tmp);
393 }
394}