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 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
31mod conversions;
32mod exports;
33mod imports;
34mod inject;
35mod javascript;
36#[cfg(feature = "optimize")]
37mod optimize;
38mod rust_bindgen;
39mod skeleton;
40mod types;
41mod typescript;
42mod wit;
43
44pub use inject::{SLOT_END_MAGIC, SLOT_MAGIC, create_marker_file, inject_js_into_component};
45#[cfg(feature = "optimize")]
46pub use optimize::optimize_component;
47
48pub(crate) fn write_if_changed(
51 path: impl AsRef<std::path::Path>,
52 contents: impl AsRef<[u8]>,
53) -> std::io::Result<()> {
54 let path = path.as_ref();
55 let contents = contents.as_ref();
56 if let Ok(existing) = std::fs::read(path)
57 && existing == contents
58 {
59 return Ok(());
60 }
61 std::fs::write(path, contents)
62}
63
64pub(crate) fn copy_if_changed(
66 src: impl AsRef<std::path::Path>,
67 dst: impl AsRef<std::path::Path>,
68) -> std::io::Result<()> {
69 let src = src.as_ref();
70 let dst = dst.as_ref();
71 let src_contents = std::fs::read(src)?;
72 if let Ok(existing) = std::fs::read(dst)
73 && existing == src_contents
74 {
75 return Ok(());
76 }
77 std::fs::write(dst, src_contents)
78}
79
80#[derive(Debug, Clone)]
82pub enum EmbeddingMode {
83 EmbedFile(Utf8PathBuf),
85 Composition,
87 BinarySlot,
92}
93
94impl EmbeddingMode {
95 pub fn is_binary_slot(&self) -> bool {
96 matches!(self, EmbeddingMode::BinarySlot)
97 }
98}
99
100#[derive(Debug, Clone)]
102pub struct JsModuleSpec {
103 pub name: String,
104 pub mode: EmbeddingMode,
105}
106
107impl JsModuleSpec {
108 pub fn file_name(&self) -> String {
109 self.name.replace('/', "_") + ".js"
110 }
111}
112
113pub fn generate_wrapper_crate(
127 wit: &Utf8Path,
128 js_modules: &[JsModuleSpec],
129 output: &Utf8Path,
130 world: Option<&str>,
131) -> anyhow::Result<()> {
132 std::fs::create_dir_all(output).context("Failed to create output directory")?;
134 std::fs::create_dir_all(output.join("src")).context("Failed to create output/src directory")?;
135 std::fs::create_dir_all(output.join("src").join("modules"))
136 .context("Failed to create output/src/modules directory")?;
137
138 let context = GeneratorContext::new(output, wit, world)?;
140
141 generate_cargo_toml(&context)?;
143
144 copy_skeleton_lock(context.output).context("Failed to copy skeleton Cargo.lock")?;
146
147 copy_skeleton_sources(context.output).context("Failed to copy skeleton sources")?;
149
150 copy_wit_directory(wit, &context.output.join("wit"))
152 .context("Failed to copy WIT package to output directory")?;
153
154 if uses_composition(js_modules) {
155 add_get_script_import(&context.output.join("wit"), world)
156 .context("Failed to add get-script import to the WIT world")?;
157 }
158
159 add_wizer_init_export(&context.output.join("wit"), world)
161 .context("Failed to add wizer-initialize export to the WIT world")?;
162
163 let modified_wit = output.join("wit");
165 let context = GeneratorContext::new(output, &modified_wit, world)?;
166
167 copy_js_modules(js_modules, context.output)
169 .context("Failed to copy JavaScript module to output directory")?;
170
171 generate_export_impls(&context, js_modules)
173 .context("Failed to generate the component export implementations")?;
174
175 generate_import_modules(&context).context("Failed to generate the component import modules")?;
177
178 generate_conversions(&context)
181 .context("Failed to generate the IntoJs and FromJs typeclass instances")?;
182
183 Ok(())
184}
185
186pub fn generate_dts(
190 wit: &Utf8Path,
191 output: &Utf8Path,
192 world: Option<&str>,
193) -> anyhow::Result<Vec<Utf8PathBuf>> {
194 std::fs::create_dir_all(output).context("Failed to create output directory")?;
196
197 let context = GeneratorContext::new(output, wit, world)?;
199
200 let mut result = Vec::new();
201 result.extend(
202 typescript::generate_export_module(&context)
203 .context("Failed to generate the TypeScript module definition for the exports")?,
204 );
205
206 result.extend(typescript::generate_import_modules(&context).context(
208 "Failed to generate the TypeScript module definitions for the imported modules",
209 )?);
210
211 Ok(result)
212}
213
214struct GeneratorContext<'a> {
215 output: &'a Utf8Path,
216 #[allow(dead_code)]
217 wit_source_path: &'a Utf8Path,
218 resolve: Resolve,
219 root_package: PackageId,
220 world: WorldId,
221 #[allow(dead_code)]
222 source_map: PackageSourceMap,
223 visited_types: RefCell<BTreeSet<TypeId>>,
224 world_name: String,
225 types: wit_bindgen_core::Types,
226}
227
228impl<'a> GeneratorContext<'a> {
229 fn new(output: &'a Utf8Path, wit: &'a Utf8Path, world: Option<&str>) -> anyhow::Result<Self> {
230 let mut resolve = Resolve::default();
231 let (root_package, source_map) = resolve
232 .push_path(wit)
233 .context("Failed to resolve WIT package")?;
234 let world = resolve
235 .select_world(std::slice::from_ref(&root_package), world)
236 .context("Failed to select WIT world")?;
237
238 let world_name = resolve.worlds[world].name.clone();
239
240 let mut types = wit_bindgen_core::Types::default();
241 types.analyze(&resolve);
242
243 Ok(Self {
244 output,
245 wit_source_path: wit,
246 resolve,
247 root_package,
248 world,
249 source_map,
250 visited_types: RefCell::new(BTreeSet::new()),
251 world_name,
252 types,
253 })
254 }
255
256 fn root_package_name(&self) -> String {
257 self.resolve.packages[self.root_package].name.to_string()
258 }
259
260 fn record_visited_type(&self, type_id: TypeId) {
261 self.visited_types.borrow_mut().insert(type_id);
262 }
263
264 fn is_exported_interface(&self, interface_id: InterfaceId) -> bool {
265 let world = &self.resolve.worlds[self.world];
266 world
267 .exports
268 .iter()
269 .any(|(_, item)| matches!(item, WorldItem::Interface { id, .. } if id == &interface_id))
270 }
271
272 fn exported_interface_js_name(
273 &self,
274 interface_id: InterfaceId,
275 export_name: &str,
276 ) -> anyhow::Result<String> {
277 let names = self.exported_interface_js_names()?;
278 names
279 .get(&interface_id)
280 .cloned()
281 .ok_or_else(|| anyhow!("Interface export not found: {export_name}"))
282 }
283
284 fn exported_interface_js_names(&self) -> anyhow::Result<BTreeMap<InterfaceId, String>> {
285 let world = &self.resolve.worlds[self.world];
286 let mut exported_interfaces = Vec::new();
287
288 for (key, export) in &world.exports {
289 if let WorldItem::Interface { id, .. } = export {
290 let interface = &self.resolve.interfaces[*id];
291 let export_name = match key {
292 WorldKey::Name(name) => name.as_str(),
293 WorldKey::Interface(_) => interface
294 .name
295 .as_deref()
296 .ok_or_else(|| anyhow!("Interface export does not have a name"))?,
297 };
298 let short_name = exported_interface_short_js_name(export_name);
299 exported_interfaces.push((*id, export_name.to_string(), short_name));
300 }
301 }
302
303 let mut short_name_counts = BTreeMap::<String, usize>::new();
304 for (_, _, short_name) in &exported_interfaces {
305 *short_name_counts.entry(short_name.clone()).or_default() += 1;
306 }
307
308 let mut result = BTreeMap::new();
309 let mut used_names = BTreeMap::<String, InterfaceId>::new();
310 for (interface_id, export_name, short_name) in exported_interfaces {
311 let js_name = if short_name_counts.get(&short_name).copied().unwrap_or(0) > 1 {
312 let interface = &self.resolve.interfaces[interface_id];
313 exported_interface_qualified_js_name(self, interface, &export_name)?
314 } else {
315 short_name
316 };
317
318 if let Some(previous_id) = used_names.insert(js_name.clone(), interface_id) {
319 anyhow::bail!(
320 "Exported WIT interfaces {previous_id:?} and {interface_id:?} both map to JavaScript export name '{js_name}'"
321 );
322 }
323
324 result.insert(interface_id, js_name);
325 }
326
327 Ok(result)
328 }
329
330 fn is_exported_type(&self, type_id: TypeId) -> bool {
331 if let Some(typ) = self.resolve.types.get(type_id) {
332 match &typ.owner {
333 TypeOwner::World(world_id) => {
334 if world_id == &self.world {
335 let world = &self.resolve.worlds[self.world];
336 world
337 .exports
338 .iter()
339 .any(|(_, item)| matches!(item, WorldItem::Type { id, .. } if id == &type_id))
340 } else {
341 false
342 }
343 }
344 TypeOwner::Interface(interface_id) => self.is_exported_interface(*interface_id),
345 TypeOwner::None => false,
346 }
347 } else {
348 false
349 }
350 }
351
352 fn bindgen_type_info(&self, type_id: TypeId) -> wit_bindgen_core::TypeInfo {
353 self.types.get(type_id)
354 }
355
356 fn get_imported_interface(
357 &self,
358 interface_id: &InterfaceId,
359 ) -> anyhow::Result<ImportedInterface<'_>> {
360 let interface = &self.resolve.interfaces[*interface_id];
361 let name = interface
362 .name
363 .as_ref()
364 .ok_or_else(|| anyhow!("Interface import does not have a name"))?
365 .as_str();
366
367 let functions = interface
368 .functions
369 .iter()
370 .map(|(name, f)| (name.as_str(), f))
371 .collect();
372
373 let package_id = interface
374 .package
375 .ok_or_else(|| anyhow!("Anonymous interface imports are not supported yet"))?;
376 let package = self
377 .resolve
378 .packages
379 .get(package_id)
380 .ok_or_else(|| anyhow!("Could not find package of imported interface {name}"))?;
381 let package_name = &package.name;
382
383 Ok(ImportedInterface {
384 package_name: Some(package_name),
385 name: name.to_string(),
386 functions,
387 interface: Some(interface),
388 interface_id: Some(*interface_id),
389 })
390 }
391
392 fn typ(&self, type_id: TypeId) -> anyhow::Result<&TypeDef> {
393 self.resolve
394 .types
395 .get(type_id)
396 .ok_or_else(|| anyhow!("Unknown type id: {type_id:?}"))
397 }
398
399 fn is_wasi_remapped_package(&self, package_id: PackageId) -> bool {
402 let package = &self.resolve.packages[package_id];
403 if package.name.namespace != "wasi" {
404 return false;
405 }
406 WASI_REMAP_NAMESPACES
407 .iter()
408 .any(|(pkg_name, _)| *pkg_name == package.name.name.as_str())
409 }
410
411 fn wasi_resource_module_path(
414 &self,
415 type_id: TypeId,
416 ) -> Option<(proc_macro2::TokenStream, Ident)> {
417 let typ = self.resolve.types.get(type_id)?;
418 let resource_name = typ.name.as_ref()?;
419 let resource_ident = Ident::new(&resource_name.to_upper_camel_case(), Span::call_site());
420
421 let interface_id = match &typ.owner {
422 TypeOwner::Interface(id) => *id,
423 _ => return None,
424 };
425 let interface = self.resolve.interfaces.get(interface_id)?;
426 let interface_name = interface.name.as_ref()?;
427 let package_id = interface.package?;
428 let package = self.resolve.packages.get(package_id)?;
429 let package_name = &package.name;
430
431 let module_name = format!(
432 "{}_{}",
433 package_name.to_string().to_snake_case(),
434 interface_name.to_snake_case()
435 );
436 let module_ident = Ident::new(&module_name, Span::call_site());
437
438 Some((
439 quote::quote! { crate::modules::#module_ident },
440 resource_ident,
441 ))
442 }
443
444 fn is_wasi_remapped_type(&self, type_id: TypeId) -> bool {
446 if let Some(typ) = self.resolve.types.get(type_id) {
447 match &typ.owner {
448 TypeOwner::Interface(interface_id) => {
449 if let Some(interface) = self.resolve.interfaces.get(*interface_id)
450 && let Some(package_id) = interface.package
451 {
452 return self.is_wasi_remapped_package(package_id);
453 }
454 false
455 }
456 _ => false,
457 }
458 } else {
459 false
460 }
461 }
462}
463
464fn exported_interface_short_js_name(export_name: &str) -> String {
465 escape_js_ident(export_name.to_lower_camel_case())
466}
467
468fn exported_interface_qualified_js_name(
469 context: &GeneratorContext<'_>,
470 interface: &Interface,
471 export_name: &str,
472) -> anyhow::Result<String> {
473 let package_id = interface
474 .package
475 .ok_or_else(|| anyhow!("Anonymous interface exports cannot be qualified: {export_name}"))?;
476 let package = context
477 .resolve
478 .packages
479 .get(package_id)
480 .ok_or_else(|| anyhow!("Unknown owner package of interface export: {export_name}"))?;
481 let interface_name = interface.name.as_deref().unwrap_or(export_name);
482 let module_name = format!(
483 "{}_{}",
484 package.name.to_string().to_snake_case(),
485 interface_name.to_snake_case()
486 );
487
488 Ok(escape_js_ident(module_name.to_lower_camel_case()))
489}
490
491pub struct ImportedInterface<'a> {
492 package_name: Option<&'a PackageName>,
493 name: String,
494 functions: Vec<(&'a str, &'a Function)>,
495 interface: Option<&'a Interface>,
496 interface_id: Option<InterfaceId>,
497}
498
499impl<'a> ImportedInterface<'a> {
500 pub fn module_name(&self) -> anyhow::Result<String> {
501 let package_name = self
502 .package_name
503 .ok_or_else(|| anyhow!("imported interface has no package name"))?;
504 let interface_name = &self.name;
505
506 Ok(format!(
507 "{}_{}",
508 package_name.to_string().to_snake_case(),
509 interface_name.to_snake_case()
510 ))
511 }
512
513 pub fn rust_interface_name(&self) -> Ident {
514 let interface_name = format!("Js{}Module", self.name.to_upper_camel_case());
515 Ident::new(&interface_name, Span::call_site())
516 }
517
518 pub fn name_and_interface(&self) -> Option<(&str, &Interface)> {
519 self.interface
520 .map(|interface| (self.name.as_str(), interface))
521 }
522
523 pub fn fully_qualified_interface_name(&self) -> String {
524 if let Some(package_name) = &self.package_name {
525 package_name.interface_id(&self.name)
526 } else {
527 self.name.clone()
528 }
529 }
530
531 pub fn interface_stack(&self) -> VecDeque<InterfaceId> {
532 self.interface_id.iter().cloned().collect()
533 }
534}
535
536fn copy_wit_directory(wit: &Utf8Path, output: &Utf8Path) -> anyhow::Result<()> {
538 std::fs::create_dir_all(output)?;
539 copy_dir_if_changed(wit.as_std_path(), output.as_std_path())
540 .context("Failed to copy WIT directory")?;
541 Ok(())
542}
543
544fn copy_dir_if_changed(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
545 std::fs::create_dir_all(dst)?;
546 for entry in std::fs::read_dir(src)? {
547 let entry = entry?;
548 let src_path = entry.path();
549 let dst_path = dst.join(entry.file_name());
550 if src_path.is_dir() {
551 copy_dir_if_changed(&src_path, &dst_path)?;
552 } else {
553 copy_if_changed(&src_path, &dst_path)?;
554 }
555 }
556 Ok(())
557}
558
559fn copy_js_modules(js_modules: &[JsModuleSpec], output: &Utf8Path) -> anyhow::Result<()> {
561 let mut slot_index: u32 = 0;
562 for module in js_modules {
563 match &module.mode {
564 EmbeddingMode::EmbedFile(source) => {
565 let filename = module.file_name();
566 let js_dest = output.join("src").join(filename);
567 copy_if_changed(source, js_dest)
568 .context(format!("Failed to copy JavaScript module {}", module.name))?;
569 }
570 EmbeddingMode::BinarySlot => {
571 let slot_filename = module.name.replace('/', "_") + ".slot";
572 let slot_dest = output.join("src").join(slot_filename);
573 let slot_data = inject::create_marker_file(slot_index);
574 write_if_changed(slot_dest, slot_data).context(format!(
575 "Failed to create marker file for module {}",
576 module.name
577 ))?;
578 slot_index += 1;
579 }
580 EmbeddingMode::Composition => {}
581 }
582 }
583 Ok(())
584}
585
586fn uses_composition(js_module_spec: &[JsModuleSpec]) -> bool {
588 js_module_spec
589 .iter()
590 .any(|m| matches!(m.mode, EmbeddingMode::Composition))
591}