1use crate::conversions::generate_conversions;
2use crate::exports::generate_export_impls;
3use crate::imports::generate_import_modules;
4use crate::skeleton::{copy_skeleton_lock, copy_skeleton_sources, generate_cargo_toml};
5use crate::wit::{add_get_script_import, add_wizer_init_export};
6use anyhow::{Context, anyhow};
7use camino::{Utf8Path, Utf8PathBuf};
8use heck::{ToSnakeCase, ToUpperCamelCase};
9use proc_macro2::{Ident, Span};
10use std::cell::RefCell;
11use std::collections::{BTreeSet, VecDeque};
12use wit_parser::{
13 Function, Interface, InterfaceId, PackageId, PackageName, PackageSourceMap, Resolve, TypeDef,
14 TypeId, TypeOwner, WorldId, WorldItem,
15};
16
17const WASI_REMAP_NAMESPACES: &[(&str, &str)] = &[
21 ("cli", "cli"),
22 ("clocks", "clocks"),
23 ("filesystem", "filesystem"),
24 ("http", "http"),
25 ("io", "io"),
26 ("random", "random"),
27 ("sockets", "sockets"),
28];
29
30mod conversions;
31mod exports;
32mod imports;
33mod inject;
34mod javascript;
35#[cfg(feature = "optimize")]
36mod optimize;
37mod rust_bindgen;
38mod skeleton;
39mod types;
40mod typescript;
41mod wit;
42
43pub use inject::{SLOT_END_MAGIC, SLOT_MAGIC, create_marker_file, inject_js_into_component};
44#[cfg(feature = "optimize")]
45pub use optimize::optimize_component;
46
47pub(crate) fn write_if_changed(
50 path: impl AsRef<std::path::Path>,
51 contents: impl AsRef<[u8]>,
52) -> std::io::Result<()> {
53 let path = path.as_ref();
54 let contents = contents.as_ref();
55 if let Ok(existing) = std::fs::read(path)
56 && existing == contents
57 {
58 return Ok(());
59 }
60 std::fs::write(path, contents)
61}
62
63pub(crate) fn copy_if_changed(
65 src: impl AsRef<std::path::Path>,
66 dst: impl AsRef<std::path::Path>,
67) -> std::io::Result<()> {
68 let src = src.as_ref();
69 let dst = dst.as_ref();
70 let src_contents = std::fs::read(src)?;
71 if let Ok(existing) = std::fs::read(dst)
72 && existing == src_contents
73 {
74 return Ok(());
75 }
76 std::fs::write(dst, src_contents)
77}
78
79#[derive(Debug, Clone)]
81pub enum EmbeddingMode {
82 EmbedFile(Utf8PathBuf),
84 Composition,
86 BinarySlot,
91}
92
93impl EmbeddingMode {
94 pub fn is_binary_slot(&self) -> bool {
95 matches!(self, EmbeddingMode::BinarySlot)
96 }
97}
98
99#[derive(Debug, Clone)]
101pub struct JsModuleSpec {
102 pub name: String,
103 pub mode: EmbeddingMode,
104}
105
106impl JsModuleSpec {
107 pub fn file_name(&self) -> String {
108 self.name.replace('/', "_") + ".js"
109 }
110}
111
112pub fn generate_wrapper_crate(
126 wit: &Utf8Path,
127 js_modules: &[JsModuleSpec],
128 output: &Utf8Path,
129 world: Option<&str>,
130) -> anyhow::Result<()> {
131 std::fs::create_dir_all(output).context("Failed to create output directory")?;
133 std::fs::create_dir_all(output.join("src")).context("Failed to create output/src directory")?;
134 std::fs::create_dir_all(output.join("src").join("modules"))
135 .context("Failed to create output/src/modules directory")?;
136
137 let context = GeneratorContext::new(output, wit, world)?;
139
140 generate_cargo_toml(&context)?;
142
143 copy_skeleton_lock(context.output).context("Failed to copy skeleton Cargo.lock")?;
145
146 copy_skeleton_sources(context.output).context("Failed to copy skeleton sources")?;
148
149 copy_wit_directory(wit, &context.output.join("wit"))
151 .context("Failed to copy WIT package to output directory")?;
152
153 if uses_composition(js_modules) {
154 add_get_script_import(&context.output.join("wit"), world)
155 .context("Failed to add get-script import to the WIT world")?;
156 }
157
158 add_wizer_init_export(&context.output.join("wit"), world)
160 .context("Failed to add wizer-initialize export to the WIT world")?;
161
162 let modified_wit = output.join("wit");
164 let context = GeneratorContext::new(output, &modified_wit, world)?;
165
166 copy_js_modules(js_modules, context.output)
168 .context("Failed to copy JavaScript module to output directory")?;
169
170 generate_export_impls(&context, js_modules)
172 .context("Failed to generate the component export implementations")?;
173
174 generate_import_modules(&context).context("Failed to generate the component import modules")?;
176
177 generate_conversions(&context)
180 .context("Failed to generate the IntoJs and FromJs typeclass instances")?;
181
182 Ok(())
183}
184
185pub fn generate_dts(
189 wit: &Utf8Path,
190 output: &Utf8Path,
191 world: Option<&str>,
192) -> anyhow::Result<Vec<Utf8PathBuf>> {
193 std::fs::create_dir_all(output).context("Failed to create output directory")?;
195
196 let context = GeneratorContext::new(output, wit, world)?;
198
199 let mut result = Vec::new();
200 result.extend(
201 typescript::generate_export_module(&context)
202 .context("Failed to generate the TypeScript module definition for the exports")?,
203 );
204
205 result.extend(typescript::generate_import_modules(&context).context(
207 "Failed to generate the TypeScript module definitions for the imported modules",
208 )?);
209
210 Ok(result)
211}
212
213struct GeneratorContext<'a> {
214 output: &'a Utf8Path,
215 #[allow(dead_code)]
216 wit_source_path: &'a Utf8Path,
217 resolve: Resolve,
218 root_package: PackageId,
219 world: WorldId,
220 #[allow(dead_code)]
221 source_map: PackageSourceMap,
222 visited_types: RefCell<BTreeSet<TypeId>>,
223 world_name: String,
224 types: wit_bindgen_core::Types,
225}
226
227impl<'a> GeneratorContext<'a> {
228 fn new(output: &'a Utf8Path, wit: &'a Utf8Path, world: Option<&str>) -> anyhow::Result<Self> {
229 let mut resolve = Resolve::default();
230 let (root_package, source_map) = resolve
231 .push_path(wit)
232 .context("Failed to resolve WIT package")?;
233 let world = resolve
234 .select_world(root_package, world)
235 .context("Failed to select WIT world")?;
236
237 let world_name = resolve.worlds[world].name.clone();
238
239 let mut types = wit_bindgen_core::Types::default();
240 types.analyze(&resolve);
241
242 Ok(Self {
243 output,
244 wit_source_path: wit,
245 resolve,
246 root_package,
247 world,
248 source_map,
249 visited_types: RefCell::new(BTreeSet::new()),
250 world_name,
251 types,
252 })
253 }
254
255 fn root_package_name(&self) -> String {
256 self.resolve.packages[self.root_package].name.to_string()
257 }
258
259 fn record_visited_type(&self, type_id: TypeId) {
260 self.visited_types.borrow_mut().insert(type_id);
261 }
262
263 fn is_exported_interface(&self, interface_id: InterfaceId) -> bool {
264 let world = &self.resolve.worlds[self.world];
265 world
266 .exports
267 .iter()
268 .any(|(_, item)| matches!(item, WorldItem::Interface { id, .. } if id == &interface_id))
269 }
270
271 fn is_exported_type(&self, type_id: TypeId) -> bool {
272 if let Some(typ) = self.resolve.types.get(type_id) {
273 match &typ.owner {
274 TypeOwner::World(world_id) => {
275 if world_id == &self.world {
276 let world = &self.resolve.worlds[self.world];
277 world
278 .exports
279 .iter()
280 .any(|(_, item)| matches!(item, WorldItem::Type(id) if id == &type_id))
281 } else {
282 false
283 }
284 }
285 TypeOwner::Interface(interface_id) => self.is_exported_interface(*interface_id),
286 TypeOwner::None => false,
287 }
288 } else {
289 false
290 }
291 }
292
293 fn bindgen_type_info(&self, type_id: TypeId) -> wit_bindgen_core::TypeInfo {
294 self.types.get(type_id)
295 }
296
297 fn get_imported_interface(
298 &self,
299 interface_id: &InterfaceId,
300 ) -> anyhow::Result<ImportedInterface<'_>> {
301 let interface = &self.resolve.interfaces[*interface_id];
302 let name = interface
303 .name
304 .as_ref()
305 .ok_or_else(|| anyhow!("Interface import does not have a name"))?
306 .as_str();
307
308 let functions = interface
309 .functions
310 .iter()
311 .map(|(name, f)| (name.as_str(), f))
312 .collect();
313
314 let package_id = interface
315 .package
316 .ok_or_else(|| anyhow!("Anonymous interface imports are not supported yet"))?;
317 let package = self
318 .resolve
319 .packages
320 .get(package_id)
321 .ok_or_else(|| anyhow!("Could not find package of imported interface {name}"))?;
322 let package_name = &package.name;
323
324 Ok(ImportedInterface {
325 package_name: Some(package_name),
326 name: name.to_string(),
327 functions,
328 interface: Some(interface),
329 interface_id: Some(*interface_id),
330 })
331 }
332
333 fn typ(&self, type_id: TypeId) -> anyhow::Result<&TypeDef> {
334 self.resolve
335 .types
336 .get(type_id)
337 .ok_or_else(|| anyhow!("Unknown type id: {type_id:?}"))
338 }
339
340 fn is_wasi_remapped_package(&self, package_id: PackageId) -> bool {
343 let package = &self.resolve.packages[package_id];
344 if package.name.namespace != "wasi" {
345 return false;
346 }
347 WASI_REMAP_NAMESPACES
348 .iter()
349 .any(|(pkg_name, _)| *pkg_name == package.name.name.as_str())
350 }
351
352 fn wasi_resource_module_path(
355 &self,
356 type_id: TypeId,
357 ) -> Option<(proc_macro2::TokenStream, Ident)> {
358 let typ = self.resolve.types.get(type_id)?;
359 let resource_name = typ.name.as_ref()?;
360 let resource_ident = Ident::new(&resource_name.to_upper_camel_case(), Span::call_site());
361
362 let interface_id = match &typ.owner {
363 TypeOwner::Interface(id) => *id,
364 _ => return None,
365 };
366 let interface = self.resolve.interfaces.get(interface_id)?;
367 let interface_name = interface.name.as_ref()?;
368 let package_id = interface.package?;
369 let package = self.resolve.packages.get(package_id)?;
370 let package_name = &package.name;
371
372 let module_name = format!(
373 "{}_{}",
374 package_name.to_string().to_snake_case(),
375 interface_name.to_snake_case()
376 );
377 let module_ident = Ident::new(&module_name, Span::call_site());
378
379 Some((
380 quote::quote! { crate::modules::#module_ident },
381 resource_ident,
382 ))
383 }
384
385 fn is_wasi_remapped_type(&self, type_id: TypeId) -> bool {
387 if let Some(typ) = self.resolve.types.get(type_id) {
388 match &typ.owner {
389 TypeOwner::Interface(interface_id) => {
390 if let Some(interface) = self.resolve.interfaces.get(*interface_id)
391 && let Some(package_id) = interface.package
392 {
393 return self.is_wasi_remapped_package(package_id);
394 }
395 false
396 }
397 _ => false,
398 }
399 } else {
400 false
401 }
402 }
403}
404
405pub struct ImportedInterface<'a> {
406 package_name: Option<&'a PackageName>,
407 name: String,
408 functions: Vec<(&'a str, &'a Function)>,
409 interface: Option<&'a Interface>,
410 interface_id: Option<InterfaceId>,
411}
412
413impl<'a> ImportedInterface<'a> {
414 pub fn module_name(&self) -> anyhow::Result<String> {
415 let package_name = self
416 .package_name
417 .ok_or_else(|| anyhow!("imported interface has no package name"))?;
418 let interface_name = &self.name;
419
420 Ok(format!(
421 "{}_{}",
422 package_name.to_string().to_snake_case(),
423 interface_name.to_snake_case()
424 ))
425 }
426
427 pub fn rust_interface_name(&self) -> Ident {
428 let interface_name = format!("Js{}Module", self.name.to_upper_camel_case());
429 Ident::new(&interface_name, Span::call_site())
430 }
431
432 pub fn name_and_interface(&self) -> Option<(&str, &Interface)> {
433 self.interface
434 .map(|interface| (self.name.as_str(), interface))
435 }
436
437 pub fn fully_qualified_interface_name(&self) -> String {
438 if let Some(package_name) = &self.package_name {
439 package_name.interface_id(&self.name)
440 } else {
441 self.name.clone()
442 }
443 }
444
445 pub fn interface_stack(&self) -> VecDeque<InterfaceId> {
446 self.interface_id.iter().cloned().collect()
447 }
448}
449
450fn copy_wit_directory(wit: &Utf8Path, output: &Utf8Path) -> anyhow::Result<()> {
452 std::fs::create_dir_all(output)?;
453 copy_dir_if_changed(wit.as_std_path(), output.as_std_path())
454 .context("Failed to copy WIT directory")?;
455 Ok(())
456}
457
458fn copy_dir_if_changed(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
459 std::fs::create_dir_all(dst)?;
460 for entry in std::fs::read_dir(src)? {
461 let entry = entry?;
462 let src_path = entry.path();
463 let dst_path = dst.join(entry.file_name());
464 if src_path.is_dir() {
465 copy_dir_if_changed(&src_path, &dst_path)?;
466 } else {
467 copy_if_changed(&src_path, &dst_path)?;
468 }
469 }
470 Ok(())
471}
472
473fn copy_js_modules(js_modules: &[JsModuleSpec], output: &Utf8Path) -> anyhow::Result<()> {
475 let mut slot_index: u32 = 0;
476 for module in js_modules {
477 match &module.mode {
478 EmbeddingMode::EmbedFile(source) => {
479 let filename = module.file_name();
480 let js_dest = output.join("src").join(filename);
481 copy_if_changed(source, js_dest)
482 .context(format!("Failed to copy JavaScript module {}", module.name))?;
483 }
484 EmbeddingMode::BinarySlot => {
485 let slot_filename = module.name.replace('/', "_") + ".slot";
486 let slot_dest = output.join("src").join(slot_filename);
487 let slot_data = inject::create_marker_file(slot_index);
488 write_if_changed(slot_dest, slot_data).context(format!(
489 "Failed to create marker file for module {}",
490 module.name
491 ))?;
492 slot_index += 1;
493 }
494 EmbeddingMode::Composition => {}
495 }
496 }
497 Ok(())
498}
499
500fn uses_composition(js_module_spec: &[JsModuleSpec]) -> bool {
502 js_module_spec
503 .iter()
504 .any(|m| matches!(m.mode, EmbeddingMode::Composition))
505}