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