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, TypeDef,
15    TypeId, 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.
116///
117/// Returns the list of generated files.
118pub fn generate_dts(
119    wit: &Utf8Path,
120    output: &Utf8Path,
121    world: Option<&str>,
122) -> anyhow::Result<Vec<Utf8PathBuf>> {
123    // Making sure the target directories exist
124    std::fs::create_dir_all(output).context("Failed to create output directory")?;
125
126    // Resolving the WIT package
127    let context = GeneratorContext::new(output, wit, world)?;
128
129    let mut result = Vec::new();
130    result.extend(
131        typescript::generate_export_module(&context)
132            .context("Failed to generate the TypeScript module definition for the exports")?,
133    );
134
135    // Generating the native modules implementing the component imports
136    result.extend(typescript::generate_import_modules(&context).context(
137        "Failed to generate the TypeScript module definitions for the imported modules",
138    )?);
139
140    Ok(result)
141}
142
143struct GeneratorContext<'a> {
144    output: &'a Utf8Path,
145    wit_source_path: &'a Utf8Path,
146    resolve: Resolve,
147    root_package: PackageId,
148    world: WorldId,
149    source_map: PackageSourceMap,
150    visited_types: RefCell<BTreeSet<TypeId>>,
151    world_name: String,
152    types: wit_bindgen_core::Types,
153}
154
155impl<'a> GeneratorContext<'a> {
156    fn new(output: &'a Utf8Path, wit: &'a Utf8Path, world: Option<&str>) -> anyhow::Result<Self> {
157        let mut resolve = Resolve::default();
158        let (root_package, source_map) = resolve
159            .push_path(wit)
160            .context("Failed to resolve WIT package")?;
161        let world = resolve
162            .select_world(root_package, world)
163            .context("Failed to select WIT world")?;
164
165        let world_name = resolve.worlds[world].name.clone();
166
167        let mut types = wit_bindgen_core::Types::default();
168        types.analyze(&resolve);
169
170        Ok(Self {
171            output,
172            wit_source_path: wit,
173            resolve,
174            root_package,
175            world,
176            source_map,
177            visited_types: RefCell::new(BTreeSet::new()),
178            world_name,
179            types,
180        })
181    }
182
183    fn root_package_name(&self) -> String {
184        self.resolve.packages[self.root_package].name.to_string()
185    }
186
187    fn record_visited_type(&self, type_id: TypeId) {
188        self.visited_types.borrow_mut().insert(type_id);
189    }
190
191    fn is_exported_interface(&self, interface_id: InterfaceId) -> bool {
192        let world = &self.resolve.worlds[self.world];
193        world
194            .exports
195            .iter()
196            .any(|(_, item)| matches!(item, WorldItem::Interface { id, .. } if id == &interface_id))
197    }
198
199    fn is_exported_type(&self, type_id: TypeId) -> bool {
200        if let Some(typ) = self.resolve.types.get(type_id) {
201            match &typ.owner {
202                TypeOwner::World(world_id) => {
203                    if world_id == &self.world {
204                        let world = &self.resolve.worlds[self.world];
205                        world
206                            .exports
207                            .iter()
208                            .any(|(_, item)| matches!(item, WorldItem::Type(id) if id == &type_id))
209                    } else {
210                        false
211                    }
212                }
213                TypeOwner::Interface(interface_id) => self.is_exported_interface(*interface_id),
214                TypeOwner::None => false,
215            }
216        } else {
217            false
218        }
219    }
220
221    fn bindgen_type_info(&self, type_id: TypeId) -> wit_bindgen_core::TypeInfo {
222        self.types.get(type_id)
223    }
224
225    fn get_imported_interface(
226        &self,
227        interface_id: &InterfaceId,
228    ) -> anyhow::Result<ImportedInterface<'_>> {
229        let interface = &self.resolve.interfaces[*interface_id];
230        let name = interface
231            .name
232            .as_ref()
233            .ok_or_else(|| anyhow!("Interface import does not have a name"))?
234            .as_str();
235
236        let functions = interface
237            .functions
238            .iter()
239            .map(|(name, f)| (name.as_str(), f))
240            .collect();
241
242        let package_id = interface
243            .package
244            .ok_or_else(|| anyhow!("Anonymous interface imports are not supported yet"))?;
245        let package = self
246            .resolve
247            .packages
248            .get(package_id)
249            .ok_or_else(|| anyhow!("Could not find package of imported interface {name}"))?;
250        let package_name = &package.name;
251
252        Ok(ImportedInterface {
253            package_name: Some(package_name),
254            name: name.to_string(),
255            functions,
256            interface: Some(interface),
257            interface_id: Some(*interface_id),
258        })
259    }
260
261    fn typ(&self, type_id: TypeId) -> anyhow::Result<&TypeDef> {
262        self.resolve
263            .types
264            .get(type_id)
265            .ok_or_else(|| anyhow!("Unknown type id: {type_id:?}"))
266    }
267}
268
269pub struct ImportedInterface<'a> {
270    package_name: Option<&'a PackageName>,
271    name: String,
272    functions: Vec<(&'a str, &'a Function)>,
273    interface: Option<&'a Interface>,
274    interface_id: Option<InterfaceId>,
275}
276
277impl<'a> ImportedInterface<'a> {
278    pub fn module_name(&self) -> anyhow::Result<String> {
279        let package_name = self
280            .package_name
281            .ok_or_else(|| anyhow!("imported interface has no package name"))?;
282        let interface_name = &self.name;
283
284        Ok(format!(
285            "{}_{}",
286            package_name.to_string().to_snake_case(),
287            interface_name.to_snake_case()
288        ))
289    }
290
291    pub fn rust_interface_name(&self) -> Ident {
292        let interface_name = format!("Js{}Module", self.name.to_upper_camel_case());
293        Ident::new(&interface_name, Span::call_site())
294    }
295
296    pub fn name_and_interface(&self) -> Option<(&str, &Interface)> {
297        self.interface
298            .map(|interface| (self.name.as_str(), interface))
299    }
300
301    pub fn fully_qualified_interface_name(&self) -> String {
302        if let Some(package_name) = &self.package_name {
303            package_name.interface_id(&self.name)
304        } else {
305            self.name.clone()
306        }
307    }
308
309    pub fn interface_stack(&self) -> VecDeque<InterfaceId> {
310        self.interface_id.iter().cloned().collect()
311    }
312}
313
314/// Recursively copies a WIT directory to `<output>/wit`.
315fn copy_wit_directory(wit: &Utf8Path, output: &Utf8Path) -> anyhow::Result<()> {
316    fs_extra::dir::create(output, true)
317        .context("Failed to create and erase output WIT directory")?;
318    fs_extra::dir::copy(wit, output, &CopyOptions::new().content_only(true))
319        .context("Failed to copy WIT directory")?;
320
321    Ok(())
322}
323
324/// Copies the JS module files to `<output>/src/<name>.js`.
325fn copy_js_modules(js_modules: &[JsModuleSpec], output: &Utf8Path) -> anyhow::Result<()> {
326    for module in js_modules {
327        if let EmbeddingMode::EmbedFile(source) = &module.mode {
328            let filename = module.file_name();
329            let js_dest = output.join("src").join(filename);
330            std::fs::copy(source, js_dest)
331                .context(format!("Failed to copy JavaScript module {}", module.name))?;
332        }
333    }
334    Ok(())
335}
336
337/// Checks if any of the provided JS modules uses composition mode.
338fn uses_composition(js_module_spec: &[JsModuleSpec]) -> bool {
339    js_module_spec
340        .iter()
341        .any(|m| matches!(m.mode, EmbeddingMode::Composition))
342}