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 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 pub fn add_import_binding(&mut self, path: &[String], binding_name: String) {
38 let mut iface = &mut self.imports;
39
40 for i in 0..path.len() - 1 {
43 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 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 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 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 pub fn import_specifiers(&self) -> Vec<String> {
135 self.imports.keys().map(|impt| impt.to_string()).collect()
136 }
137
138 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 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 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}