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 crate::wit::add_get_script_import;
6use anyhow::{Context, anyhow};
7use camino::{Utf8Path, Utf8PathBuf};
8use fs_extra::dir::CopyOptions;
9use heck::{ToSnakeCase, ToUpperCamelCase};
10use proc_macro2::{Ident, Span};
11use std::cell::RefCell;
12use std::collections::{BTreeSet, VecDeque};
13use wit_parser::{
14 Function, Interface, InterfaceId, PackageId, PackageName, PackageSourceMap, Resolve, TypeDef,
15 TypeId, TypeOwner, WorldId, WorldItem,
16};
17
18mod conversions;
19mod exports;
20mod imports;
21mod javascript;
22mod rust_bindgen;
23mod skeleton;
24mod types;
25mod typescript;
26mod wit;
27
28#[derive(Debug, Clone)]
30pub enum EmbeddingMode {
31 EmbedFile(Utf8PathBuf),
33 Composition,
35}
36
37#[derive(Debug, Clone)]
39pub struct JsModuleSpec {
40 pub name: String,
41 pub mode: EmbeddingMode,
42}
43
44impl JsModuleSpec {
45 pub fn file_name(&self) -> String {
46 self.name.replace('/', "_") + ".js"
47 }
48}
49
50pub fn generate_wrapper_crate(
64 wit: &Utf8Path,
65 js_modules: &[JsModuleSpec],
66 output: &Utf8Path,
67 world: Option<&str>,
68) -> anyhow::Result<()> {
69 std::fs::create_dir_all(output).context("Failed to create output directory")?;
71 std::fs::create_dir_all(output.join("src")).context("Failed to create output/src directory")?;
72 std::fs::create_dir_all(output.join("src").join("modules"))
73 .context("Failed to create output/src/modules directory")?;
74
75 let context = GeneratorContext::new(output, wit, world)?;
77
78 generate_cargo_toml(&context)?;
80
81 generate_app_manifest(&context)?;
83
84 copy_skeleton_sources(context.output).context("Failed to copy skeleton sources")?;
86
87 copy_wit_directory(wit, &context.output.join("wit"))
89 .context("Failed to copy WIT package to output directory")?;
90
91 if uses_composition(js_modules) {
92 add_get_script_import(&context.output.join("wit"), world)
93 .context("Failed to add get-script import to the WIT world")?;
94 }
95
96 copy_js_modules(js_modules, context.output)
98 .context("Failed to copy JavaScript module to output directory")?;
99
100 generate_export_impls(&context, js_modules)
102 .context("Failed to generate the component export implementations")?;
103
104 generate_import_modules(&context).context("Failed to generate the component import modules")?;
106
107 generate_conversions(&context)
110 .context("Failed to generate the IntoJs and FromJs typeclass instances")?;
111
112 Ok(())
113}
114
115pub fn generate_dts(
119 wit: &Utf8Path,
120 output: &Utf8Path,
121 world: Option<&str>,
122) -> anyhow::Result<Vec<Utf8PathBuf>> {
123 std::fs::create_dir_all(output).context("Failed to create output directory")?;
125
126 let context = GeneratorContext::new(output, wit, world)?;
128
129 let mut result = Vec::new();
130 result.extend(
131 typescript::generate_export_module(&context)
132 .context("Failed to generate the TypeScript module definition for the exports")?,
133 );
134
135 result.extend(typescript::generate_import_modules(&context).context(
137 "Failed to generate the TypeScript module definitions for the imported modules",
138 )?);
139
140 Ok(result)
141}
142
143struct GeneratorContext<'a> {
144 output: &'a Utf8Path,
145 wit_source_path: &'a Utf8Path,
146 resolve: Resolve,
147 root_package: PackageId,
148 world: WorldId,
149 source_map: PackageSourceMap,
150 visited_types: RefCell<BTreeSet<TypeId>>,
151 world_name: String,
152 types: wit_bindgen_core::Types,
153}
154
155impl<'a> GeneratorContext<'a> {
156 fn new(output: &'a Utf8Path, wit: &'a Utf8Path, world: Option<&str>) -> anyhow::Result<Self> {
157 let mut resolve = Resolve::default();
158 let (root_package, source_map) = resolve
159 .push_path(wit)
160 .context("Failed to resolve WIT package")?;
161 let world = resolve
162 .select_world(root_package, world)
163 .context("Failed to select WIT world")?;
164
165 let world_name = resolve.worlds[world].name.clone();
166
167 let mut types = wit_bindgen_core::Types::default();
168 types.analyze(&resolve);
169
170 Ok(Self {
171 output,
172 wit_source_path: wit,
173 resolve,
174 root_package,
175 world,
176 source_map,
177 visited_types: RefCell::new(BTreeSet::new()),
178 world_name,
179 types,
180 })
181 }
182
183 fn root_package_name(&self) -> String {
184 self.resolve.packages[self.root_package].name.to_string()
185 }
186
187 fn record_visited_type(&self, type_id: TypeId) {
188 self.visited_types.borrow_mut().insert(type_id);
189 }
190
191 fn is_exported_interface(&self, interface_id: InterfaceId) -> bool {
192 let world = &self.resolve.worlds[self.world];
193 world
194 .exports
195 .iter()
196 .any(|(_, item)| matches!(item, WorldItem::Interface { id, .. } if id == &interface_id))
197 }
198
199 fn is_exported_type(&self, type_id: TypeId) -> bool {
200 if let Some(typ) = self.resolve.types.get(type_id) {
201 match &typ.owner {
202 TypeOwner::World(world_id) => {
203 if world_id == &self.world {
204 let world = &self.resolve.worlds[self.world];
205 world
206 .exports
207 .iter()
208 .any(|(_, item)| matches!(item, WorldItem::Type(id) if id == &type_id))
209 } else {
210 false
211 }
212 }
213 TypeOwner::Interface(interface_id) => self.is_exported_interface(*interface_id),
214 TypeOwner::None => false,
215 }
216 } else {
217 false
218 }
219 }
220
221 fn bindgen_type_info(&self, type_id: TypeId) -> wit_bindgen_core::TypeInfo {
222 self.types.get(type_id)
223 }
224
225 fn get_imported_interface(
226 &self,
227 interface_id: &InterfaceId,
228 ) -> anyhow::Result<ImportedInterface<'_>> {
229 let interface = &self.resolve.interfaces[*interface_id];
230 let name = interface
231 .name
232 .as_ref()
233 .ok_or_else(|| anyhow!("Interface import does not have a name"))?
234 .as_str();
235
236 let functions = interface
237 .functions
238 .iter()
239 .map(|(name, f)| (name.as_str(), f))
240 .collect();
241
242 let package_id = interface
243 .package
244 .ok_or_else(|| anyhow!("Anonymous interface imports are not supported yet"))?;
245 let package = self
246 .resolve
247 .packages
248 .get(package_id)
249 .ok_or_else(|| anyhow!("Could not find package of imported interface {name}"))?;
250 let package_name = &package.name;
251
252 Ok(ImportedInterface {
253 package_name: Some(package_name),
254 name: name.to_string(),
255 functions,
256 interface: Some(interface),
257 interface_id: Some(*interface_id),
258 })
259 }
260
261 fn typ(&self, type_id: TypeId) -> anyhow::Result<&TypeDef> {
262 self.resolve
263 .types
264 .get(type_id)
265 .ok_or_else(|| anyhow!("Unknown type id: {type_id:?}"))
266 }
267}
268
269pub struct ImportedInterface<'a> {
270 package_name: Option<&'a PackageName>,
271 name: String,
272 functions: Vec<(&'a str, &'a Function)>,
273 interface: Option<&'a Interface>,
274 interface_id: Option<InterfaceId>,
275}
276
277impl<'a> ImportedInterface<'a> {
278 pub fn module_name(&self) -> anyhow::Result<String> {
279 let package_name = self
280 .package_name
281 .ok_or_else(|| anyhow!("imported interface has no package name"))?;
282 let interface_name = &self.name;
283
284 Ok(format!(
285 "{}_{}",
286 package_name.to_string().to_snake_case(),
287 interface_name.to_snake_case()
288 ))
289 }
290
291 pub fn rust_interface_name(&self) -> Ident {
292 let interface_name = format!("Js{}Module", self.name.to_upper_camel_case());
293 Ident::new(&interface_name, Span::call_site())
294 }
295
296 pub fn name_and_interface(&self) -> Option<(&str, &Interface)> {
297 self.interface
298 .map(|interface| (self.name.as_str(), interface))
299 }
300
301 pub fn fully_qualified_interface_name(&self) -> String {
302 if let Some(package_name) = &self.package_name {
303 package_name.interface_id(&self.name)
304 } else {
305 self.name.clone()
306 }
307 }
308
309 pub fn interface_stack(&self) -> VecDeque<InterfaceId> {
310 self.interface_id.iter().cloned().collect()
311 }
312}
313
314fn copy_wit_directory(wit: &Utf8Path, output: &Utf8Path) -> anyhow::Result<()> {
316 fs_extra::dir::create(output, true)
317 .context("Failed to create and erase output WIT directory")?;
318 fs_extra::dir::copy(wit, output, &CopyOptions::new().content_only(true))
319 .context("Failed to copy WIT directory")?;
320
321 Ok(())
322}
323
324fn copy_js_modules(js_modules: &[JsModuleSpec], output: &Utf8Path) -> anyhow::Result<()> {
326 for module in js_modules {
327 if let EmbeddingMode::EmbedFile(source) = &module.mode {
328 let filename = module.file_name();
329 let js_dest = output.join("src").join(filename);
330 std::fs::copy(source, js_dest)
331 .context(format!("Failed to copy JavaScript module {}", module.name))?;
332 }
333 }
334 Ok(())
335}
336
337fn uses_composition(js_module_spec: &[JsModuleSpec]) -> bool {
339 js_module_spec
340 .iter()
341 .any(|m| matches!(m.mode, EmbeddingMode::Composition))
342}