1use std::path::Path;
2
3use anyhow::Error;
4use heck::{ToPascalCase, ToSnakeCase};
5use minijinja::Environment;
6use once_cell::sync::Lazy;
7use wai_bindgen_gen_core::Generator;
8use wai_bindgen_gen_js::Js;
9use wai_parser::Interface;
10
11use crate::{types::Command, Files, Library, Metadata, Package, SourceFile};
12
13const WASMER_WASI_VERSION: &str = "^1.2.2";
18
19static TEMPLATES: Lazy<Environment> = Lazy::new(|| {
20 let mut env = Environment::new();
21 env.add_template("bindings.index.js", include_str!("bindings.index.js.j2"))
22 .unwrap();
23 env.add_template(
24 "bindings.index.d.ts",
25 include_str!("bindings.index.d.ts.j2"),
26 )
27 .unwrap();
28 env.add_template("command.d.ts", include_str!("command.d.ts.j2"))
29 .unwrap();
30 env.add_template("command.js", include_str!("command.js.j2"))
31 .unwrap();
32 env.add_template("top-level.index.js", include_str!("top-level.index.js.j2"))
33 .unwrap();
34 env.add_template(
35 "top-level.index.d.ts",
36 include_str!("top-level.index.d.ts.j2"),
37 )
38 .unwrap();
39
40 env
41});
42
43pub fn generate_javascript(package: &Package) -> Result<Files, Error> {
45 let mut files = Files::new();
46
47 let ctx = Context::for_package(package);
48
49 files.insert_child_directory(Path::new("src").join("bindings"), library_bindings(&ctx)?);
50
51 for cmd in &ctx.commands {
52 files.insert_child_directory(Path::new("src").join("commands"), command_bindings(cmd)?);
53 }
54
55 files.insert_child_directory("src", top_level(&ctx)?);
56
57 let package_json = generate_package_json(package.requires_wasi(), package.metadata());
58 files.insert("package.json", package_json);
59
60 let mut f = Files::new();
63 f.insert_child_directory("package", files);
64
65 Ok(f)
66}
67
68#[derive(Debug, serde::Serialize)]
69struct Context {
70 libraries: Vec<LibraryContext>,
71 commands: Vec<CommandContext>,
72 generator: String,
73 wasi: bool,
74 has_wasi_libraries: bool,
75}
76
77impl Context {
78 fn for_package(pkg: &Package) -> Self {
79 let libraries: Vec<_> = pkg
80 .libraries()
81 .iter()
82 .map(LibraryContext::for_lib)
83 .collect();
84 let commands: Vec<_> = pkg.commands().iter().map(CommandContext::for_cmd).collect();
85
86 let has_wasi_libraries = libraries.iter().any(|lib| lib.wasi);
87
88 let wasi = !commands.is_empty() || has_wasi_libraries;
89
90 Context {
91 libraries,
92 commands,
93 generator: crate::GENERATOR.to_string(),
94 wasi,
95 has_wasi_libraries,
96 }
97 }
98}
99
100#[derive(Debug, serde::Serialize)]
101struct CommandContext {
102 name: String,
103 ident: String,
104 module_filename: String,
105 #[serde(skip)]
106 wasm: Vec<u8>,
107}
108
109impl CommandContext {
110 fn for_cmd(cmd: &Command) -> CommandContext {
111 let module_filename = Path::new(&cmd.name).with_extension("wasm");
112
113 CommandContext {
114 name: cmd.name.clone(),
115 ident: cmd.name.replace('-', "_"),
116 module_filename: module_filename.display().to_string(),
117 wasm: cmd.wasm.clone(),
118 }
119 }
120}
121
122#[derive(Debug, serde::Serialize)]
123struct LibraryContext {
124 ident: String,
126 module_filename: String,
128 wasi: bool,
130 exports: InterfaceContext,
131 imports: Vec<InterfaceContext>,
132 #[serde(skip)]
133 wasm: Vec<u8>,
134}
135
136impl LibraryContext {
137 fn for_lib(lib: &Library) -> Self {
138 let module_filename = Path::new(lib.module_filename()).with_extension("wasm");
139 let interface_name = lib.interface_name();
140 let ident = interface_name.to_snake_case();
141
142 let exports = InterfaceContext {
143 interface_name: lib.exports.name().to_string(),
144 class_name: lib.exports.name().to_pascal_case(),
145 interface: lib.exports.0.clone(),
146 };
147 let imports = lib
148 .imports
149 .iter()
150 .map(|interface| InterfaceContext {
151 interface_name: interface.name().to_string(),
152 class_name: interface.name().to_pascal_case(),
153 interface: interface.0.clone(),
154 })
155 .collect();
156
157 LibraryContext {
158 ident,
159 module_filename: module_filename.display().to_string(),
160 wasi: lib.requires_wasi(),
161 exports,
162 imports,
163 wasm: lib.module.wasm.clone(),
164 }
165 }
166}
167
168#[derive(Debug, serde::Serialize)]
169struct InterfaceContext {
170 interface_name: String,
173 class_name: String,
175 #[serde(skip)]
176 interface: Interface,
177}
178
179fn command_bindings(cmd: &CommandContext) -> Result<Files, Error> {
180 let mut files = Files::new();
181 let module_filename = Path::new(&cmd.name).with_extension("wasm");
182
183 files.insert(
184 Path::new(&cmd.name).with_extension("js"),
185 TEMPLATES
186 .get_template("command.js")
187 .unwrap()
188 .render(cmd)?
189 .into(),
190 );
191
192 files.insert(
193 Path::new(&cmd.name).with_extension("d.ts"),
194 TEMPLATES
195 .get_template("command.d.ts")
196 .unwrap()
197 .render(cmd)?
198 .into(),
199 );
200 files.insert(module_filename, SourceFile::from(&cmd.wasm));
201
202 Ok(files)
203}
204
205fn top_level(ctx: &Context) -> Result<Files, Error> {
206 let mut files = Files::new();
207
208 files.insert(
209 "index.js",
210 TEMPLATES
211 .get_template("top-level.index.js")
212 .unwrap()
213 .render(ctx)?
214 .into(),
215 );
216
217 files.insert(
218 "index.d.ts",
219 TEMPLATES
220 .get_template("top-level.index.d.ts")
221 .unwrap()
222 .render(ctx)?
223 .into(),
224 );
225
226 Ok(files)
227}
228
229fn library_bindings(ctx: &Context) -> Result<Files, Error> {
230 let mut files = Files::new();
231
232 for LibraryContext {
233 module_filename,
234 exports,
235 imports,
236 wasm,
237 ..
238 } in &ctx.libraries
239 {
240 let mut bindings = generate_bindings(exports, imports);
241 bindings.insert(module_filename, wasm.into());
242 files.insert_child_directory(&exports.interface_name, bindings);
243 }
244
245 let index_js = TEMPLATES
246 .get_template("bindings.index.js")
247 .unwrap()
248 .render(ctx)?;
249 files.insert("index.js", index_js.into());
250
251 let typings_file = TEMPLATES
252 .get_template("bindings.index.d.ts")
253 .unwrap()
254 .render(ctx)?;
255 files.insert("index.d.ts", typings_file.into());
256
257 Ok(files)
258}
259
260fn generate_package_json(needs_wasi: bool, metadata: &Metadata) -> SourceFile {
261 let dependencies = if needs_wasi {
262 serde_json::json!({
263 "@wasmer/wasi": WASMER_WASI_VERSION,
264 })
265 } else {
266 serde_json::json!({})
267 };
268
269 let package_json = serde_json::json!({
270 "name": metadata.package_name.javascript_package(),
271 "version": &metadata.version,
272 "main": format!("src/index.js"),
273 "types": format!("src/index.d.ts"),
274 "type": "commonjs",
275 "dependencies": dependencies,
276 });
277
278 format!("{package_json:#}").into()
279}
280
281fn generate_bindings(
282 guest_exports: &InterfaceContext,
283 guest_imports: &[InterfaceContext],
284) -> Files {
285 let host_imports: &[wai_parser::Interface] = &[guest_exports.interface.clone()];
289 let host_exports: Vec<_> = guest_imports.iter().map(|i| i.interface.clone()).collect();
290
291 let mut generated = wai_bindgen_gen_core::Files::default();
292
293 Js::new().generate_all(host_imports, &host_exports, &mut generated);
294
295 generated.into()
296}
297
298#[cfg(test)]
299mod tests {
300 use std::collections::BTreeSet;
301
302 use insta::Settings;
303
304 use crate::{Metadata, Module};
305
306 use super::*;
307
308 #[test]
309 fn package_json() {
310 let metadata = Metadata::new("wasmerio/wasmer-pack".parse().unwrap(), "0.0.0");
311
312 let got = generate_package_json(false, &metadata);
313
314 insta::assert_display_snapshot!(got.utf8_contents().unwrap());
315 }
316
317 #[test]
318 fn package_json_wasi() {
319 let metadata = Metadata::new("wasmerio/wabt".parse().unwrap(), "0.0.0");
320
321 let got = generate_package_json(true, &metadata);
322
323 insta::assert_display_snapshot!(got.utf8_contents().unwrap());
324 }
325
326 const WASMER_PACK_EXPORTS: &str = include_str!(concat!(
327 env!("CARGO_MANIFEST_DIR"),
328 "/../wasm/wasmer-pack.exports.wai"
329 ));
330
331 #[test]
332 fn generated_files() {
333 let expected: BTreeSet<&Path> = [
334 "package/package.json",
335 "package/src/bindings/index.d.ts",
336 "package/src/bindings/index.js",
337 "package/src/bindings/wasmer-pack/browser.d.ts",
338 "package/src/bindings/wasmer-pack/browser.js",
339 "package/src/bindings/wasmer-pack/intrinsics.js",
340 "package/src/bindings/wasmer-pack/wasmer_pack_wasm.wasm",
341 "package/src/bindings/wasmer-pack/wasmer-pack.d.ts",
342 "package/src/bindings/wasmer-pack/wasmer-pack.js",
343 "package/src/commands/first.d.ts",
344 "package/src/commands/first.js",
345 "package/src/commands/first.wasm",
346 "package/src/commands/second-with-dashes.d.ts",
347 "package/src/commands/second-with-dashes.js",
348 "package/src/commands/second-with-dashes.wasm",
349 "package/src/index.d.ts",
350 "package/src/index.js",
351 ]
352 .iter()
353 .map(Path::new)
354 .collect();
355 let metadata = Metadata::new("wasmer/wasmer-pack".parse().unwrap(), "1.2.3");
356 let module = Module {
357 name: "wasmer_pack_wasm.wasm".to_string(),
358 abi: crate::Abi::None,
359 wasm: Vec::new(),
360 };
361 let exports =
362 crate::Interface::from_wit("wasmer-pack.exports.wit", WASMER_PACK_EXPORTS).unwrap();
363 let commands = vec![
364 Command::new("first", []),
365 Command::new("second-with-dashes", []),
366 ];
367 let browser =
368 crate::Interface::from_wit("browser.wit", "greet: func(who: string) -> string")
369 .unwrap();
370 let libraries = vec![Library {
371 module,
372 exports,
373 imports: vec![browser],
374 }];
375 let pkg = Package::new(metadata, libraries, commands);
376
377 let files = generate_javascript(&pkg).unwrap();
378
379 let actual_files: BTreeSet<_> = files.iter().map(|(p, _)| p).collect();
380 assert_eq!(actual_files, expected);
381
382 let mut settings = Settings::clone_current();
383 settings.add_filter(
384 r"Generated by wasmer-pack v\d+\.\d+\.\d+(-\w+(\.\d+)?)?",
385 "Generated by XXX",
386 );
387 settings.bind(|| {
388 insta::assert_display_snapshot!(files["package/package.json"].utf8_contents().unwrap());
389 insta::assert_display_snapshot!(files["package/src/commands/first.d.ts"]
390 .utf8_contents()
391 .unwrap());
392 insta::assert_display_snapshot!(files["package/src/commands/first.js"]
393 .utf8_contents()
394 .unwrap());
395 insta::assert_display_snapshot!(files["package/src/commands/second-with-dashes.d.ts"]
396 .utf8_contents()
397 .unwrap());
398 insta::assert_display_snapshot!(files["package/src/commands/second-with-dashes.js"]
399 .utf8_contents()
400 .unwrap());
401 insta::assert_display_snapshot!(files["package/src/index.d.ts"]
402 .utf8_contents()
403 .unwrap());
404 insta::assert_display_snapshot!(files["package/src/index.js"].utf8_contents().unwrap());
405 insta::assert_display_snapshot!(files["package/src/bindings/index.d.ts"]
406 .utf8_contents()
407 .unwrap());
408 insta::assert_display_snapshot!(files["package/src/bindings/index.js"]
409 .utf8_contents()
410 .unwrap());
411 insta::assert_display_snapshot!(files["package/src/bindings/wasmer-pack/browser.d.ts"]
412 .utf8_contents()
413 .unwrap());
414 insta::assert_display_snapshot!(files["package/src/bindings/wasmer-pack/browser.js"]
415 .utf8_contents()
416 .unwrap());
417 });
418 }
419}