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::javascript::escape_js_ident;
5use crate::skeleton::{copy_skeleton_lock, copy_skeleton_sources, generate_cargo_toml};
6use crate::wit::{add_get_script_import, add_wizer_init_export};
7use anyhow::{Context, anyhow};
8use camino::{Utf8Path, Utf8PathBuf};
9use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase};
10use proc_macro2::{Ident, Span};
11use std::cell::RefCell;
12use std::collections::{BTreeMap, BTreeSet, VecDeque};
13use wit_parser::{
14    Function, Interface, InterfaceId, PackageId, PackageName, PackageSourceMap, Resolve, TypeDef,
15    TypeDefKind, TypeId, TypeOwner, WorldId, WorldItem, WorldKey,
16};
17
18/// WASI package namespaces whose interfaces are remapped to `wasip2::` in the generated code.
19/// These correspond to interfaces provided by the `wasip2` crate and are mapped via the
20/// `with:` block in `wit_bindgen::generate!`.
21const WASI_REMAP_NAMESPACES: &[(&str, &str)] = &[
22    ("cli", "cli"),
23    ("clocks", "clocks"),
24    ("filesystem", "filesystem"),
25    ("http", "http"),
26    ("io", "io"),
27    ("random", "random"),
28    ("sockets", "sockets"),
29];
30
31/// WASI package namespaces remapped to `wasip3::` in the WASI Preview 3 generation path.
32///
33/// Only the clock interfaces are remapped for now (Phase 1); every other WASI interface
34/// present in a Preview 3 world keeps the bindings generated by `wit-bindgen` (i.e. it is
35/// *not* treated as remapped). This list must stay consistent with the `with:` remap table
36/// (`WASI_REMAPS_P3`) in `exports.rs`.
37const WASI_REMAP_NAMESPACES_P3: &[(&str, &str)] = &[("clocks", "clocks")];
38
39/// Selects which WASI generation the wrapper crate targets.
40///
41/// The default ([`GenerationTarget::WasiP2`]) reproduces the historical behavior:
42/// synchronous exports/imports backed by the full Preview 2 skeleton (`wasip2`,
43/// `wstd`, `golem-wasi-http`, Node.js builtins, Wizer pre-initialization).
44///
45/// [`GenerationTarget::WasiP3`] is the opt-in Preview 3 path. It uses the
46/// **same** skeleton crate compiled with the `p3` Cargo feature instead of `p2`: a
47/// minimal async runtime spine depending only on `rquickjs`, `wasip3` and a renamed
48/// `wit-bindgen` (so the Node.js builtins are not duplicated). It supports synchronous
49/// and asynchronous WIT exports and imports, including Wizer pre-initialization.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51pub enum GenerationTarget {
52    /// WASI Preview 2 (default).
53    #[default]
54    WasiP2,
55    /// WASI Preview 3 (opt-in, async).
56    WasiP3,
57}
58
59impl GenerationTarget {
60    pub fn is_p3(&self) -> bool {
61        matches!(self, GenerationTarget::WasiP3)
62    }
63}
64
65mod async_values;
66mod conversions;
67mod exports;
68mod imports;
69mod inject;
70mod javascript;
71#[cfg(feature = "optimize")]
72mod optimize;
73mod rust_bindgen;
74mod skeleton;
75mod types;
76mod typescript;
77mod wit;
78
79pub use inject::{SLOT_END_MAGIC, SLOT_MAGIC, create_marker_file, inject_js_into_component};
80#[cfg(feature = "optimize")]
81pub use optimize::optimize_component;
82
83/// Write `contents` to `path` only if the file doesn't exist or its current content differs.
84/// This preserves file timestamps when content hasn't changed, avoiding unnecessary recompilation.
85pub(crate) fn write_if_changed(
86    path: impl AsRef<std::path::Path>,
87    contents: impl AsRef<[u8]>,
88) -> std::io::Result<()> {
89    let path = path.as_ref();
90    let contents = contents.as_ref();
91    if let Ok(existing) = std::fs::read(path)
92        && existing == contents
93    {
94        return Ok(());
95    }
96    std::fs::write(path, contents)
97}
98
99/// Copy a file from `src` to `dst` only if the destination doesn't exist or its content differs.
100pub(crate) fn copy_if_changed(
101    src: impl AsRef<std::path::Path>,
102    dst: impl AsRef<std::path::Path>,
103) -> std::io::Result<()> {
104    let src = src.as_ref();
105    let dst = dst.as_ref();
106    let src_contents = std::fs::read(src)?;
107    if let Ok(existing) = std::fs::read(dst)
108        && existing == src_contents
109    {
110        return Ok(());
111    }
112    std::fs::write(dst, src_contents)
113}
114
115/// Specifies how a given user-defined JS module gets embedded into the generated Rust crate.
116#[derive(Debug, Clone)]
117pub enum EmbeddingMode {
118    /// Points to a JS module file that is going to be embedded into the generated Rust crate
119    EmbedFile(Utf8PathBuf),
120    /// The JS module is going to be fetched run-time through an imported WIT interface
121    Composition,
122    /// Embeds a small marker in the compiled WASM component.
123    /// After compilation, JS source can be injected into the marker via `inject_js_into_component`
124    /// without recompiling the Rust crate. The injected JS can be any size — the WASM component
125    /// is structurally rewritten to accommodate the new data.
126    BinarySlot,
127}
128
129impl EmbeddingMode {
130    pub fn is_binary_slot(&self) -> bool {
131        matches!(self, EmbeddingMode::BinarySlot)
132    }
133}
134
135/// Specifies a JS module to be evaluated in the generated component.
136#[derive(Debug, Clone)]
137pub struct JsModuleSpec {
138    pub name: String,
139    pub mode: EmbeddingMode,
140}
141
142impl JsModuleSpec {
143    pub fn file_name(&self) -> String {
144        self.name.replace('/', "_") + ".js"
145    }
146}
147
148/// Generates a Rust wrapper crate for a combination of a WIT package and a JavaScript module.
149///
150/// The `wit` parameter should point to a WIT root (holding the WIT package of the component, with
151/// optionally a `deps` subdirectory with an arbitrary number of dependencies).
152///
153/// The `js_modules` parameter must point to at least one JavaScript module that implements the WIT package,
154/// and optionally additional modules that get imported during the initialization of the component. It is
155/// always the first in the list that is considered the one containing the implementation of the WIT exports.
156///
157/// The `output` parameter is the root directory where the generated Rust crate's source code and
158/// Cargo manifest is placed.
159///
160/// If `world` is `None`, the default world is selected and used, otherwise the specified one.
161///
162/// This always targets WASI Preview 2. Use [`generate_wrapper_crate_with_target`] to select the
163/// (opt-in) WASI Preview 3 generation path.
164pub fn generate_wrapper_crate(
165    wit: &Utf8Path,
166    js_modules: &[JsModuleSpec],
167    output: &Utf8Path,
168    world: Option<&str>,
169) -> anyhow::Result<()> {
170    generate_wrapper_crate_with_target(wit, js_modules, output, world, GenerationTarget::WasiP2)
171}
172
173/// Generates a Rust wrapper crate, selecting the WASI generation via `target`.
174///
175/// See [`generate_wrapper_crate`] for the description of the common parameters and
176/// [`GenerationTarget`] for the differences between the Preview 2 and Preview 3 paths.
177pub fn generate_wrapper_crate_with_target(
178    wit: &Utf8Path,
179    js_modules: &[JsModuleSpec],
180    output: &Utf8Path,
181    world: Option<&str>,
182    target: GenerationTarget,
183) -> anyhow::Result<()> {
184    if target.is_p3() && uses_composition(js_modules) {
185        anyhow::bail!(
186            "Composition (@composition) JS modules are not supported by the WASI Preview 3 generation path yet"
187        );
188    }
189
190    // Making sure the target directories exists
191    std::fs::create_dir_all(output).context("Failed to create output directory")?;
192    std::fs::create_dir_all(output.join("src")).context("Failed to create output/src directory")?;
193    std::fs::create_dir_all(output.join("src").join("modules"))
194        .context("Failed to create output/src/modules directory")?;
195
196    // Resolving the WIT package (initial parse for Cargo.toml generation)
197    let context = GeneratorContext::new(output, wit, world, target)?;
198
199    // Generating the Cargo.toml file
200    generate_cargo_toml(&context)?;
201
202    // Copying the skeleton's Cargo.lock for faster dependency resolution
203    copy_skeleton_lock(context.output, &context.world_name)
204        .context("Failed to copy skeleton Cargo.lock")?;
205
206    // Copying the skeleton files
207    copy_skeleton_sources(context.output).context("Failed to copy skeleton sources")?;
208
209    // Copying the WIT package to the output directory
210    copy_wit_directory(wit, &context.output.join("wit"))
211        .context("Failed to copy WIT package to output directory")?;
212
213    if uses_composition(js_modules) {
214        add_get_script_import(&context.output.join("wit"), world)
215            .context("Failed to add get-script import to the WIT world")?;
216    }
217
218    // Add wizer-initialize export for pre-initialization support.
219    add_wizer_init_export(&context.output.join("wit"), world, target.is_p3())
220        .context("Failed to add wizer-initialize export to the WIT world")?;
221
222    // Re-resolve the WIT package after modifications (wizer-initialize export was added)
223    let modified_wit = output.join("wit");
224    let context = GeneratorContext::new(output, &modified_wit, world, target)?;
225
226    // Copying the JavaScript module to the output directory
227    copy_js_modules(js_modules, context.output)
228        .context("Failed to copy JavaScript module to output directory")?;
229
230    // Generating the lib.rs file implementing the component exports
231    generate_export_impls(&context, js_modules)
232        .context("Failed to generate the component export implementations")?;
233
234    // Generating the native modules implementing the component imports
235    generate_import_modules(&context).context("Failed to generate the component import modules")?;
236
237    // Generating the conversions.rs file implementing the IntoJs and FromJs typeclass instances
238    // This step must be done after `generate_export_impls` to ensure all visited types are registered.
239    generate_conversions(&context)
240        .context("Failed to generate the IntoJs and FromJs typeclass instances")?;
241
242    Ok(())
243}
244
245/// Generates TypeScript module definitions for a given (or default) world of a WIT package.
246///
247/// Returns the list of generated files.
248pub fn generate_dts(
249    wit: &Utf8Path,
250    output: &Utf8Path,
251    world: Option<&str>,
252) -> anyhow::Result<Vec<Utf8PathBuf>> {
253    generate_dts_with_target(wit, output, world, GenerationTarget::WasiP2)
254}
255
256/// Generates TypeScript module definitions, selecting the WASI generation via `target`.
257///
258/// Preview 2 permits a Promise-returning JavaScript implementation for every export. Preview 3
259/// follows the WIT function kind: plain functions are synchronous and `async func` functions
260/// return Promises.
261pub fn generate_dts_with_target(
262    wit: &Utf8Path,
263    output: &Utf8Path,
264    world: Option<&str>,
265    target: GenerationTarget,
266) -> anyhow::Result<Vec<Utf8PathBuf>> {
267    // Making sure the target directories exist
268    std::fs::create_dir_all(output).context("Failed to create output directory")?;
269
270    let context = GeneratorContext::new(output, wit, world, target)?;
271
272    let mut result = Vec::new();
273    result.extend(
274        typescript::generate_export_module(&context)
275            .context("Failed to generate the TypeScript module definition for the exports")?,
276    );
277
278    // Generating the native modules implementing the component imports
279    result.extend(typescript::generate_import_modules(&context).context(
280        "Failed to generate the TypeScript module definitions for the imported modules",
281    )?);
282
283    Ok(result)
284}
285
286struct GeneratorContext<'a> {
287    output: &'a Utf8Path,
288    #[allow(dead_code)]
289    wit_source_path: &'a Utf8Path,
290    resolve: Resolve,
291    root_package: PackageId,
292    world: WorldId,
293    #[allow(dead_code)]
294    source_map: PackageSourceMap,
295    visited_types: RefCell<BTreeSet<TypeId>>,
296    world_name: String,
297    types: wit_bindgen_core::Types,
298    target: GenerationTarget,
299}
300
301impl<'a> GeneratorContext<'a> {
302    fn new(
303        output: &'a Utf8Path,
304        wit: &'a Utf8Path,
305        world: Option<&str>,
306        target: GenerationTarget,
307    ) -> anyhow::Result<Self> {
308        let mut resolve = Resolve::default();
309        let (root_package, source_map) = resolve
310            .push_path(wit)
311            .context("Failed to resolve WIT package")?;
312        let world = resolve
313            .select_world(std::slice::from_ref(&root_package), world)
314            .context("Failed to select WIT world")?;
315
316        if target.is_p3() {
317            let mut unsupported = Vec::new();
318            for (key, item) in &resolve.worlds[world].imports {
319                let is_unsupported = match item {
320                    WorldItem::Function(_) => true,
321                    WorldItem::Type { id, .. } => resolve.types[*id].kind == TypeDefKind::Resource,
322                    WorldItem::Interface { .. } => false,
323                };
324                if is_unsupported {
325                    unsupported.push(match key {
326                        WorldKey::Name(name) => name.clone(),
327                        WorldKey::Interface(_) => "<resource>".to_string(),
328                    });
329                }
330            }
331            if !unsupported.is_empty() {
332                return Err(anyhow!(
333                    "Functions or resources declared directly in the world are not supported by \
334                     the WASI Preview 3 generation path ({}); declare them inside an imported \
335                     interface instead",
336                    unsupported.join(", ")
337                ));
338            }
339        }
340
341        let world_name = resolve.worlds[world].name.clone();
342
343        let mut types = wit_bindgen_core::Types::default();
344        types.analyze(&resolve);
345
346        Ok(Self {
347            output,
348            wit_source_path: wit,
349            resolve,
350            root_package,
351            world,
352            source_map,
353            visited_types: RefCell::new(BTreeSet::new()),
354            world_name,
355            types,
356            target,
357        })
358    }
359
360    fn root_package_name(&self) -> String {
361        self.resolve.packages[self.root_package].name.to_string()
362    }
363
364    fn record_visited_type(&self, type_id: TypeId) {
365        self.visited_types.borrow_mut().insert(type_id);
366    }
367
368    fn is_exported_interface(&self, interface_id: InterfaceId) -> bool {
369        let world = &self.resolve.worlds[self.world];
370        world
371            .exports
372            .iter()
373            .any(|(_, item)| matches!(item, WorldItem::Interface { id, .. } if id == &interface_id))
374    }
375
376    fn exported_interface_js_name(
377        &self,
378        interface_id: InterfaceId,
379        export_name: &str,
380    ) -> anyhow::Result<String> {
381        let names = self.exported_interface_js_names()?;
382        names
383            .get(&interface_id)
384            .cloned()
385            .ok_or_else(|| anyhow!("Interface export not found: {export_name}"))
386    }
387
388    fn exported_interface_js_names(&self) -> anyhow::Result<BTreeMap<InterfaceId, String>> {
389        let world = &self.resolve.worlds[self.world];
390        let mut exported_interfaces = Vec::new();
391
392        for (key, export) in &world.exports {
393            if let WorldItem::Interface { id, .. } = export {
394                let interface = &self.resolve.interfaces[*id];
395                let export_name = match key {
396                    WorldKey::Name(name) => name.as_str(),
397                    WorldKey::Interface(_) => interface
398                        .name
399                        .as_deref()
400                        .ok_or_else(|| anyhow!("Interface export does not have a name"))?,
401                };
402                let short_name = exported_interface_short_js_name(export_name);
403                exported_interfaces.push((*id, export_name.to_string(), short_name));
404            }
405        }
406
407        let mut short_name_counts = BTreeMap::<String, usize>::new();
408        for (_, _, short_name) in &exported_interfaces {
409            *short_name_counts.entry(short_name.clone()).or_default() += 1;
410        }
411
412        let mut result = BTreeMap::new();
413        let mut used_names = BTreeMap::<String, InterfaceId>::new();
414        for (interface_id, export_name, short_name) in exported_interfaces {
415            let js_name = if short_name_counts.get(&short_name).copied().unwrap_or(0) > 1 {
416                let interface = &self.resolve.interfaces[interface_id];
417                exported_interface_qualified_js_name(self, interface, &export_name)?
418            } else {
419                short_name
420            };
421
422            if let Some(previous_id) = used_names.insert(js_name.clone(), interface_id) {
423                anyhow::bail!(
424                    "Exported WIT interfaces {previous_id:?} and {interface_id:?} both map to JavaScript export name '{js_name}'"
425                );
426            }
427
428            result.insert(interface_id, js_name);
429        }
430
431        Ok(result)
432    }
433
434    fn is_exported_type(&self, type_id: TypeId) -> bool {
435        if let Some(typ) = self.resolve.types.get(type_id) {
436            match &typ.owner {
437                TypeOwner::World(world_id) => {
438                    if world_id == &self.world {
439                        let world = &self.resolve.worlds[self.world];
440                        world
441                            .exports
442                            .iter()
443                            .any(|(_, item)| matches!(item, WorldItem::Type { id, .. } if id == &type_id))
444                    } else {
445                        false
446                    }
447                }
448                TypeOwner::Interface(interface_id) => self.is_exported_interface(*interface_id),
449                TypeOwner::None => false,
450            }
451        } else {
452            false
453        }
454    }
455
456    fn bindgen_type_info(&self, type_id: TypeId) -> wit_bindgen_core::TypeInfo {
457        self.types.get(type_id)
458    }
459
460    fn get_imported_interface(
461        &self,
462        interface_id: &InterfaceId,
463    ) -> anyhow::Result<ImportedInterface<'_>> {
464        let interface = &self.resolve.interfaces[*interface_id];
465        let name = interface
466            .name
467            .as_ref()
468            .ok_or_else(|| anyhow!("Interface import does not have a name"))?
469            .as_str();
470
471        let functions = interface
472            .functions
473            .iter()
474            .map(|(name, f)| (name.as_str(), f))
475            .collect();
476
477        let package_id = interface
478            .package
479            .ok_or_else(|| anyhow!("Anonymous interface imports are not supported yet"))?;
480        let package = self
481            .resolve
482            .packages
483            .get(package_id)
484            .ok_or_else(|| anyhow!("Could not find package of imported interface {name}"))?;
485        let package_name = &package.name;
486
487        Ok(ImportedInterface {
488            package_name: Some(package_name),
489            name: name.to_string(),
490            functions,
491            interface: Some(interface),
492            interface_id: Some(*interface_id),
493        })
494    }
495
496    fn typ(&self, type_id: TypeId) -> anyhow::Result<&TypeDef> {
497        self.resolve
498            .types
499            .get(type_id)
500            .ok_or_else(|| anyhow!("Unknown type id: {type_id:?}"))
501    }
502
503    /// Returns `true` if the given package is a WASI package whose interfaces are remapped
504    /// to `wasip2::` via the `with:` block in `wit_bindgen::generate!`.
505    fn is_wasi_remapped_package(&self, package_id: PackageId) -> bool {
506        let package = &self.resolve.packages[package_id];
507        if package.name.namespace != "wasi" {
508            return false;
509        }
510        if !self
511            .wasi_remap_namespaces()
512            .iter()
513            .any(|(pkg_name, _)| *pkg_name == package.name.name.as_str())
514        {
515            return false;
516        }
517        // The remap only applies when the imported package's version matches the WASI
518        // generation the runtime crate provides. The `wasip3` crate implements the
519        // `wasi:*@0.3.x` interfaces, so a Preview 2 versioned import (e.g.
520        // `wasi:clocks/monotonic-clock@0.2.3`) in a Preview 3 world must NOT be remapped —
521        // its API surface differs and the bindings for it are generated by `wit-bindgen`
522        // instead (via `generate_all`).
523        if self.target.is_p3() {
524            matches!(&package.name.version, Some(v) if v.major == 0 && v.minor == 3)
525        } else {
526            true
527        }
528    }
529
530    /// The WASI namespace remap table for the selected generation target.
531    fn wasi_remap_namespaces(&self) -> &'static [(&'static str, &'static str)] {
532        if self.target.is_p3() {
533            WASI_REMAP_NAMESPACES_P3
534        } else {
535            WASI_REMAP_NAMESPACES
536        }
537    }
538
539    /// The Rust crate that remapped WASI interfaces resolve to for the selected target
540    /// (`wasip2` for Preview 2, `wasip3` for Preview 3).
541    fn wasi_remap_crate_ident(&self) -> Ident {
542        let name = if self.target.is_p3() {
543            "wasip3"
544        } else {
545            "wasip2"
546        };
547        Ident::new(name, Span::call_site())
548    }
549
550    /// Path to the `wit-bindgen` runtime module for the selected target. The Preview 3 path
551    /// uses a renamed `wit-bindgen` dependency (`wit-bindgen-p3`) so that the two major
552    /// versions can coexist in one skeleton crate.
553    fn wit_bindgen_rt_path(&self) -> proc_macro2::TokenStream {
554        if self.target.is_p3() {
555            quote::quote! { wit_bindgen_p3::rt }
556        } else {
557            quote::quote! { wit_bindgen_rt }
558        }
559    }
560
561    /// For a WASI-remapped resource type, returns the import module path and resource class
562    /// name (e.g., `crate::modules::wasi_io_0_2_3_poll::Pollable`).
563    fn wasi_resource_module_path(
564        &self,
565        type_id: TypeId,
566    ) -> Option<(proc_macro2::TokenStream, Ident)> {
567        let typ = self.resolve.types.get(type_id)?;
568        let resource_name = typ.name.as_ref()?;
569        let resource_ident = Ident::new(&resource_name.to_upper_camel_case(), Span::call_site());
570
571        let interface_id = match &typ.owner {
572            TypeOwner::Interface(id) => *id,
573            _ => return None,
574        };
575        let interface = self.resolve.interfaces.get(interface_id)?;
576        let interface_name = interface.name.as_ref()?;
577        let package_id = interface.package?;
578        let package = self.resolve.packages.get(package_id)?;
579        let package_name = &package.name;
580
581        let module_name = format!(
582            "{}_{}",
583            package_name.to_string().to_snake_case(),
584            interface_name.to_snake_case()
585        );
586        let module_ident = Ident::new(&module_name, Span::call_site());
587
588        Some((
589            quote::quote! { crate::modules::#module_ident },
590            resource_ident,
591        ))
592    }
593
594    /// Returns `true` if the given type belongs to a WASI-remapped interface.
595    fn is_wasi_remapped_type(&self, type_id: TypeId) -> bool {
596        if let Some(typ) = self.resolve.types.get(type_id) {
597            match &typ.owner {
598                TypeOwner::Interface(interface_id) => {
599                    if let Some(interface) = self.resolve.interfaces.get(*interface_id)
600                        && let Some(package_id) = interface.package
601                    {
602                        return self.is_wasi_remapped_package(package_id);
603                    }
604                    false
605                }
606                _ => false,
607            }
608        } else {
609            false
610        }
611    }
612}
613
614fn exported_interface_short_js_name(export_name: &str) -> String {
615    escape_js_ident(export_name.to_lower_camel_case())
616}
617
618fn exported_interface_qualified_js_name(
619    context: &GeneratorContext<'_>,
620    interface: &Interface,
621    export_name: &str,
622) -> anyhow::Result<String> {
623    let package_id = interface
624        .package
625        .ok_or_else(|| anyhow!("Anonymous interface exports cannot be qualified: {export_name}"))?;
626    let package = context
627        .resolve
628        .packages
629        .get(package_id)
630        .ok_or_else(|| anyhow!("Unknown owner package of interface export: {export_name}"))?;
631    let interface_name = interface.name.as_deref().unwrap_or(export_name);
632    let module_name = format!(
633        "{}_{}",
634        package.name.to_string().to_snake_case(),
635        interface_name.to_snake_case()
636    );
637
638    Ok(escape_js_ident(module_name.to_lower_camel_case()))
639}
640
641pub struct ImportedInterface<'a> {
642    package_name: Option<&'a PackageName>,
643    name: String,
644    functions: Vec<(&'a str, &'a Function)>,
645    interface: Option<&'a Interface>,
646    interface_id: Option<InterfaceId>,
647}
648
649impl<'a> ImportedInterface<'a> {
650    pub fn module_name(&self) -> anyhow::Result<String> {
651        let package_name = self
652            .package_name
653            .ok_or_else(|| anyhow!("imported interface has no package name"))?;
654        let interface_name = &self.name;
655
656        Ok(format!(
657            "{}_{}",
658            package_name.to_string().to_snake_case(),
659            interface_name.to_snake_case()
660        ))
661    }
662
663    pub fn rust_interface_name(&self) -> Ident {
664        let interface_name = format!("Js{}Module", self.name.to_upper_camel_case());
665        Ident::new(&interface_name, Span::call_site())
666    }
667
668    pub fn name_and_interface(&self) -> Option<(&str, &Interface)> {
669        self.interface
670            .map(|interface| (self.name.as_str(), interface))
671    }
672
673    pub fn fully_qualified_interface_name(&self) -> String {
674        if let Some(package_name) = &self.package_name {
675            package_name.interface_id(&self.name)
676        } else {
677            self.name.clone()
678        }
679    }
680
681    pub fn interface_stack(&self) -> VecDeque<InterfaceId> {
682        self.interface_id.iter().cloned().collect()
683    }
684}
685
686/// Recursively copies a WIT directory to `<output>/wit`.
687fn copy_wit_directory(wit: &Utf8Path, output: &Utf8Path) -> anyhow::Result<()> {
688    std::fs::create_dir_all(output)?;
689    copy_dir_if_changed(wit.as_std_path(), output.as_std_path())
690        .context("Failed to copy WIT directory")?;
691    Ok(())
692}
693
694fn copy_dir_if_changed(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
695    std::fs::create_dir_all(dst)?;
696    for entry in std::fs::read_dir(src)? {
697        let entry = entry?;
698        let src_path = entry.path();
699        let dst_path = dst.join(entry.file_name());
700        if src_path.is_dir() {
701            copy_dir_if_changed(&src_path, &dst_path)?;
702        } else {
703            copy_if_changed(&src_path, &dst_path)?;
704        }
705    }
706    Ok(())
707}
708
709/// Copies the JS module files to `<output>/src/<name>.js` or generates slot files.
710fn copy_js_modules(js_modules: &[JsModuleSpec], output: &Utf8Path) -> anyhow::Result<()> {
711    let mut slot_index: u32 = 0;
712    for module in js_modules {
713        match &module.mode {
714            EmbeddingMode::EmbedFile(source) => {
715                let filename = module.file_name();
716                let js_dest = output.join("src").join(filename);
717                copy_if_changed(source, js_dest)
718                    .context(format!("Failed to copy JavaScript module {}", module.name))?;
719            }
720            EmbeddingMode::BinarySlot => {
721                let slot_filename = module.name.replace('/', "_") + ".slot";
722                let slot_dest = output.join("src").join(slot_filename);
723                let slot_data = inject::create_marker_file(slot_index);
724                write_if_changed(slot_dest, slot_data).context(format!(
725                    "Failed to create marker file for module {}",
726                    module.name
727                ))?;
728                slot_index += 1;
729            }
730            EmbeddingMode::Composition => {}
731        }
732    }
733    Ok(())
734}
735
736/// Checks if any of the provided JS modules uses composition mode.
737fn uses_composition(js_module_spec: &[JsModuleSpec]) -> bool {
738    js_module_spec
739        .iter()
740        .any(|m| matches!(m.mode, EmbeddingMode::Composition))
741}