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
11type LocalName = String;
13
14type WasmFuncName = String;
16
17enum ImportBinding {
18 Interface(BTreeMap<String, ImportBinding>),
19 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 pub fn add_import_binding(&mut self, path: &[String], binding_name: String) {
42 let mut iface = &mut self.imports;
43
44 for i in 0..path.len() - 1 {
47 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 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 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 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 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 pub fn import_specifiers(&self) -> Vec<String> {
153 self.imports.keys().map(|impt| impt.to_string()).collect()
154 }
155
156 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 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 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 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 ImportBinding::Interface(bindings) => {
305 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 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 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 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 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 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 for (member_name, local_name) in generated_member_names {
483 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 uwriteln!(output, "{local_name}._isHostProvided = true;");
497 }
498 }
499 }
500}