wasm_bindgen_cli_support/
wasm2es6js.rs1use anyhow::{bail, Error};
2use base64::{prelude::BASE64_STANDARD, Engine as _};
3use std::collections::HashSet;
4use std::fmt::Write;
5use walrus::Module;
6
7pub struct Config {
8 base64: bool,
9 fetch_path: Option<String>,
10}
11
12pub struct Output {
13 module: Module,
14 base64: bool,
15 fetch_path: Option<String>,
16}
17
18impl Config {
19 pub fn new() -> Config {
20 Config {
21 base64: false,
22 fetch_path: None,
23 }
24 }
25
26 pub fn base64(&mut self, base64: bool) -> &mut Self {
27 self.base64 = base64;
28 self
29 }
30
31 pub fn fetch(&mut self, path: Option<String>) -> &mut Self {
32 self.fetch_path = path;
33 self
34 }
35
36 pub fn generate(&mut self, wasm: &[u8]) -> Result<Output, Error> {
37 if !self.base64 && self.fetch_path.is_none() {
38 bail!("one of --base64 or --fetch is required");
39 }
40 let module = Module::from_buffer(wasm)?;
41 Ok(Output {
42 module,
43 base64: self.base64,
44 fetch_path: self.fetch_path.clone(),
45 })
46 }
47}
48
49fn push_index_identifier(i: usize, s: &mut String) {
52 let letter = b'a' + ((i % 26) as u8);
53 s.push(letter as char);
54 if i >= 26 {
55 write!(s, "{}", i / 26).unwrap();
56 }
57}
58
59fn args_are_optional(name: &str) -> bool {
60 name == "__wbindgen_thread_destroy"
61}
62
63pub fn interface(module: &Module) -> Result<String, Error> {
64 let mut exports = String::new();
65 module_export_types(module, |name, ty| {
66 if name.contains(':') {
67 writeln!(exports, " readonly {name:?}: {ty};").unwrap();
70 } else {
71 writeln!(exports, " readonly {name}: {ty};").unwrap();
72 }
73 });
74 Ok(exports)
75}
76
77pub fn typescript(module: &Module) -> Result<String, Error> {
78 let mut exports = "/* tslint:disable */\n/* eslint-disable */\n".to_string();
79 module_export_types(module, |name, ty| {
80 writeln!(exports, "export const {name}: {ty};").unwrap();
81 });
82 Ok(exports)
83}
84
85fn module_export_types(module: &Module, mut export: impl FnMut(&str, &str)) {
88 for entry in module.exports.iter() {
89 match entry.item {
90 walrus::ExportItem::Function(id) => {
91 let func = module.funcs.get(id);
92 let ty = module.types.get(func.ty());
93 let ts_type = function_type_to_ts(ty, args_are_optional(&entry.name));
94 export(&entry.name, &ts_type);
95 }
96 walrus::ExportItem::Memory(_) => export(&entry.name, "WebAssembly.Memory"),
97 walrus::ExportItem::Table(_) => export(&entry.name, "WebAssembly.Table"),
98 walrus::ExportItem::Global(_) => continue,
99 walrus::ExportItem::Tag(_) => export(&entry.name, "WebAssembly.Tag"),
100 };
101 }
102}
103fn val_type_to_ts(ty: walrus::ValType) -> &'static str {
104 match ty {
107 walrus::ValType::I32 | walrus::ValType::F32 | walrus::ValType::F64 => "number",
108 walrus::ValType::I64 => "bigint",
109 walrus::ValType::Ref(_) => "any",
111 walrus::ValType::V128 => "any",
116 }
117}
118fn function_type_to_ts(function: &walrus::Type, all_args_optional: bool) -> String {
119 let mut out = String::new();
120
121 out.push('(');
123 for (i, arg_type) in function.params().iter().enumerate() {
124 if i > 0 {
125 out.push_str(", ");
126 }
127
128 push_index_identifier(i, &mut out);
129 if all_args_optional {
130 out.push('?');
131 }
132 out.push_str(": ");
133 out.push_str(val_type_to_ts(*arg_type));
134 }
135 out.push(')');
136
137 out.push_str(" => ");
139
140 let results = function.results();
142 match results.len() {
145 0 => out.push_str("void"),
146 1 => out.push_str(val_type_to_ts(results[0])),
147 _ => {
148 out.push('[');
149 for (i, result) in results.iter().enumerate() {
150 if i > 0 {
151 out.push_str(", ");
152 }
153 out.push_str(val_type_to_ts(*result));
154 }
155 out.push(']');
156 }
157 }
158
159 out
160}
161
162impl Output {
163 pub fn typescript(&self) -> Result<String, Error> {
164 let mut ts = typescript(&self.module)?;
165 if self.base64 {
166 ts.push_str("export const booted: Promise<boolean>;\n");
167 }
168 Ok(ts)
169 }
170
171 pub fn js_and_wasm(mut self) -> Result<(String, Option<Vec<u8>>), Error> {
172 let mut js_imports = String::new();
173 let mut exports = String::new();
174 let mut set_exports = String::new();
175 let mut imports = String::new();
176
177 let mut set = HashSet::new();
178 for entry in self.module.imports.iter() {
179 if !set.insert(&entry.module) {
180 continue;
181 }
182
183 let mut name = String::new();
184 push_index_identifier(set.len(), &mut name);
185
186 js_imports.push_str(&format!(
187 "import * as import_{name} from '{}';\n",
188 entry.module
189 ));
190 imports.push_str(&format!("'{}': import_{name}, ", entry.module));
191 }
192
193 for entry in self.module.exports.iter() {
194 exports.push_str("export let ");
195 exports.push_str(&entry.name);
196 exports.push_str(";\n");
197 set_exports.push_str(&entry.name);
198 set_exports.push_str(" = wasm.exports.");
199 set_exports.push_str(&entry.name);
200 set_exports.push_str(";\n");
201 }
202
203 if self.unstart() {
222 set_exports.push_str("wasm.exports.__wasm2es6js_start();\n");
223 }
224
225 let inst = format!(
226 "
227 WebAssembly.instantiate(bytes,{{ {imports} }})
228 .then(obj => {{
229 const wasm = obj.instance;
230 {set_exports}
231 }})
232 ",
233 );
234 let wasm = self.module.emit_wasm();
235 let (bytes, booted) = if self.base64 {
236 (
237 format!(
238 "
239 let bytes;
240 const base64 = \"{base64}\";
241 if (typeof Buffer === 'undefined') {{
242 bytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0));
243 }} else {{
244 bytes = Buffer.from(base64, 'base64');
245 }}
246 ",
247 base64 = BASE64_STANDARD.encode(&wasm)
248 ),
249 inst,
250 )
251 } else if let Some(ref path) = self.fetch_path {
252 (
253 String::new(),
254 format!(
255 "
256 fetch('{path}')
257 .then(res => res.arrayBuffer())
258 .then(bytes => {inst})
259 "
260 ),
261 )
262 } else {
263 bail!("the option --base64 or --fetch is required");
264 };
265 let js = format!(
266 "\
267 {js_imports}
268 {bytes}
269 export const booted = {booted};
270 {exports}
271 ",
272 );
273 let wasm = if self.base64 { None } else { Some(wasm) };
274 Ok((js, wasm))
275 }
276
277 fn unstart(&mut self) -> bool {
281 let start = match self.module.start.take() {
282 Some(id) => id,
283 None => return false,
284 };
285 self.module.exports.add("__wasm2es6js_start", start);
286 true
287 }
288}
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293
294 #[test]
295 fn test_push_index_identifier() {
296 fn index_identifier(i: usize) -> String {
297 let mut s = String::new();
298 push_index_identifier(i, &mut s);
299 s
300 }
301
302 assert_eq!(index_identifier(0), "a");
303 assert_eq!(index_identifier(1), "b");
304 assert_eq!(index_identifier(25), "z");
305 assert_eq!(index_identifier(26), "a1");
306 assert_eq!(index_identifier(27), "b1");
307 assert_eq!(index_identifier(51), "z1");
308 assert_eq!(index_identifier(52), "a2");
309 assert_eq!(index_identifier(53), "b2");
310 assert_eq!(index_identifier(260), "a10");
311 assert_eq!(index_identifier(261), "b10");
312 }
313}