wasm_rquickjs/
lib.rs

1use crate::conversions::generate_conversions;
2use crate::exports::generate_export_impls;
3use crate::imports::generate_import_modules;
4use crate::skeleton::{copy_skeleton_sources, generate_app_manifest, generate_cargo_toml};
5use anyhow::{Context, anyhow};
6use camino::Utf8Path;
7use fs_extra::dir::CopyOptions;
8use heck::{ToSnakeCase, ToUpperCamelCase};
9use proc_macro2::{Ident, Span};
10use std::cell::RefCell;
11use std::collections::{BTreeSet, VecDeque};
12use wit_parser::{
13    Function, Interface, InterfaceId, PackageId, PackageName, PackageSourceMap, Resolve, TypeId,
14    TypeOwner, WorldId, WorldItem,
15};
16
17mod conversions;
18mod exports;
19mod imports;
20mod javascript;
21mod rust_bindgen;
22mod skeleton;
23mod types;
24mod typescript;
25
26/// Generates a Rust wrapper crate for a combination of a WIT package and a JavaScript module.
27///
28/// The `wit` parameter should point to a WIT root (holding the WIT package of the component, with
29/// optionally a `deps` subdirectory with an arbitrary number of dependencies).
30///
31/// The `js` parameter must point to a single JavaScript module that implements the WIT package.
32///
33/// The `output` parameter is the root directory where the generated Rust crate's source code and
34/// Cargo manifest is placed.
35///
36/// If `world` is `None`, the default world is selected and used, otherwise the specified one.
37pub fn generate_wrapper_crate(
38    wit: &Utf8Path,
39    js: &Utf8Path,
40    output: &Utf8Path,
41    world: Option<&str>,
42) -> anyhow::Result<()> {
43    // Making sure the target directories exists
44    std::fs::create_dir_all(output).context("Failed to create output directory")?;
45    std::fs::create_dir_all(output.join("src")).context("Failed to create output/src directory")?;
46    std::fs::create_dir_all(output.join("src").join("modules"))
47        .context("Failed to create output/src/modules directory")?;
48
49    // Resolving the WIT package
50    let context = GeneratorContext::new(output, wit, world)?;
51
52    // Generating the Cargo.toml file
53    generate_cargo_toml(&context)?;
54
55    // Generating a Golem App Manifest file (for debugging)
56    generate_app_manifest(&context)?;
57
58    // Copying the skeleton files
59    copy_skeleton_sources(context.output).context("Failed to copy skeleton sources")?;
60
61    // Copying the WIT package to the output directory
62    copy_wit_directory(wit, &context.output.join("wit"))
63        .context("Failed to copy WIT package to output directory")?;
64
65    // Copying the JavaScript module to the output directory
66    copy_js_module(js, context.output)
67        .context("Failed to copy JavaScript module to output directory")?;
68
69    // Generating the lib.rs file implementing the component exports
70    generate_export_impls(&context)
71        .context("Failed to generate the component export implementations")?;
72
73    // Generating the native modules implementing the component imports
74    generate_import_modules(&context).context("Failed to generate the component import modules")?;
75
76    // Generating the conversions.rs file implementing the IntoJs and FromJs typeclass instances
77    // This step must be done after `generate_export_impls` to ensure all visited types are registered.
78    generate_conversions(&context)
79        .context("Failed to generate the IntoJs and FromJs typeclass instances")?;
80
81    Ok(())
82}
83
84/// Generates TypeScript module definitions for a given (or default) world of a WIT package.
85pub fn generate_dts(wit: &Utf8Path, output: &Utf8Path, world: Option<&str>) -> anyhow::Result<()> {
86    // Making sure the target directories exists
87    std::fs::create_dir_all(output).context("Failed to create output directory")?;
88
89    // Resolving the WIT package
90    let context = GeneratorContext::new(output, wit, world)?;
91
92    typescript::generate_export_module(&context)
93        .context("Failed to generate the TypeScript module definition for the exports")?;
94
95    // Generating the native modules implementing the component imports
96    typescript::generate_import_modules(&context)
97        .context("Failed to generate the TypeScript module definitions for the imported modules")?;
98
99    Ok(())
100}
101
102struct GeneratorContext<'a> {
103    output: &'a Utf8Path,
104    wit_source_path: &'a Utf8Path,
105    resolve: Resolve,
106    root_package: PackageId,
107    world: WorldId,
108    source_map: PackageSourceMap,
109    visited_types: RefCell<BTreeSet<TypeId>>,
110    world_name: String,
111    types: wit_bindgen_core::Types,
112}
113
114impl<'a> GeneratorContext<'a> {
115    fn new(output: &'a Utf8Path, wit: &'a Utf8Path, world: Option<&str>) -> anyhow::Result<Self> {
116        let mut resolve = Resolve::default();
117        let (root_package, source_map) = resolve
118            .push_path(wit)
119            .context("Failed to resolve WIT package")?;
120        let world = resolve
121            .select_world(root_package, world)
122            .context("Failed to select WIT world")?;
123
124        let world_name = resolve.worlds[world].name.clone();
125
126        let mut types = wit_bindgen_core::Types::default();
127        types.analyze(&resolve);
128
129        Ok(Self {
130            output,
131            wit_source_path: wit,
132            resolve,
133            root_package,
134            world,
135            source_map,
136            visited_types: RefCell::new(BTreeSet::new()),
137            world_name,
138            types,
139        })
140    }
141
142    fn root_package_name(&self) -> String {
143        self.resolve.packages[self.root_package].name.to_string()
144    }
145
146    fn record_visited_type(&self, type_id: TypeId) {
147        self.visited_types.borrow_mut().insert(type_id);
148    }
149
150    fn is_exported_interface(&self, interface_id: InterfaceId) -> bool {
151        let world = &self.resolve.worlds[self.world];
152        world
153            .exports
154            .iter()
155            .any(|(_, item)| matches!(item, WorldItem::Interface { id, .. } if id == &interface_id))
156    }
157
158    fn is_exported_type(&self, type_id: TypeId) -> bool {
159        if let Some(typ) = self.resolve.types.get(type_id) {
160            match &typ.owner {
161                TypeOwner::World(world_id) => {
162                    if world_id == &self.world {
163                        let world = &self.resolve.worlds[self.world];
164                        world
165                            .exports
166                            .iter()
167                            .any(|(_, item)| matches!(item, WorldItem::Type(id) if id == &type_id))
168                    } else {
169                        false
170                    }
171                }
172                TypeOwner::Interface(interface_id) => self.is_exported_interface(*interface_id),
173                TypeOwner::None => false,
174            }
175        } else {
176            false
177        }
178    }
179
180    fn bindgen_type_info(&self, type_id: TypeId) -> wit_bindgen_core::TypeInfo {
181        self.types.get(type_id)
182    }
183
184    fn get_imported_interface(
185        &self,
186        interface_id: &InterfaceId,
187    ) -> anyhow::Result<ImportedInterface> {
188        let interface = &self.resolve.interfaces[*interface_id];
189        let name = interface
190            .name
191            .as_ref()
192            .ok_or_else(|| anyhow!("Interface import does not have a name"))?
193            .as_str();
194
195        let functions = interface
196            .functions
197            .iter()
198            .map(|(name, f)| (name.as_str(), f))
199            .collect();
200
201        let package_id = interface
202            .package
203            .ok_or_else(|| anyhow!("Anonymous interface imports are not supported yet"))?;
204        let package = self
205            .resolve
206            .packages
207            .get(package_id)
208            .ok_or_else(|| anyhow!("Could not find package of imported interface {name}"))?;
209        let package_name = &package.name;
210
211        Ok(ImportedInterface {
212            package_name: Some(package_name),
213            name: name.to_string(),
214            functions,
215            interface: Some(interface),
216            interface_id: Some(*interface_id),
217        })
218    }
219}
220
221pub struct ImportedInterface<'a> {
222    package_name: Option<&'a PackageName>,
223    name: String,
224    functions: Vec<(&'a str, &'a Function)>,
225    interface: Option<&'a Interface>,
226    interface_id: Option<InterfaceId>,
227}
228
229impl<'a> ImportedInterface<'a> {
230    pub fn module_name(&self) -> anyhow::Result<String> {
231        let package_name = self
232            .package_name
233            .ok_or_else(|| anyhow!("imported interface has no package name"))?;
234        let interface_name = &self.name;
235
236        Ok(format!(
237            "{}_{}",
238            package_name.to_string().to_snake_case(),
239            interface_name.to_snake_case()
240        ))
241    }
242
243    pub fn rust_interface_name(&self) -> Ident {
244        let interface_name = format!("Js{}Module", self.name.to_upper_camel_case());
245        Ident::new(&interface_name, Span::call_site())
246    }
247
248    pub fn name_and_interface(&self) -> Option<(&str, &Interface)> {
249        self.interface
250            .map(|interface| (self.name.as_str(), interface))
251    }
252
253    pub fn fully_qualified_interface_name(&self) -> String {
254        if let Some(package_name) = &self.package_name {
255            package_name.interface_id(&self.name)
256        } else {
257            self.name.clone()
258        }
259    }
260
261    pub fn interface_stack(&self) -> VecDeque<InterfaceId> {
262        self.interface_id.iter().cloned().collect()
263    }
264}
265
266/// Recursively copies a WIT directory to `<output>/wit`.
267fn copy_wit_directory(wit: &Utf8Path, output: &Utf8Path) -> anyhow::Result<()> {
268    fs_extra::dir::create(output, true)
269        .context("Failed to create and erase output WIT directory")?;
270    fs_extra::dir::copy(wit, output, &CopyOptions::new().content_only(true))
271        .context("Failed to copy WIT directory")?;
272
273    Ok(())
274}
275
276/// Copies the JS module file to `<output>/src/module.js`.
277fn copy_js_module(js: &Utf8Path, output: &Utf8Path) -> anyhow::Result<()> {
278    let js_dest = output.join("src").join("module.js");
279    std::fs::copy(js, js_dest).context("Failed to copy JavaScript module")?;
280    Ok(())
281}