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
18const 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
31const WASI_REMAP_NAMESPACES_P3: &[(&str, &str)] = &[("clocks", "clocks")];
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
51pub enum GenerationTarget {
52 #[default]
54 WasiP2,
55 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
83pub(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
99pub(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#[derive(Debug, Clone)]
117pub enum EmbeddingMode {
118 EmbedFile(Utf8PathBuf),
120 Composition,
122 BinarySlot,
127}
128
129impl EmbeddingMode {
130 pub fn is_binary_slot(&self) -> bool {
131 matches!(self, EmbeddingMode::BinarySlot)
132 }
133}
134
135#[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
148pub 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
173pub 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 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 let context = GeneratorContext::new(output, wit, world, target)?;
198
199 generate_cargo_toml(&context)?;
201
202 copy_skeleton_lock(context.output).context("Failed to copy skeleton Cargo.lock")?;
204
205 copy_skeleton_sources(context.output).context("Failed to copy skeleton sources")?;
207
208 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_init_export(&context.output.join("wit"), world, target.is_p3())
219 .context("Failed to add wizer-initialize export to the WIT world")?;
220
221 let modified_wit = output.join("wit");
223 let context = GeneratorContext::new(output, &modified_wit, world, target)?;
224
225 copy_js_modules(js_modules, context.output)
227 .context("Failed to copy JavaScript module to output directory")?;
228
229 generate_export_impls(&context, js_modules)
231 .context("Failed to generate the component export implementations")?;
232
233 generate_import_modules(&context).context("Failed to generate the component import modules")?;
235
236 generate_conversions(&context)
239 .context("Failed to generate the IntoJs and FromJs typeclass instances")?;
240
241 Ok(())
242}
243
244pub 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
255pub fn generate_dts_with_target(
261 wit: &Utf8Path,
262 output: &Utf8Path,
263 world: Option<&str>,
264 target: GenerationTarget,
265) -> anyhow::Result<Vec<Utf8PathBuf>> {
266 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 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 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 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 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 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 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 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 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
685fn 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
708fn 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
735fn uses_composition(js_module_spec: &[JsModuleSpec]) -> bool {
737 js_module_spec
738 .iter()
739 .any(|m| matches!(m.mode, EmbeddingMode::Composition))
740}