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