Skip to main content

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::{LocalNames, maybe_quote_id, maybe_quote_member};
8use crate::source::Source;
9use crate::{TranspileOpts, uwrite, uwriteln};
10
11/// JS local name
12type LocalName = String;
13
14/// Original Wasm function name
15type WasmFuncName = String;
16
17enum ImportBinding {
18    Interface(BTreeMap<String, ImportBinding>),
19    // an import binding can have multiple local names,
20    // used in eg multi-version workflows
21    Local(Vec<LocalName>),
22}
23
24enum ExportBinding {
25    Interface(BTreeMap<String, ExportBinding>),
26    Local(LocalName, WasmFuncName),
27    Constant(LocalName),
28}
29
30#[derive(Default)]
31pub struct EsmBindgen {
32    imports: BTreeMap<String, ImportBinding>,
33    exports: BTreeMap<String, ExportBinding>,
34    export_aliases: BTreeMap<String, String>,
35}
36
37impl EsmBindgen {
38    /// add imported function binding, using a path slice starting with the import specifier as its
39    /// first segment
40    /// arbitrary nesting of interfaces is supported in order to support virtual WASI interfaces
41    /// only two-level nesting supports serialization into imports currently
42    pub fn add_import_binding(&mut self, path: &[String], binding_name: String) {
43        let mut iface = &mut self.imports;
44
45        // Process elements of the path in order
46        // ex. ["@bytecodealliance/preview2-shim/filesystem", "types", "Descriptor"]
47        for i in 0..path.len() - 1 {
48            // Create the interface if it is not already present
49            if !iface.contains_key(&path[i]) {
50                iface.insert(
51                    path[i].to_string(),
52                    ImportBinding::Interface(BTreeMap::new()),
53                );
54            }
55
56            iface = match iface.get_mut(&path[i]).unwrap() {
57                ImportBinding::Interface(iface) => iface,
58                ImportBinding::Local(local) => {
59                    panic!(
60                        "Internal bindgen error: Import '{}' cannot be both an interface '{}' and a function '{}'",
61                        path[0..i + 1].join("."),
62                        path[i + 1..].join("."),
63                        local[0],
64                    );
65                }
66            };
67        }
68
69        if let Some(ref mut existing) = iface.get_mut(&path[path.len() - 1]) {
70            match existing {
71                ImportBinding::Interface(_) => {
72                    unreachable!("Multi-version interfaces must have the same shape")
73                }
74                ImportBinding::Local(binding_local_names) => {
75                    if !binding_local_names.contains(&binding_name) {
76                        binding_local_names.push(binding_name);
77                    }
78                }
79            }
80        } else {
81            iface.insert(
82                path[path.len() - 1].to_string(),
83                ImportBinding::Local(vec![binding_name]),
84            );
85        }
86    }
87
88    /// Add an exported function binding, optionally on an interface id or kebab name
89    pub fn add_export_binding(
90        &mut self,
91        iface_id_or_kebab: Option<&str>,
92        local_name: String,
93        func_name: String,
94        func: &wit_parser::Function,
95    ) {
96        let mut iface = &mut self.exports;
97        // If we weren't provided an interface ID, it's a local name
98        let Some(iface_id_or_kebab) = iface_id_or_kebab else {
99            iface.insert(
100                func_name,
101                ExportBinding::Local(local_name, func.name.to_string()),
102            );
103            return;
104        };
105
106        // convert kebab names to camel case, leave ids as-is
107        let iface_id_or_kebab = if iface_id_or_kebab.contains(':') {
108            iface_id_or_kebab.to_string()
109        } else {
110            iface_id_or_kebab.to_lower_camel_case()
111        };
112
113        if !iface.contains_key(&iface_id_or_kebab) {
114            iface.insert(
115                iface_id_or_kebab.to_string(),
116                ExportBinding::Interface(BTreeMap::new()),
117            );
118        }
119
120        iface = match iface.get_mut(&iface_id_or_kebab).unwrap() {
121            ExportBinding::Interface(iface) => iface,
122            ExportBinding::Local(_, _) | ExportBinding::Constant(_) => panic!(
123                "Exported interface {iface_id_or_kebab} cannot be both a function and an interface"
124            ),
125        };
126
127        iface.insert(
128            func_name,
129            ExportBinding::Local(local_name, func.name.to_string()),
130        );
131    }
132
133    /// Add a generated constant to an exported interface.
134    pub fn add_export_constant(
135        &mut self,
136        iface_id_or_kebab: &str,
137        local_name: String,
138        constant_name: String,
139    ) {
140        let iface_id_or_kebab = if iface_id_or_kebab.contains(':') {
141            iface_id_or_kebab.to_string()
142        } else {
143            iface_id_or_kebab.to_lower_camel_case()
144        };
145
146        let iface = self
147            .exports
148            .entry(iface_id_or_kebab.clone())
149            .or_insert_with(|| ExportBinding::Interface(BTreeMap::new()));
150        let ExportBinding::Interface(iface) = iface else {
151            panic!(
152                "Exported interface {iface_id_or_kebab} cannot be both a value and an interface"
153            );
154        };
155        iface.insert(constant_name, ExportBinding::Constant(local_name));
156    }
157
158    /// once all exports have been created, aliases can be populated for interface
159    /// names that do not collide with kebab names or other interface names
160    pub fn populate_export_aliases(&mut self) {
161        for expt_name in self.exports.keys() {
162            if let Some(path_idx) = expt_name.rfind('/') {
163                let end = if let Some(version_idx) = expt_name.rfind('@') {
164                    version_idx
165                } else {
166                    expt_name.len()
167                };
168                let alias = &expt_name[path_idx + 1..end].to_lower_camel_case();
169                if !self.exports.contains_key(alias) && !self.export_aliases.contains_key(alias) {
170                    self.export_aliases
171                        .insert(alias.to_string(), expt_name.to_string());
172                }
173            }
174        }
175    }
176
177    /// get the final top-level import specifier list
178    pub fn import_specifiers(&self) -> Vec<String> {
179        self.imports.keys().map(|impt| impt.to_string()).collect()
180    }
181
182    /// get the exports (including exported aliases) from the bindgen
183    pub fn exports(&self) -> Vec<(&str, &str)> {
184        self.export_aliases
185            .iter()
186            .map(|(alias, name)| (alias.as_ref(), name.as_ref()))
187            .chain(self.exports.iter().map(|(name, binding)| {
188                (
189                    name.as_ref(),
190                    match binding {
191                        ExportBinding::Interface(_) => name.as_ref(),
192                        ExportBinding::Constant(_) => {
193                            unreachable!("constants are only emitted within interfaces")
194                        }
195                        ExportBinding::Local(_, wasm_func_name) => wasm_func_name.as_str(),
196                    },
197                )
198            }))
199            .collect()
200    }
201
202    pub fn render_exports(
203        &mut self,
204        output: &mut Source,
205        instantiation: bool,
206        local_names: &mut LocalNames,
207        opts: &TranspileOpts,
208    ) {
209        if self.exports.is_empty() {
210            if instantiation {
211                output.push_str("return {}");
212            }
213            return;
214        }
215        // first create all the interfaces
216        // we currently only support first-level nesting so there is no ordering to figure out
217        // in the process we also populate the alias info
218        for (export_name, export) in self.exports.iter() {
219            let ExportBinding::Interface(iface) = export else {
220                continue;
221            };
222            let (local_name, _) =
223                local_names.get_or_create(format!("export:{export_name}"), export_name);
224            uwriteln!(output, "const {local_name} = {{");
225            for (func_name, export) in iface {
226                let local_name = match export {
227                    ExportBinding::Local(local_name, _) | ExportBinding::Constant(local_name) => {
228                        local_name
229                    }
230                    ExportBinding::Interface(_) => panic!("Unsupported nested export interface"),
231                };
232                uwriteln!(output, "{}: {local_name},", maybe_quote_id(func_name));
233            }
234            uwriteln!(output, "\n}};");
235        }
236        uwrite!(
237            output,
238            "\n{} {{ ",
239            if instantiation { "return" } else { "export" }
240        );
241        let mut first = true;
242        for (alias, export_name) in &self.export_aliases {
243            if first {
244                first = false
245            }
246            let local_name = match &self.exports[export_name] {
247                ExportBinding::Local(local_name, _) => local_name,
248                ExportBinding::Constant(local_name) => local_name,
249                ExportBinding::Interface(_) => local_names.get(format!("export:{export_name}")),
250            };
251            let alias_maybe_quoted = maybe_quote_id(alias);
252            if local_name == alias_maybe_quoted {
253                output.push_str(local_name);
254                uwrite!(output, ", ");
255            } else if instantiation {
256                uwrite!(output, "{alias_maybe_quoted}: {local_name}");
257                uwrite!(output, ", ");
258            } else if !self.contains_js_quote(&alias_maybe_quoted) || !opts.no_namespaced_exports {
259                uwrite!(output, "{local_name} as {alias_maybe_quoted}");
260                uwrite!(output, ", ");
261            }
262        }
263        for (export_name, export) in &self.exports {
264            if first {
265                first = false
266            }
267            let local_name = match export {
268                ExportBinding::Local(local_name, _) => local_name,
269                ExportBinding::Constant(local_name) => local_name,
270                ExportBinding::Interface(_) => local_names.get(format!("export:{export_name}")),
271            };
272            let export_name_maybe_quoted = maybe_quote_id(export_name);
273            if local_name == export_name_maybe_quoted {
274                output.push_str(local_name);
275                uwrite!(output, ", ");
276            } else if instantiation {
277                uwrite!(output, "{export_name_maybe_quoted}: {local_name}");
278                uwrite!(output, ", ");
279            } else if !self.contains_js_quote(&export_name_maybe_quoted)
280                || !opts.no_namespaced_exports
281            {
282                uwrite!(output, "{local_name} as {export_name_maybe_quoted}");
283                uwrite!(output, ", ");
284            }
285        }
286        uwrite!(output, " }}");
287    }
288
289    fn contains_js_quote(&self, js_string: &str) -> bool {
290        js_string.contains("\"") || js_string.contains("'") || js_string.contains("`")
291    }
292
293    /// Render a block of imports
294    ///
295    /// This is normally right before the instantiation code in the generated output, e.g.:
296    ///
297    /// ```js
298    /// const { someFn } = imports['ns:pkg/iface'];
299    /// const { asyncOtherFn } = imports['ns:pkg/iface2'];
300    /// const { someFn, asyncSomeFn } = imports['ns:pkg/iface3'];
301    /// const { getDirectories } = imports['wasi:filesystem/preopens'];
302    /// const { Descriptor, filesystemErrorCode } = imports['wasi:filesystem/types'];
303    /// const { Error: Error$1 } = imports['wasi:io/error'];
304    /// const { InputStream, OutputStream } = imports['wasi:io/streams'];
305    /// let gen = (function* _initGenerator () {
306    /// ```
307    ///
308    pub fn render_imports(
309        &mut self,
310        output: &mut Source,
311        imports_object: Option<&str>,
312        local_names: &mut LocalNames,
313    ) {
314        let mut iface_imports = Vec::new();
315
316        for (specifier, binding) in &self.imports {
317            // Build IDL binding if the specifier uses specifal WebIDL support
318            let idl_binding = if specifier.starts_with("webidl:") {
319                let iface_idx = specifier.find('/').unwrap() + 1;
320                let iface_name = if let Some(version_idx) = specifier.find('@') {
321                    &specifier[iface_idx..version_idx]
322                } else {
323                    &specifier[iface_idx..]
324                };
325                Some(iface_name.strip_prefix("global-").unwrap_or_default())
326            } else {
327                None
328            };
329
330            if imports_object.is_some() || idl_binding.is_some() {
331                uwrite!(output, "const ");
332            } else {
333                uwrite!(output, "import ");
334            }
335
336            match binding {
337                // For interfaces we import the entire object as one
338                ImportBinding::Interface(bindings) => {
339                    // If we there is no import object and it's not an IDL binding and there's only
340                    // *one* binding, then we can directly import rather than attempting to extract
341                    // individual imports
342                    if imports_object.is_none() && idl_binding.is_none() && bindings.len() == 1 {
343                        let (import_name, import) = bindings.iter().next().unwrap();
344                        if import_name == "default" {
345                            match import {
346                                ImportBinding::Interface(iface) => {
347                                    let iface_local_name = local_names.create_once(specifier);
348                                    iface_imports.push((iface_local_name.to_string(), iface));
349                                    uwriteln!(output, "{iface_local_name} from '{specifier}';");
350                                }
351                                ImportBinding::Local(local_names) => {
352                                    let local_name = &local_names[0];
353                                    uwriteln!(output, "{local_name} from '{specifier}';");
354                                    for other_local_name in &local_names[1..] {
355                                        uwriteln!(
356                                            output,
357                                            "const {other_local_name} = {local_name};"
358                                        );
359                                    }
360                                }
361                            };
362                            continue;
363                        }
364                    }
365
366                    uwrite!(output, "{{");
367
368                    let mut first = true;
369                    let mut bound_external_names = Vec::new();
370                    // Generate individual imports for all the bindings that were provided,
371                    // to generate the lhs of the destructured assignment
372                    for (external_name, import) in bindings {
373                        match import {
374                            ImportBinding::Interface(iface) => {
375                                if first {
376                                    output.push_str(" ");
377                                    first = false;
378                                } else {
379                                    output.push_str(", ");
380                                }
381                                let (iface_local_name, _) = local_names.get_or_create(
382                                    format!("import:{specifier}#{external_name}"),
383                                    external_name,
384                                );
385                                iface_imports.push((iface_local_name.to_string(), iface));
386                                if external_name == iface_local_name {
387                                    uwrite!(output, "{external_name}");
388                                } else if imports_object.is_some() || idl_binding.is_some() {
389                                    uwrite!(output, "{external_name}: {iface_local_name}");
390                                } else {
391                                    uwrite!(output, "{external_name} as {iface_local_name}");
392                                }
393                                bound_external_names.push((
394                                    external_name.to_string(),
395                                    iface_local_name.to_string(),
396                                ));
397                            }
398
399                            ImportBinding::Local(local_names) => {
400                                for local_name in local_names {
401                                    if first {
402                                        output.push_str(" ");
403                                        first = false;
404                                    } else {
405                                        output.push_str(", ");
406                                    }
407                                    if external_name == local_name {
408                                        uwrite!(output, "{external_name}");
409                                    } else if imports_object.is_some() || idl_binding.is_some() {
410                                        uwrite!(output, "{external_name}: {local_name}");
411                                    } else {
412                                        uwrite!(output, "{external_name} as {local_name}");
413                                    }
414                                    bound_external_names
415                                        .push((external_name.to_string(), local_name.to_string()));
416                                }
417                            }
418                        };
419                    }
420
421                    if !first {
422                        output.push_str(" ");
423                    }
424
425                    // End the destructured assignment
426                    if let Some(imports_object) = imports_object {
427                        uwriteln!(
428                            output,
429                            "}} = {imports_object}{};",
430                            maybe_quote_member(specifier)
431                        );
432                        for (external_name, local_name) in bound_external_names {
433                            uwriteln!(
434                                output,
435                                r#"
436                                if ({local_name} === undefined) {{
437                                    const err = new Error("unexpectedly undefined instance import '{local_name}', was '{external_name}' available at instantiation?");
438                                    console.error("ERROR:", err.toString());
439                                    throw err;
440                                }}
441                                "#,
442                            );
443                        }
444                    } else if let Some(idl_binding) = idl_binding {
445                        uwrite!(
446                            output,
447                            "}} = {}()",
448                            WebIdlIntrinsic::GlobalThisIdlProxy.name()
449                        );
450                        if !idl_binding.is_empty() {
451                            for segment in idl_binding.split('-') {
452                                uwrite!(output, ".{}()", segment.to_lowercase());
453                            }
454                        }
455                        uwrite!(output, ";\n");
456                    } else {
457                        uwriteln!(output, "}} from '{specifier}';");
458                    }
459                }
460
461                // For local bindings we can use a simpler direct assignment
462                ImportBinding::Local(binding_local_names) => {
463                    let local_name = &binding_local_names[0];
464                    if let Some(imports_object) = imports_object {
465                        uwriteln!(
466                            output,
467                            "{local_name} = {imports_object}{}.default;",
468                            maybe_quote_member(specifier)
469                        );
470                    } else {
471                        uwriteln!(output, "{local_name} from '{specifier}';");
472                    }
473
474                    for other_local_name in &binding_local_names[1..] {
475                        uwriteln!(output, "const {other_local_name} = {local_name};");
476                    }
477                }
478            }
479        }
480
481        // Render interface import member getters
482        for (iface_local_name, iface_imports) in iface_imports {
483            uwrite!(output, "const {{");
484            let mut first = true;
485            let mut generated_member_names = Vec::new();
486
487            for (member_name, binding) in iface_imports {
488                let ImportBinding::Local(binding_local_names) = binding else {
489                    continue;
490                };
491                for local_name in binding_local_names {
492                    if first {
493                        output.push_str(" ");
494                        first = false;
495                    } else {
496                        output.push_str(",\n");
497                    }
498                    if member_name == local_name {
499                        output.push_str(local_name);
500                    } else {
501                        uwrite!(output, "{member_name}: {local_name}");
502                    }
503                    generated_member_names.push((member_name, local_name));
504                }
505            }
506            if !first {
507                output.push_str(" ");
508            }
509            uwriteln!(output, "}} = {iface_local_name};");
510
511            // Process all external host-provided imports
512            for (member_name, local_name) in generated_member_names {
513                // Ensure that the imports we destructured were defined
514                // (if they were not, the user is likely missing an import @ instantiation time)
515                uwriteln!(
516                    output,
517                    r#"
518                    if ({local_name} === undefined) {{
519                        const err = new Error("unexpectedly undefined local import '{local_name}', was '{member_name}' available at instantiation?");
520                        console.error("ERROR:", err.toString());
521                        throw err;
522                    }}
523                    "#,
524                );
525            }
526        }
527    }
528}