wasm_bindgen_cli_support/
wasm2es6js.rs

1use 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
49// Function to ensure we always append a valid typescript parameter name based
50// on parameter index
51fn 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            // This can happen when `name` is namespaced, like `__wbgt__reference_test::foo`.
68            // We should quote the name, as : is not valid in TypeScript identifiers.
69            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
85/// Iterates over all the exports in a module and generates TypeScript types. All
86/// name-type pairs are passed to the `export` function.
87fn 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    // see https://webassembly.github.io/spec/js-api/index.html#towebassemblyvalue
105    // and https://webassembly.github.io/spec/js-api/index.html#tojsvalue
106    match ty {
107        walrus::ValType::I32 | walrus::ValType::F32 | walrus::ValType::F64 => "number",
108        walrus::ValType::I64 => "bigint",
109        // there could be anything behind a reference
110        walrus::ValType::Ref(_) => "any",
111        // V128 currently isn't supported in JS and therefore doesn't have a
112        // specific type in the spec. When it does get support, this type will
113        // still be technically correct, but should be updated to something more
114        // specific.
115        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    // parameters
122    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    // arrow
138    out.push_str(" => ");
139
140    // results
141    let results = function.results();
142    // this match follows the spec:
143    // https://webassembly.github.io/spec/js-api/index.html#exported-function-exotic-objects
144    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        // This is sort of tricky, but the gist of it is that if there's a start
204        // function we want to defer execution of the start function until after
205        // all our module's exports are bound. That way we'll execute it as soon
206        // as we're ready, but the module's imports and such will be able to
207        // work as everything is wired up.
208        //
209        // This ends up helping out in situations such as:
210        //
211        // * The start function calls an imported function
212        // * That imported function in turn tries to access the Wasm module
213        //
214        // If we don't do this then the second step won't work because the start
215        // function is automatically executed before the promise of
216        // instantiation resolves, meaning that we won't actually have anything
217        // bound for it to access.
218        //
219        // If we remove the start function here (via `unstart`) then we'll
220        // reexport it as `__wasm2es6js_start` so be manually executed here.
221        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    /// See comments above for what this is doing, but in a nutshell this
278    /// removes the start section, if any, and moves it to an exported function.
279    /// Returns whether a start function was found and removed.
280    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}