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.world_name)
204 .context("Failed to copy skeleton Cargo.lock")?;
205
206 copy_skeleton_sources(context.output).context("Failed to copy skeleton sources")?;
208
209 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_init_export(&context.output.join("wit"), world, target.is_p3())
220 .context("Failed to add wizer-initialize export to the WIT world")?;
221
222 let modified_wit = output.join("wit");
224 let context = GeneratorContext::new(output, &modified_wit, world, target)?;
225
226 copy_js_modules(js_modules, context.output)
228 .context("Failed to copy JavaScript module to output directory")?;
229
230 generate_export_impls(&context, js_modules)
232 .context("Failed to generate the component export implementations")?;
233
234 generate_import_modules(&context).context("Failed to generate the component import modules")?;
236
237 generate_conversions(&context)
240 .context("Failed to generate the IntoJs and FromJs typeclass instances")?;
241
242 Ok(())
243}
244
245pub 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
256pub fn generate_dts_with_target(
262 wit: &Utf8Path,
263 output: &Utf8Path,
264 world: Option<&str>,
265 target: GenerationTarget,
266) -> anyhow::Result<Vec<Utf8PathBuf>> {
267 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 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 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 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 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 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 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 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 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
686fn 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
709fn 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
736fn uses_composition(js_module_spec: &[JsModuleSpec]) -> bool {
738 js_module_spec
739 .iter()
740 .any(|m| matches!(m.mode, EmbeddingMode::Composition))
741}