js_component_bindgen/
esm_bindgen.rs

1use heck::ToLowerCamelCase;
2
3use crate::intrinsics::Intrinsic;
4use crate::names::{maybe_quote_id, maybe_quote_member, LocalNames};
5use crate::source::Source;
6use crate::{uwrite, uwriteln, TranspileOpts};
7use std::collections::BTreeMap;
8use std::fmt::Write;
9
10type LocalName = String;
11
12enum ImportBinding {
13    Interface(BTreeMap<String, ImportBinding>),
14    // an import binding can have multiple local names,
15    // used in eg multi-version workflows
16    Local(Vec<LocalName>),
17}
18
19enum ExportBinding {
20    Interface(BTreeMap<String, ExportBinding>),
21    Local(LocalName),
22}
23
24#[derive(Default)]
25pub struct EsmBindgen {
26    imports: BTreeMap<String, ImportBinding>,
27    exports: BTreeMap<String, ExportBinding>,
28    export_aliases: BTreeMap<String, String>,
29}
30
31impl EsmBindgen {
32    /// add imported function binding, using a path slice starting with the import specifier as its
33    /// first segment
34    /// arbitrary nesting of interfaces is supported in order to support virtual WASI interfaces
35    /// only two-level nesting supports serialization into imports currently
36    pub fn add_import_binding(&mut self, path: &[String], binding_name: String) {
37        let mut iface = &mut self.imports;
38        for i in 0..path.len() - 1 {
39            if !iface.contains_key(&path[i]) {
40                iface.insert(
41                    path[i].to_string(),
42                    ImportBinding::Interface(BTreeMap::new()),
43                );
44            }
45            iface = match iface.get_mut(&path[i]).unwrap() {
46                ImportBinding::Interface(iface) => iface,
47                ImportBinding::Local(local) => {
48                    panic!(
49                        "Internal bindgen error: Import '{}' cannot be both an interface '{}' and a function '{}'",
50                        &path[0..i + 1].join("."),
51                        &path[i + 1..].join("."),
52                        &local[0],
53                    );
54                }
55            };
56        }
57        if let Some(ref mut existing) = iface.get_mut(&path[path.len() - 1]) {
58            match existing {
59                ImportBinding::Interface(_) => {
60                    unreachable!("Multi-version interfaces must have the same shape")
61                }
62                ImportBinding::Local(ref mut local_names) => {
63                    if !local_names.contains(&binding_name) {
64                        local_names.push(binding_name);
65                    }
66                }
67            }
68        } else {
69            iface.insert(
70                path[path.len() - 1].to_string(),
71                ImportBinding::Local(vec![binding_name]),
72            );
73        }
74    }
75
76    /// add an exported function binding, optionally on an interface id or kebab name
77    pub fn add_export_binding(
78        &mut self,
79        iface_id_or_kebab: Option<&str>,
80        local_name: String,
81        func_name: String,
82    ) {
83        let mut iface = &mut self.exports;
84        if let Some(iface_id_or_kebab) = iface_id_or_kebab {
85            // convert kebab names to camel case, leave ids as-is
86            let iface_id_or_kebab = if iface_id_or_kebab.contains(':') {
87                iface_id_or_kebab.to_string()
88            } else {
89                iface_id_or_kebab.to_lower_camel_case()
90            };
91            if !iface.contains_key(&iface_id_or_kebab) {
92                iface.insert(
93                    iface_id_or_kebab.to_string(),
94                    ExportBinding::Interface(BTreeMap::new()),
95                );
96            }
97            iface = match iface.get_mut(&iface_id_or_kebab).unwrap() {
98                ExportBinding::Interface(iface) => iface,
99                ExportBinding::Local(_) => panic!(
100                    "Exported interface {} cannot be both a function and an interface",
101                    iface_id_or_kebab
102                ),
103            };
104        }
105        iface.insert(func_name, ExportBinding::Local(local_name));
106    }
107
108    /// once all exports have been created, aliases can be populated for interface
109    /// names that do not collide with kebab names or other interface names
110    pub fn populate_export_aliases(&mut self) {
111        for expt_name in self.exports.keys() {
112            if let Some(path_idx) = expt_name.rfind('/') {
113                let end = if let Some(version_idx) = expt_name.rfind('@') {
114                    version_idx
115                } else {
116                    expt_name.len()
117                };
118                let alias = &expt_name[path_idx + 1..end].to_lower_camel_case();
119                if !self.exports.contains_key(alias) && !self.export_aliases.contains_key(alias) {
120                    self.export_aliases
121                        .insert(alias.to_string(), expt_name.to_string());
122                }
123            }
124        }
125    }
126
127    /// get the final top-level import specifier list
128    pub fn import_specifiers(&self) -> Vec<String> {
129        self.imports.keys().map(|impt| impt.to_string()).collect()
130    }
131
132    /// get the exports (including exported aliases) from the bindgen
133    pub fn exports(&self) -> Vec<(&str, &str)> {
134        self.export_aliases
135            .iter()
136            .map(|(alias, name)| (alias.as_ref(), name.as_ref()))
137            .chain(
138                self.exports
139                    .keys()
140                    .map(|name| (name.as_ref(), name.as_ref())),
141            )
142            .collect()
143    }
144
145    pub fn render_exports(
146        &mut self,
147        output: &mut Source,
148        instantiation: bool,
149        local_names: &mut LocalNames,
150        opts: &TranspileOpts,
151    ) {
152        if self.exports.is_empty() {
153            if instantiation {
154                output.push_str("return {}");
155            }
156            return;
157        }
158        // first create all the interfaces
159        // we currently only support first-level nesting so there is no ordering to figure out
160        // in the process we also populate the alias info
161        for (export_name, export) in self.exports.iter() {
162            let ExportBinding::Interface(iface) = export else {
163                continue;
164            };
165            let (local_name, _) =
166                local_names.get_or_create(format!("export:{export_name}"), export_name);
167            uwriteln!(output, "const {local_name} = {{");
168            for (func_name, export) in iface {
169                let ExportBinding::Local(local_name) = export else {
170                    panic!("Unsupported nested export interface");
171                };
172                uwriteln!(output, "{}: {local_name},", maybe_quote_id(func_name));
173            }
174            uwriteln!(output, "\n}};");
175        }
176        uwrite!(
177            output,
178            "\n{} {{ ",
179            if instantiation { "return" } else { "export" }
180        );
181        let mut first = true;
182        for (alias, export_name) in &self.export_aliases {
183            if first {
184                first = false
185            }
186            let local_name = match &self.exports[export_name] {
187                ExportBinding::Local(local_name) => local_name,
188                ExportBinding::Interface(_) => local_names.get(format!("export:{}", export_name)),
189            };
190            let alias_maybe_quoted = maybe_quote_id(alias);
191            if local_name == alias_maybe_quoted {
192                output.push_str(local_name);
193                uwrite!(output, ", ");
194            } else if instantiation {
195                uwrite!(output, "{alias_maybe_quoted}: {local_name}");
196                uwrite!(output, ", ");
197            } else if !self.contains_js_quote(&alias_maybe_quoted) || !opts.no_namespaced_exports {
198                uwrite!(output, "{local_name} as {alias_maybe_quoted}");
199                uwrite!(output, ", ");
200            }
201        }
202        for (export_name, export) in &self.exports {
203            if first {
204                first = false
205            }
206            let local_name = match export {
207                ExportBinding::Local(local_name) => local_name,
208                ExportBinding::Interface(_) => local_names.get(format!("export:{}", export_name)),
209            };
210            let export_name_maybe_quoted = maybe_quote_id(export_name);
211            if local_name == export_name_maybe_quoted {
212                output.push_str(local_name);
213                uwrite!(output, ", ");
214            } else if instantiation {
215                uwrite!(output, "{export_name_maybe_quoted}: {local_name}");
216                uwrite!(output, ", ");
217            } else if !self.contains_js_quote(&export_name_maybe_quoted)
218                || !opts.no_namespaced_exports
219            {
220                uwrite!(output, "{local_name} as {export_name_maybe_quoted}");
221                uwrite!(output, ", ");
222            }
223        }
224        uwrite!(output, " }}");
225    }
226
227    fn contains_js_quote(&self, js_string: &str) -> bool {
228        js_string.contains("\"") || js_string.contains("'") || js_string.contains("`")
229    }
230
231    pub fn render_imports(
232        &mut self,
233        output: &mut Source,
234        imports_object: Option<&str>,
235        local_names: &mut LocalNames,
236    ) {
237        let mut iface_imports = Vec::new();
238        for (specifier, binding) in &self.imports {
239            let idl_binding = if specifier.starts_with("webidl:") {
240                let iface_idx = specifier.find('/').unwrap() + 1;
241                let iface_name = if let Some(version_idx) = specifier.find('@') {
242                    &specifier[iface_idx..version_idx]
243                } else {
244                    &specifier[iface_idx..]
245                };
246                Some(iface_name.strip_prefix("global-").unwrap_or_default())
247            } else {
248                None
249            };
250            if imports_object.is_some() || idl_binding.is_some() {
251                uwrite!(output, "const ");
252            } else {
253                uwrite!(output, "import ");
254            }
255            match binding {
256                ImportBinding::Interface(bindings) => {
257                    if imports_object.is_none() && idl_binding.is_none() && bindings.len() == 1 {
258                        let (import_name, import) = bindings.iter().next().unwrap();
259                        if import_name == "default" {
260                            match import {
261                                ImportBinding::Interface(iface) => {
262                                    let iface_local_name = local_names.create_once(specifier);
263                                    iface_imports.push((iface_local_name.to_string(), iface));
264                                    uwriteln!(output, "{iface_local_name} from '{specifier}';");
265                                }
266                                ImportBinding::Local(local_names) => {
267                                    let local_name = &local_names[0];
268                                    uwriteln!(output, "{local_name} from '{specifier}';");
269                                    for other_local_name in &local_names[1..] {
270                                        uwriteln!(
271                                            output,
272                                            "const {other_local_name} = {local_name};"
273                                        );
274                                    }
275                                }
276                            };
277                            continue;
278                        }
279                    }
280                    uwrite!(output, "{{");
281                    let mut first = true;
282                    for (external_name, import) in bindings {
283                        match import {
284                            ImportBinding::Interface(iface) => {
285                                if first {
286                                    output.push_str(" ");
287                                    first = false;
288                                } else {
289                                    output.push_str(", ");
290                                }
291                                let (iface_local_name, _) = local_names.get_or_create(
292                                    format!("import:{specifier}#{external_name}"),
293                                    external_name,
294                                );
295                                iface_imports.push((iface_local_name.to_string(), iface));
296                                if external_name == iface_local_name {
297                                    uwrite!(output, "{external_name}");
298                                } else if imports_object.is_some() || idl_binding.is_some() {
299                                    uwrite!(output, "{external_name}: {iface_local_name}");
300                                } else {
301                                    uwrite!(output, "{external_name} as {iface_local_name}");
302                                }
303                            }
304                            ImportBinding::Local(local_names) => {
305                                for local_name in local_names {
306                                    if first {
307                                        output.push_str(" ");
308                                        first = false;
309                                    } else {
310                                        output.push_str(", ");
311                                    }
312                                    if external_name == local_name {
313                                        uwrite!(output, "{external_name}");
314                                    } else if imports_object.is_some() || idl_binding.is_some() {
315                                        uwrite!(output, "{external_name}: {local_name}");
316                                    } else {
317                                        uwrite!(output, "{external_name} as {local_name}");
318                                    }
319                                }
320                            }
321                        };
322                    }
323                    if !first {
324                        output.push_str(" ");
325                    }
326                    if let Some(imports_object) = imports_object {
327                        uwriteln!(
328                            output,
329                            "}} = {imports_object}{};",
330                            maybe_quote_member(specifier)
331                        );
332                    } else if let Some(idl_binding) = idl_binding {
333                        uwrite!(output, "}} = {}()", Intrinsic::GlobalThisIdlProxy.name());
334                        if !idl_binding.is_empty() {
335                            for segment in idl_binding.split('-') {
336                                uwrite!(output, ".{}()", segment.to_lowercase());
337                            }
338                        }
339                        uwrite!(output, ";\n");
340                    } else {
341                        uwriteln!(output, "}} from '{specifier}';");
342                    }
343                }
344                ImportBinding::Local(local_names) => {
345                    let local_name = &local_names[0];
346                    if let Some(imports_object) = imports_object {
347                        uwriteln!(
348                            output,
349                            "{local_name} = {imports_object}{}.default;",
350                            maybe_quote_member(specifier)
351                        );
352                    } else {
353                        uwriteln!(output, "{local_name} from '{specifier}';");
354                    }
355                    for other_local_name in &local_names[1..] {
356                        uwriteln!(output, "const {other_local_name} = {local_name};");
357                    }
358                }
359            }
360        }
361        // render interface import member getters
362        for (iface_local_name, iface_imports) in iface_imports {
363            uwrite!(output, "const {{");
364            let mut first = true;
365            for (member_name, binding) in iface_imports {
366                let ImportBinding::Local(local_names) = binding else {
367                    continue;
368                };
369                for local_name in local_names {
370                    if first {
371                        output.push_str(" ");
372                        first = false;
373                    } else {
374                        output.push_str(",\n");
375                    }
376                    if member_name == local_name {
377                        output.push_str(local_name);
378                    } else {
379                        uwrite!(output, "{member_name}: {local_name}");
380                    }
381                }
382            }
383            if !first {
384                output.push_str(" ");
385            }
386            uwriteln!(output, "}} = {iface_local_name};");
387        }
388    }
389}