1use crate::conversions::generate_conversions;
2use crate::exports::generate_export_impls;
3use crate::imports::generate_import_modules;
4use crate::skeleton::{copy_skeleton_sources, generate_app_manifest, generate_cargo_toml};
5use anyhow::{Context, anyhow};
6use camino::Utf8Path;
7use fs_extra::dir::CopyOptions;
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, TypeId,
14 TypeOwner, WorldId, WorldItem,
15};
16
17mod conversions;
18mod exports;
19mod imports;
20mod javascript;
21mod rust_bindgen;
22mod skeleton;
23mod types;
24mod typescript;
25
26pub fn generate_wrapper_crate(
38 wit: &Utf8Path,
39 js: &Utf8Path,
40 output: &Utf8Path,
41 world: Option<&str>,
42) -> anyhow::Result<()> {
43 std::fs::create_dir_all(output).context("Failed to create output directory")?;
45 std::fs::create_dir_all(output.join("src")).context("Failed to create output/src directory")?;
46 std::fs::create_dir_all(output.join("src").join("modules"))
47 .context("Failed to create output/src/modules directory")?;
48
49 let context = GeneratorContext::new(output, wit, world)?;
51
52 generate_cargo_toml(&context)?;
54
55 generate_app_manifest(&context)?;
57
58 copy_skeleton_sources(context.output).context("Failed to copy skeleton sources")?;
60
61 copy_wit_directory(wit, &context.output.join("wit"))
63 .context("Failed to copy WIT package to output directory")?;
64
65 copy_js_module(js, context.output)
67 .context("Failed to copy JavaScript module to output directory")?;
68
69 generate_export_impls(&context)
71 .context("Failed to generate the component export implementations")?;
72
73 generate_import_modules(&context).context("Failed to generate the component import modules")?;
75
76 generate_conversions(&context)
79 .context("Failed to generate the IntoJs and FromJs typeclass instances")?;
80
81 Ok(())
82}
83
84pub fn generate_dts(wit: &Utf8Path, output: &Utf8Path, world: Option<&str>) -> anyhow::Result<()> {
86 std::fs::create_dir_all(output).context("Failed to create output directory")?;
88
89 let context = GeneratorContext::new(output, wit, world)?;
91
92 typescript::generate_export_module(&context)
93 .context("Failed to generate the TypeScript module definition for the exports")?;
94
95 typescript::generate_import_modules(&context)
97 .context("Failed to generate the TypeScript module definitions for the imported modules")?;
98
99 Ok(())
100}
101
102struct GeneratorContext<'a> {
103 output: &'a Utf8Path,
104 wit_source_path: &'a Utf8Path,
105 resolve: Resolve,
106 root_package: PackageId,
107 world: WorldId,
108 source_map: PackageSourceMap,
109 visited_types: RefCell<BTreeSet<TypeId>>,
110 world_name: String,
111 types: wit_bindgen_core::Types,
112}
113
114impl<'a> GeneratorContext<'a> {
115 fn new(output: &'a Utf8Path, wit: &'a Utf8Path, world: Option<&str>) -> anyhow::Result<Self> {
116 let mut resolve = Resolve::default();
117 let (root_package, source_map) = resolve
118 .push_path(wit)
119 .context("Failed to resolve WIT package")?;
120 let world = resolve
121 .select_world(root_package, world)
122 .context("Failed to select WIT world")?;
123
124 let world_name = resolve.worlds[world].name.clone();
125
126 let mut types = wit_bindgen_core::Types::default();
127 types.analyze(&resolve);
128
129 Ok(Self {
130 output,
131 wit_source_path: wit,
132 resolve,
133 root_package,
134 world,
135 source_map,
136 visited_types: RefCell::new(BTreeSet::new()),
137 world_name,
138 types,
139 })
140 }
141
142 fn root_package_name(&self) -> String {
143 self.resolve.packages[self.root_package].name.to_string()
144 }
145
146 fn record_visited_type(&self, type_id: TypeId) {
147 self.visited_types.borrow_mut().insert(type_id);
148 }
149
150 fn is_exported_interface(&self, interface_id: InterfaceId) -> bool {
151 let world = &self.resolve.worlds[self.world];
152 world
153 .exports
154 .iter()
155 .any(|(_, item)| matches!(item, WorldItem::Interface { id, .. } if id == &interface_id))
156 }
157
158 fn is_exported_type(&self, type_id: TypeId) -> bool {
159 if let Some(typ) = self.resolve.types.get(type_id) {
160 match &typ.owner {
161 TypeOwner::World(world_id) => {
162 if world_id == &self.world {
163 let world = &self.resolve.worlds[self.world];
164 world
165 .exports
166 .iter()
167 .any(|(_, item)| matches!(item, WorldItem::Type(id) if id == &type_id))
168 } else {
169 false
170 }
171 }
172 TypeOwner::Interface(interface_id) => self.is_exported_interface(*interface_id),
173 TypeOwner::None => false,
174 }
175 } else {
176 false
177 }
178 }
179
180 fn bindgen_type_info(&self, type_id: TypeId) -> wit_bindgen_core::TypeInfo {
181 self.types.get(type_id)
182 }
183
184 fn get_imported_interface(
185 &self,
186 interface_id: &InterfaceId,
187 ) -> anyhow::Result<ImportedInterface> {
188 let interface = &self.resolve.interfaces[*interface_id];
189 let name = interface
190 .name
191 .as_ref()
192 .ok_or_else(|| anyhow!("Interface import does not have a name"))?
193 .as_str();
194
195 let functions = interface
196 .functions
197 .iter()
198 .map(|(name, f)| (name.as_str(), f))
199 .collect();
200
201 let package_id = interface
202 .package
203 .ok_or_else(|| anyhow!("Anonymous interface imports are not supported yet"))?;
204 let package = self
205 .resolve
206 .packages
207 .get(package_id)
208 .ok_or_else(|| anyhow!("Could not find package of imported interface {name}"))?;
209 let package_name = &package.name;
210
211 Ok(ImportedInterface {
212 package_name: Some(package_name),
213 name: name.to_string(),
214 functions,
215 interface: Some(interface),
216 interface_id: Some(*interface_id),
217 })
218 }
219}
220
221pub struct ImportedInterface<'a> {
222 package_name: Option<&'a PackageName>,
223 name: String,
224 functions: Vec<(&'a str, &'a Function)>,
225 interface: Option<&'a Interface>,
226 interface_id: Option<InterfaceId>,
227}
228
229impl<'a> ImportedInterface<'a> {
230 pub fn module_name(&self) -> anyhow::Result<String> {
231 let package_name = self
232 .package_name
233 .ok_or_else(|| anyhow!("imported interface has no package name"))?;
234 let interface_name = &self.name;
235
236 Ok(format!(
237 "{}_{}",
238 package_name.to_string().to_snake_case(),
239 interface_name.to_snake_case()
240 ))
241 }
242
243 pub fn rust_interface_name(&self) -> Ident {
244 let interface_name = format!("Js{}Module", self.name.to_upper_camel_case());
245 Ident::new(&interface_name, Span::call_site())
246 }
247
248 pub fn name_and_interface(&self) -> Option<(&str, &Interface)> {
249 self.interface
250 .map(|interface| (self.name.as_str(), interface))
251 }
252
253 pub fn fully_qualified_interface_name(&self) -> String {
254 if let Some(package_name) = &self.package_name {
255 package_name.interface_id(&self.name)
256 } else {
257 self.name.clone()
258 }
259 }
260
261 pub fn interface_stack(&self) -> VecDeque<InterfaceId> {
262 self.interface_id.iter().cloned().collect()
263 }
264}
265
266fn copy_wit_directory(wit: &Utf8Path, output: &Utf8Path) -> anyhow::Result<()> {
268 fs_extra::dir::create(output, true)
269 .context("Failed to create and erase output WIT directory")?;
270 fs_extra::dir::copy(wit, output, &CopyOptions::new().content_only(true))
271 .context("Failed to copy WIT directory")?;
272
273 Ok(())
274}
275
276fn copy_js_module(js: &Utf8Path, output: &Utf8Path) -> anyhow::Result<()> {
278 let js_dest = output.join("src").join("module.js");
279 std::fs::copy(js, js_dest).context("Failed to copy JavaScript module")?;
280 Ok(())
281}