Skip to main content

weaveffi_gen_node/
lib.rs

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}