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 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 pub fn add_import_binding(&mut self, path: &[String], binding_name: String) {
43 let mut iface = &mut self.imports;
44
45 for i in 0..path.len() - 1 {
48 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 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 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 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 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 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 pub fn import_specifiers(&self) -> Vec<String> {
179 self.imports.keys().map(|impt| impt.to_string()).collect()
180 }
181
182 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 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 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 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 ImportBinding::Interface(bindings) => {
339 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 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 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 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 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 for (member_name, local_name) in generated_member_names {
513 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}