js_component_bindgen/
esm_bindgen.rs

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