1use heck::ToLowerCamelCase;
2
3use crate::intrinsics::Intrinsic;
4use crate::names::{maybe_quote_id, maybe_quote_member, LocalNames};
5use crate::source::Source;
6use crate::{uwrite, uwriteln, TranspileOpts};
7use std::collections::BTreeMap;
8use std::fmt::Write;
9
10type LocalName = String;
11
12enum ImportBinding {
13 Interface(BTreeMap<String, ImportBinding>),
14 Local(Vec<LocalName>),
17}
18
19enum ExportBinding {
20 Interface(BTreeMap<String, ExportBinding>),
21 Local(LocalName),
22}
23
24#[derive(Default)]
25pub struct EsmBindgen {
26 imports: BTreeMap<String, ImportBinding>,
27 exports: BTreeMap<String, ExportBinding>,
28 export_aliases: BTreeMap<String, String>,
29}
30
31impl EsmBindgen {
32 pub fn add_import_binding(&mut self, path: &[String], binding_name: String) {
37 let mut iface = &mut self.imports;
38 for i in 0..path.len() - 1 {
39 if !iface.contains_key(&path[i]) {
40 iface.insert(
41 path[i].to_string(),
42 ImportBinding::Interface(BTreeMap::new()),
43 );
44 }
45 iface = match iface.get_mut(&path[i]).unwrap() {
46 ImportBinding::Interface(iface) => iface,
47 ImportBinding::Local(local) => {
48 panic!(
49 "Internal bindgen error: Import '{}' cannot be both an interface '{}' and a function '{}'",
50 &path[0..i + 1].join("."),
51 &path[i + 1..].join("."),
52 &local[0],
53 );
54 }
55 };
56 }
57 if let Some(ref mut existing) = iface.get_mut(&path[path.len() - 1]) {
58 match existing {
59 ImportBinding::Interface(_) => {
60 unreachable!("Multi-version interfaces must have the same shape")
61 }
62 ImportBinding::Local(ref mut local_names) => {
63 if !local_names.contains(&binding_name) {
64 local_names.push(binding_name);
65 }
66 }
67 }
68 } else {
69 iface.insert(
70 path[path.len() - 1].to_string(),
71 ImportBinding::Local(vec![binding_name]),
72 );
73 }
74 }
75
76 pub fn add_export_binding(
78 &mut self,
79 iface_id_or_kebab: Option<&str>,
80 local_name: String,
81 func_name: String,
82 ) {
83 let mut iface = &mut self.exports;
84 if let Some(iface_id_or_kebab) = iface_id_or_kebab {
85 let iface_id_or_kebab = if iface_id_or_kebab.contains(':') {
87 iface_id_or_kebab.to_string()
88 } else {
89 iface_id_or_kebab.to_lower_camel_case()
90 };
91 if !iface.contains_key(&iface_id_or_kebab) {
92 iface.insert(
93 iface_id_or_kebab.to_string(),
94 ExportBinding::Interface(BTreeMap::new()),
95 );
96 }
97 iface = match iface.get_mut(&iface_id_or_kebab).unwrap() {
98 ExportBinding::Interface(iface) => iface,
99 ExportBinding::Local(_) => panic!(
100 "Exported interface {} cannot be both a function and an interface",
101 iface_id_or_kebab
102 ),
103 };
104 }
105 iface.insert(func_name, ExportBinding::Local(local_name));
106 }
107
108 pub fn populate_export_aliases(&mut self) {
111 for expt_name in self.exports.keys() {
112 if let Some(path_idx) = expt_name.rfind('/') {
113 let end = if let Some(version_idx) = expt_name.rfind('@') {
114 version_idx
115 } else {
116 expt_name.len()
117 };
118 let alias = &expt_name[path_idx + 1..end].to_lower_camel_case();
119 if !self.exports.contains_key(alias) && !self.export_aliases.contains_key(alias) {
120 self.export_aliases
121 .insert(alias.to_string(), expt_name.to_string());
122 }
123 }
124 }
125 }
126
127 pub fn import_specifiers(&self) -> Vec<String> {
129 self.imports.keys().map(|impt| impt.to_string()).collect()
130 }
131
132 pub fn exports(&self) -> Vec<(&str, &str)> {
134 self.export_aliases
135 .iter()
136 .map(|(alias, name)| (alias.as_ref(), name.as_ref()))
137 .chain(
138 self.exports
139 .keys()
140 .map(|name| (name.as_ref(), name.as_ref())),
141 )
142 .collect()
143 }
144
145 pub fn render_exports(
146 &mut self,
147 output: &mut Source,
148 instantiation: bool,
149 local_names: &mut LocalNames,
150 opts: &TranspileOpts,
151 ) {
152 if self.exports.is_empty() {
153 if instantiation {
154 output.push_str("return {}");
155 }
156 return;
157 }
158 for (export_name, export) in self.exports.iter() {
162 let ExportBinding::Interface(iface) = export else {
163 continue;
164 };
165 let (local_name, _) =
166 local_names.get_or_create(format!("export:{export_name}"), export_name);
167 uwriteln!(output, "const {local_name} = {{");
168 for (func_name, export) in iface {
169 let ExportBinding::Local(local_name) = export else {
170 panic!("Unsupported nested export interface");
171 };
172 uwriteln!(output, "{}: {local_name},", maybe_quote_id(func_name));
173 }
174 uwriteln!(output, "\n}};");
175 }
176 uwrite!(
177 output,
178 "\n{} {{ ",
179 if instantiation { "return" } else { "export" }
180 );
181 let mut first = true;
182 for (alias, export_name) in &self.export_aliases {
183 if first {
184 first = false
185 }
186 let local_name = match &self.exports[export_name] {
187 ExportBinding::Local(local_name) => local_name,
188 ExportBinding::Interface(_) => local_names.get(format!("export:{}", export_name)),
189 };
190 let alias_maybe_quoted = maybe_quote_id(alias);
191 if local_name == alias_maybe_quoted {
192 output.push_str(local_name);
193 uwrite!(output, ", ");
194 } else if instantiation {
195 uwrite!(output, "{alias_maybe_quoted}: {local_name}");
196 uwrite!(output, ", ");
197 } else if !self.contains_js_quote(&alias_maybe_quoted) || !opts.no_namespaced_exports {
198 uwrite!(output, "{local_name} as {alias_maybe_quoted}");
199 uwrite!(output, ", ");
200 }
201 }
202 for (export_name, export) in &self.exports {
203 if first {
204 first = false
205 }
206 let local_name = match export {
207 ExportBinding::Local(local_name) => local_name,
208 ExportBinding::Interface(_) => local_names.get(format!("export:{}", export_name)),
209 };
210 let export_name_maybe_quoted = maybe_quote_id(export_name);
211 if local_name == export_name_maybe_quoted {
212 output.push_str(local_name);
213 uwrite!(output, ", ");
214 } else if instantiation {
215 uwrite!(output, "{export_name_maybe_quoted}: {local_name}");
216 uwrite!(output, ", ");
217 } else if !self.contains_js_quote(&export_name_maybe_quoted)
218 || !opts.no_namespaced_exports
219 {
220 uwrite!(output, "{local_name} as {export_name_maybe_quoted}");
221 uwrite!(output, ", ");
222 }
223 }
224 uwrite!(output, " }}");
225 }
226
227 fn contains_js_quote(&self, js_string: &str) -> bool {
228 js_string.contains("\"") || js_string.contains("'") || js_string.contains("`")
229 }
230
231 pub fn render_imports(
232 &mut self,
233 output: &mut Source,
234 imports_object: Option<&str>,
235 local_names: &mut LocalNames,
236 ) {
237 let mut iface_imports = Vec::new();
238 for (specifier, binding) in &self.imports {
239 let idl_binding = if specifier.starts_with("webidl:") {
240 let iface_idx = specifier.find('/').unwrap() + 1;
241 let iface_name = if let Some(version_idx) = specifier.find('@') {
242 &specifier[iface_idx..version_idx]
243 } else {
244 &specifier[iface_idx..]
245 };
246 Some(iface_name.strip_prefix("global-").unwrap_or_default())
247 } else {
248 None
249 };
250 if imports_object.is_some() || idl_binding.is_some() {
251 uwrite!(output, "const ");
252 } else {
253 uwrite!(output, "import ");
254 }
255 match binding {
256 ImportBinding::Interface(bindings) => {
257 if imports_object.is_none() && idl_binding.is_none() && bindings.len() == 1 {
258 let (import_name, import) = bindings.iter().next().unwrap();
259 if import_name == "default" {
260 match import {
261 ImportBinding::Interface(iface) => {
262 let iface_local_name = local_names.create_once(specifier);
263 iface_imports.push((iface_local_name.to_string(), iface));
264 uwriteln!(output, "{iface_local_name} from '{specifier}';");
265 }
266 ImportBinding::Local(local_names) => {
267 let local_name = &local_names[0];
268 uwriteln!(output, "{local_name} from '{specifier}';");
269 for other_local_name in &local_names[1..] {
270 uwriteln!(
271 output,
272 "const {other_local_name} = {local_name};"
273 );
274 }
275 }
276 };
277 continue;
278 }
279 }
280 uwrite!(output, "{{");
281 let mut first = true;
282 for (external_name, import) in bindings {
283 match import {
284 ImportBinding::Interface(iface) => {
285 if first {
286 output.push_str(" ");
287 first = false;
288 } else {
289 output.push_str(", ");
290 }
291 let (iface_local_name, _) = local_names.get_or_create(
292 format!("import:{specifier}#{external_name}"),
293 external_name,
294 );
295 iface_imports.push((iface_local_name.to_string(), iface));
296 if external_name == iface_local_name {
297 uwrite!(output, "{external_name}");
298 } else if imports_object.is_some() || idl_binding.is_some() {
299 uwrite!(output, "{external_name}: {iface_local_name}");
300 } else {
301 uwrite!(output, "{external_name} as {iface_local_name}");
302 }
303 }
304 ImportBinding::Local(local_names) => {
305 for local_name in local_names {
306 if first {
307 output.push_str(" ");
308 first = false;
309 } else {
310 output.push_str(", ");
311 }
312 if external_name == local_name {
313 uwrite!(output, "{external_name}");
314 } else if imports_object.is_some() || idl_binding.is_some() {
315 uwrite!(output, "{external_name}: {local_name}");
316 } else {
317 uwrite!(output, "{external_name} as {local_name}");
318 }
319 }
320 }
321 };
322 }
323 if !first {
324 output.push_str(" ");
325 }
326 if let Some(imports_object) = imports_object {
327 uwriteln!(
328 output,
329 "}} = {imports_object}{};",
330 maybe_quote_member(specifier)
331 );
332 } else if let Some(idl_binding) = idl_binding {
333 uwrite!(output, "}} = {}()", Intrinsic::GlobalThisIdlProxy.name());
334 if !idl_binding.is_empty() {
335 for segment in idl_binding.split('-') {
336 uwrite!(output, ".{}()", segment.to_lowercase());
337 }
338 }
339 uwrite!(output, ";\n");
340 } else {
341 uwriteln!(output, "}} from '{specifier}';");
342 }
343 }
344 ImportBinding::Local(local_names) => {
345 let local_name = &local_names[0];
346 if let Some(imports_object) = imports_object {
347 uwriteln!(
348 output,
349 "{local_name} = {imports_object}{}.default;",
350 maybe_quote_member(specifier)
351 );
352 } else {
353 uwriteln!(output, "{local_name} from '{specifier}';");
354 }
355 for other_local_name in &local_names[1..] {
356 uwriteln!(output, "const {other_local_name} = {local_name};");
357 }
358 }
359 }
360 }
361 for (iface_local_name, iface_imports) in iface_imports {
363 uwrite!(output, "const {{");
364 let mut first = true;
365 for (member_name, binding) in iface_imports {
366 let ImportBinding::Local(local_names) = binding else {
367 continue;
368 };
369 for local_name in local_names {
370 if first {
371 output.push_str(" ");
372 first = false;
373 } else {
374 output.push_str(",\n");
375 }
376 if member_name == local_name {
377 output.push_str(local_name);
378 } else {
379 uwrite!(output, "{member_name}: {local_name}");
380 }
381 }
382 }
383 if !first {
384 output.push_str(" ");
385 }
386 uwriteln!(output, "}} = {iface_local_name};");
387 }
388 }
389}