Skip to main content

wasm_rquickjs/
lib.rs

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