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