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("Failed to copy skeleton Cargo.lock")?;
204
205    // Copying the skeleton files
206    copy_skeleton_sources(context.output).context("Failed to copy skeleton sources")?;
207
208    // Copying the WIT package to the output directory
209    copy_wit_directory(wit, &context.output.join("wit"))
210        .context("Failed to copy WIT package to output directory")?;
211
212    if uses_composition(js_modules) {
213        add_get_script_import(&context.output.join("wit"), world)
214            .context("Failed to add get-script import to the WIT world")?;
215    }
216
217    // Add wizer-initialize export for pre-initialization support.
218    add_wizer_init_export(&context.output.join("wit"), world, target.is_p3())
219        .context("Failed to add wizer-initialize export to the WIT world")?;
220
221    // Re-resolve the WIT package after modifications (wizer-initialize export was added)
222    let modified_wit = output.join("wit");
223    let context = GeneratorContext::new(output, &modified_wit, world, target)?;
224
225    // Copying the JavaScript module to the output directory
226    copy_js_modules(js_modules, context.output)
227        .context("Failed to copy JavaScript module to output directory")?;
228
229    // Generating the lib.rs file implementing the component exports
230    generate_export_impls(&context, js_modules)
231        .context("Failed to generate the component export implementations")?;
232
233    // Generating the native modules implementing the component imports
234    generate_import_modules(&context).context("Failed to generate the component import modules")?;
235
236    // Generating the conversions.rs file implementing the IntoJs and FromJs typeclass instances
237    // This step must be done after `generate_export_impls` to ensure all visited types are registered.
238    generate_conversions(&context)
239        .context("Failed to generate the IntoJs and FromJs typeclass instances")?;
240
241    Ok(())
242}
243
244/// Generates TypeScript module definitions for a given (or default) world of a WIT package.
245///
246/// Returns the list of generated files.
247pub fn generate_dts(
248    wit: &Utf8Path,
249    output: &Utf8Path,
250    world: Option<&str>,
251) -> anyhow::Result<Vec<Utf8PathBuf>> {
252    generate_dts_with_target(wit, output, world, GenerationTarget::WasiP2)
253}
254
255/// Generates TypeScript module definitions, selecting the WASI generation via `target`.
256///
257/// Preview 2 permits a Promise-returning JavaScript implementation for every export. Preview 3
258/// follows the WIT function kind: plain functions are synchronous and `async func` functions
259/// return Promises.
260pub fn generate_dts_with_target(
261    wit: &Utf8Path,
262    output: &Utf8Path,
263    world: Option<&str>,
264    target: GenerationTarget,
265) -> anyhow::Result<Vec<Utf8PathBuf>> {
266    // Making sure the target directories exist
267    std::fs::create_dir_all(output).context("Failed to create output directory")?;
268
269    let context = GeneratorContext::new(output, wit, world, target)?;
270
271    let mut result = Vec::new();
272    result.extend(
273        typescript::generate_export_module(&context)
274            .context("Failed to generate the TypeScript module definition for the exports")?,
275    );
276
277    // Generating the native modules implementing the component imports
278    result.extend(typescript::generate_import_modules(&context).context(
279        "Failed to generate the TypeScript module definitions for the imported modules",
280    )?);
281
282    Ok(result)
283}
284
285struct GeneratorContext<'a> {
286    output: &'a Utf8Path,
287    #[allow(dead_code)]
288    wit_source_path: &'a Utf8Path,
289    resolve: Resolve,
290    root_package: PackageId,
291    world: WorldId,
292    #[allow(dead_code)]
293    source_map: PackageSourceMap,
294    visited_types: RefCell<BTreeSet<TypeId>>,
295    world_name: String,
296    types: wit_bindgen_core::Types,
297    target: GenerationTarget,
298}
299
300impl<'a> GeneratorContext<'a> {
301    fn new(
302        output: &'a Utf8Path,
303        wit: &'a Utf8Path,
304        world: Option<&str>,
305        target: GenerationTarget,
306    ) -> anyhow::Result<Self> {
307        let mut resolve = Resolve::default();
308        let (root_package, source_map) = resolve
309            .push_path(wit)
310            .context("Failed to resolve WIT package")?;
311        let world = resolve
312            .select_world(std::slice::from_ref(&root_package), world)
313            .context("Failed to select WIT world")?;
314
315        if target.is_p3() {
316            let mut unsupported = Vec::new();
317            for (key, item) in &resolve.worlds[world].imports {
318                let is_unsupported = match item {
319                    WorldItem::Function(_) => true,
320                    WorldItem::Type { id, .. } => resolve.types[*id].kind == TypeDefKind::Resource,
321                    WorldItem::Interface { .. } => false,
322                };
323                if is_unsupported {
324                    unsupported.push(match key {
325                        WorldKey::Name(name) => name.clone(),
326                        WorldKey::Interface(_) => "<resource>".to_string(),
327                    });
328                }
329            }
330            if !unsupported.is_empty() {
331                return Err(anyhow!(
332                    "Functions or resources declared directly in the world are not supported by \
333                     the WASI Preview 3 generation path ({}); declare them inside an imported \
334                     interface instead",
335                    unsupported.join(", ")
336                ));
337            }
338        }
339
340        let world_name = resolve.worlds[world].name.clone();
341
342        let mut types = wit_bindgen_core::Types::default();
343        types.analyze(&resolve);
344
345        Ok(Self {
346            output,
347            wit_source_path: wit,
348            resolve,
349            root_package,
350            world,
351            source_map,
352            visited_types: RefCell::new(BTreeSet::new()),
353            world_name,
354            types,
355            target,
356        })
357    }
358
359    fn root_package_name(&self) -> String {
360        self.resolve.packages[self.root_package].name.to_string()
361    }
362
363    fn record_visited_type(&self, type_id: TypeId) {
364        self.visited_types.borrow_mut().insert(type_id);
365    }
366
367    fn is_exported_interface(&self, interface_id: InterfaceId) -> bool {
368        let world = &self.resolve.worlds[self.world];
369        world
370            .exports
371            .iter()
372            .any(|(_, item)| matches!(item, WorldItem::Interface { id, .. } if id == &interface_id))
373    }
374
375    fn exported_interface_js_name(
376        &self,
377        interface_id: InterfaceId,
378        export_name: &str,
379    ) -> anyhow::Result<String> {
380        let names = self.exported_interface_js_names()?;
381        names
382            .get(&interface_id)
383            .cloned()
384            .ok_or_else(|| anyhow!("Interface export not found: {export_name}"))
385    }
386
387    fn exported_interface_js_names(&self) -> anyhow::Result<BTreeMap<InterfaceId, String>> {
388        let world = &self.resolve.worlds[self.world];
389        let mut exported_interfaces = Vec::new();
390
391        for (key, export) in &world.exports {
392            if let WorldItem::Interface { id, .. } = export {
393                let interface = &self.resolve.interfaces[*id];
394                let export_name = match key {
395                    WorldKey::Name(name) => name.as_str(),
396                    WorldKey::Interface(_) => interface
397                        .name
398                        .as_deref()
399                        .ok_or_else(|| anyhow!("Interface export does not have a name"))?,
400                };
401                let short_name = exported_interface_short_js_name(export_name);
402                exported_interfaces.push((*id, export_name.to_string(), short_name));
403            }
404        }
405
406        let mut short_name_counts = BTreeMap::<String, usize>::new();
407        for (_, _, short_name) in &exported_interfaces {
408            *short_name_counts.entry(short_name.clone()).or_default() += 1;
409        }
410
411        let mut result = BTreeMap::new();
412        let mut used_names = BTreeMap::<String, InterfaceId>::new();
413        for (interface_id, export_name, short_name) in exported_interfaces {
414            let js_name = if short_name_counts.get(&short_name).copied().unwrap_or(0) > 1 {
415                let interface = &self.resolve.interfaces[interface_id];
416                exported_interface_qualified_js_name(self, interface, &export_name)?
417            } else {
418                short_name
419            };
420
421            if let Some(previous_id) = used_names.insert(js_name.clone(), interface_id) {
422                anyhow::bail!(
423                    "Exported WIT interfaces {previous_id:?} and {interface_id:?} both map to JavaScript export name '{js_name}'"
424                );
425            }
426
427            result.insert(interface_id, js_name);
428        }
429
430        Ok(result)
431    }
432
433    fn is_exported_type(&self, type_id: TypeId) -> bool {
434        if let Some(typ) = self.resolve.types.get(type_id) {
435            match &typ.owner {
436                TypeOwner::World(world_id) => {
437                    if world_id == &self.world {
438                        let world = &self.resolve.worlds[self.world];
439                        world
440                            .exports
441                            .iter()
442                            .any(|(_, item)| matches!(item, WorldItem::Type { id, .. } if id == &type_id))
443                    } else {
444                        false
445                    }
446                }
447                TypeOwner::Interface(interface_id) => self.is_exported_interface(*interface_id),
448                TypeOwner::None => false,
449            }
450        } else {
451            false
452        }
453    }
454
455    fn bindgen_type_info(&self, type_id: TypeId) -> wit_bindgen_core::TypeInfo {
456        self.types.get(type_id)
457    }
458
459    fn get_imported_interface(
460        &self,
461        interface_id: &InterfaceId,
462    ) -> anyhow::Result<ImportedInterface<'_>> {
463        let interface = &self.resolve.interfaces[*interface_id];
464        let name = interface
465            .name
466            .as_ref()
467            .ok_or_else(|| anyhow!("Interface import does not have a name"))?
468            .as_str();
469
470        let functions = interface
471            .functions
472            .iter()
473            .map(|(name, f)| (name.as_str(), f))
474            .collect();
475
476        let package_id = interface
477            .package
478            .ok_or_else(|| anyhow!("Anonymous interface imports are not supported yet"))?;
479        let package = self
480            .resolve
481            .packages
482            .get(package_id)
483            .ok_or_else(|| anyhow!("Could not find package of imported interface {name}"))?;
484        let package_name = &package.name;
485
486        Ok(ImportedInterface {
487            package_name: Some(package_name),
488            name: name.to_string(),
489            functions,
490            interface: Some(interface),
491            interface_id: Some(*interface_id),
492        })
493    }
494
495    fn typ(&self, type_id: TypeId) -> anyhow::Result<&TypeDef> {
496        self.resolve
497            .types
498            .get(type_id)
499            .ok_or_else(|| anyhow!("Unknown type id: {type_id:?}"))
500    }
501
502    /// Returns `true` if the given package is a WASI package whose interfaces are remapped
503    /// to `wasip2::` via the `with:` block in `wit_bindgen::generate!`.
504    fn is_wasi_remapped_package(&self, package_id: PackageId) -> bool {
505        let package = &self.resolve.packages[package_id];
506        if package.name.namespace != "wasi" {
507            return false;
508        }
509        if !self
510            .wasi_remap_namespaces()
511            .iter()
512            .any(|(pkg_name, _)| *pkg_name == package.name.name.as_str())
513        {
514            return false;
515        }
516        // The remap only applies when the imported package's version matches the WASI
517        // generation the runtime crate provides. The `wasip3` crate implements the
518        // `wasi:*@0.3.x` interfaces, so a Preview 2 versioned import (e.g.
519        // `wasi:clocks/monotonic-clock@0.2.3`) in a Preview 3 world must NOT be remapped —
520        // its API surface differs and the bindings for it are generated by `wit-bindgen`
521        // instead (via `generate_all`).
522        if self.target.is_p3() {
523            matches!(&package.name.version, Some(v) if v.major == 0 && v.minor == 3)
524        } else {
525            true
526        }
527    }
528
529    /// The WASI namespace remap table for the selected generation target.
530    fn wasi_remap_namespaces(&self) -> &'static [(&'static str, &'static str)] {
531        if self.target.is_p3() {
532            WASI_REMAP_NAMESPACES_P3
533        } else {
534            WASI_REMAP_NAMESPACES
535        }
536    }
537
538    /// The Rust crate that remapped WASI interfaces resolve to for the selected target
539    /// (`wasip2` for Preview 2, `wasip3` for Preview 3).
540    fn wasi_remap_crate_ident(&self) -> Ident {
541        let name = if self.target.is_p3() {
542            "wasip3"
543        } else {
544            "wasip2"
545        };
546        Ident::new(name, Span::call_site())
547    }
548
549    /// Path to the `wit-bindgen` runtime module for the selected target. The Preview 3 path
550    /// uses a renamed `wit-bindgen` dependency (`wit-bindgen-p3`) so that the two major
551    /// versions can coexist in one skeleton crate.
552    fn wit_bindgen_rt_path(&self) -> proc_macro2::TokenStream {
553        if self.target.is_p3() {
554            quote::quote! { wit_bindgen_p3::rt }
555        } else {
556            quote::quote! { wit_bindgen_rt }
557        }
558    }
559
560    /// For a WASI-remapped resource type, returns the import module path and resource class
561    /// name (e.g., `crate::modules::wasi_io_0_2_3_poll::Pollable`).
562    fn wasi_resource_module_path(
563        &self,
564        type_id: TypeId,
565    ) -> Option<(proc_macro2::TokenStream, Ident)> {
566        let typ = self.resolve.types.get(type_id)?;
567        let resource_name = typ.name.as_ref()?;
568        let resource_ident = Ident::new(&resource_name.to_upper_camel_case(), Span::call_site());
569
570        let interface_id = match &typ.owner {
571            TypeOwner::Interface(id) => *id,
572            _ => return None,
573        };
574        let interface = self.resolve.interfaces.get(interface_id)?;
575        let interface_name = interface.name.as_ref()?;
576        let package_id = interface.package?;
577        let package = self.resolve.packages.get(package_id)?;
578        let package_name = &package.name;
579
580        let module_name = format!(
581            "{}_{}",
582            package_name.to_string().to_snake_case(),
583            interface_name.to_snake_case()
584        );
585        let module_ident = Ident::new(&module_name, Span::call_site());
586
587        Some((
588            quote::quote! { crate::modules::#module_ident },
589            resource_ident,
590        ))
591    }
592
593    /// Returns `true` if the given type belongs to a WASI-remapped interface.
594    fn is_wasi_remapped_type(&self, type_id: TypeId) -> bool {
595        if let Some(typ) = self.resolve.types.get(type_id) {
596            match &typ.owner {
597                TypeOwner::Interface(interface_id) => {
598                    if let Some(interface) = self.resolve.interfaces.get(*interface_id)
599                        && let Some(package_id) = interface.package
600                    {
601                        return self.is_wasi_remapped_package(package_id);
602                    }
603                    false
604                }
605                _ => false,
606            }
607        } else {
608            false
609        }
610    }
611}
612
613fn exported_interface_short_js_name(export_name: &str) -> String {
614    escape_js_ident(export_name.to_lower_camel_case())
615}
616
617fn exported_interface_qualified_js_name(
618    context: &GeneratorContext<'_>,
619    interface: &Interface,
620    export_name: &str,
621) -> anyhow::Result<String> {
622    let package_id = interface
623        .package
624        .ok_or_else(|| anyhow!("Anonymous interface exports cannot be qualified: {export_name}"))?;
625    let package = context
626        .resolve
627        .packages
628        .get(package_id)
629        .ok_or_else(|| anyhow!("Unknown owner package of interface export: {export_name}"))?;
630    let interface_name = interface.name.as_deref().unwrap_or(export_name);
631    let module_name = format!(
632        "{}_{}",
633        package.name.to_string().to_snake_case(),
634        interface_name.to_snake_case()
635    );
636
637    Ok(escape_js_ident(module_name.to_lower_camel_case()))
638}
639
640pub struct ImportedInterface<'a> {
641    package_name: Option<&'a PackageName>,
642    name: String,
643    functions: Vec<(&'a str, &'a Function)>,
644    interface: Option<&'a Interface>,
645    interface_id: Option<InterfaceId>,
646}
647
648impl<'a> ImportedInterface<'a> {
649    pub fn module_name(&self) -> anyhow::Result<String> {
650        let package_name = self
651            .package_name
652            .ok_or_else(|| anyhow!("imported interface has no package name"))?;
653        let interface_name = &self.name;
654
655        Ok(format!(
656            "{}_{}",
657            package_name.to_string().to_snake_case(),
658            interface_name.to_snake_case()
659        ))
660    }
661
662    pub fn rust_interface_name(&self) -> Ident {
663        let interface_name = format!("Js{}Module", self.name.to_upper_camel_case());
664        Ident::new(&interface_name, Span::call_site())
665    }
666
667    pub fn name_and_interface(&self) -> Option<(&str, &Interface)> {
668        self.interface
669            .map(|interface| (self.name.as_str(), interface))
670    }
671
672    pub fn fully_qualified_interface_name(&self) -> String {
673        if let Some(package_name) = &self.package_name {
674            package_name.interface_id(&self.name)
675        } else {
676            self.name.clone()
677        }
678    }
679
680    pub fn interface_stack(&self) -> VecDeque<InterfaceId> {
681        self.interface_id.iter().cloned().collect()
682    }
683}
684
685/// Recursively copies a WIT directory to `<output>/wit`.
686fn copy_wit_directory(wit: &Utf8Path, output: &Utf8Path) -> anyhow::Result<()> {
687    std::fs::create_dir_all(output)?;
688    copy_dir_if_changed(wit.as_std_path(), output.as_std_path())
689        .context("Failed to copy WIT directory")?;
690    Ok(())
691}
692
693fn copy_dir_if_changed(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
694    std::fs::create_dir_all(dst)?;
695    for entry in std::fs::read_dir(src)? {
696        let entry = entry?;
697        let src_path = entry.path();
698        let dst_path = dst.join(entry.file_name());
699        if src_path.is_dir() {
700            copy_dir_if_changed(&src_path, &dst_path)?;
701        } else {
702            copy_if_changed(&src_path, &dst_path)?;
703        }
704    }
705    Ok(())
706}
707
708/// Copies the JS module files to `<output>/src/<name>.js` or generates slot files.
709fn copy_js_modules(js_modules: &[JsModuleSpec], output: &Utf8Path) -> anyhow::Result<()> {
710    let mut slot_index: u32 = 0;
711    for module in js_modules {
712        match &module.mode {
713            EmbeddingMode::EmbedFile(source) => {
714                let filename = module.file_name();
715                let js_dest = output.join("src").join(filename);
716                copy_if_changed(source, js_dest)
717                    .context(format!("Failed to copy JavaScript module {}", module.name))?;
718            }
719            EmbeddingMode::BinarySlot => {
720                let slot_filename = module.name.replace('/', "_") + ".slot";
721                let slot_dest = output.join("src").join(slot_filename);
722                let slot_data = inject::create_marker_file(slot_index);
723                write_if_changed(slot_dest, slot_data).context(format!(
724                    "Failed to create marker file for module {}",
725                    module.name
726                ))?;
727                slot_index += 1;
728            }
729            EmbeddingMode::Composition => {}
730        }
731    }
732    Ok(())
733}
734
735/// Checks if any of the provided JS modules uses composition mode.
736fn uses_composition(js_module_spec: &[JsModuleSpec]) -> bool {
737    js_module_spec
738        .iter()
739        .any(|m| matches!(m.mode, EmbeddingMode::Composition))
740}