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, TypeId,
15 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(wit: &Utf8Path, output: &Utf8Path, world: Option<&str>) -> anyhow::Result<()> {
117 std::fs::create_dir_all(output).context("Failed to create output directory")?;
119
120 let context = GeneratorContext::new(output, wit, world)?;
122
123 typescript::generate_export_module(&context)
124 .context("Failed to generate the TypeScript module definition for the exports")?;
125
126 typescript::generate_import_modules(&context)
128 .context("Failed to generate the TypeScript module definitions for the imported modules")?;
129
130 Ok(())
131}
132
133struct GeneratorContext<'a> {
134 output: &'a Utf8Path,
135 wit_source_path: &'a Utf8Path,
136 resolve: Resolve,
137 root_package: PackageId,
138 world: WorldId,
139 source_map: PackageSourceMap,
140 visited_types: RefCell<BTreeSet<TypeId>>,
141 world_name: String,
142 types: wit_bindgen_core::Types,
143}
144
145impl<'a> GeneratorContext<'a> {
146 fn new(output: &'a Utf8Path, wit: &'a Utf8Path, world: Option<&str>) -> anyhow::Result<Self> {
147 let mut resolve = Resolve::default();
148 let (root_package, source_map) = resolve
149 .push_path(wit)
150 .context("Failed to resolve WIT package")?;
151 let world = resolve
152 .select_world(root_package, world)
153 .context("Failed to select WIT world")?;
154
155 let world_name = resolve.worlds[world].name.clone();
156
157 let mut types = wit_bindgen_core::Types::default();
158 types.analyze(&resolve);
159
160 Ok(Self {
161 output,
162 wit_source_path: wit,
163 resolve,
164 root_package,
165 world,
166 source_map,
167 visited_types: RefCell::new(BTreeSet::new()),
168 world_name,
169 types,
170 })
171 }
172
173 fn root_package_name(&self) -> String {
174 self.resolve.packages[self.root_package].name.to_string()
175 }
176
177 fn record_visited_type(&self, type_id: TypeId) {
178 self.visited_types.borrow_mut().insert(type_id);
179 }
180
181 fn is_exported_interface(&self, interface_id: InterfaceId) -> bool {
182 let world = &self.resolve.worlds[self.world];
183 world
184 .exports
185 .iter()
186 .any(|(_, item)| matches!(item, WorldItem::Interface { id, .. } if id == &interface_id))
187 }
188
189 fn is_exported_type(&self, type_id: TypeId) -> bool {
190 if let Some(typ) = self.resolve.types.get(type_id) {
191 match &typ.owner {
192 TypeOwner::World(world_id) => {
193 if world_id == &self.world {
194 let world = &self.resolve.worlds[self.world];
195 world
196 .exports
197 .iter()
198 .any(|(_, item)| matches!(item, WorldItem::Type(id) if id == &type_id))
199 } else {
200 false
201 }
202 }
203 TypeOwner::Interface(interface_id) => self.is_exported_interface(*interface_id),
204 TypeOwner::None => false,
205 }
206 } else {
207 false
208 }
209 }
210
211 fn bindgen_type_info(&self, type_id: TypeId) -> wit_bindgen_core::TypeInfo {
212 self.types.get(type_id)
213 }
214
215 fn get_imported_interface(
216 &self,
217 interface_id: &InterfaceId,
218 ) -> anyhow::Result<ImportedInterface<'_>> {
219 let interface = &self.resolve.interfaces[*interface_id];
220 let name = interface
221 .name
222 .as_ref()
223 .ok_or_else(|| anyhow!("Interface import does not have a name"))?
224 .as_str();
225
226 let functions = interface
227 .functions
228 .iter()
229 .map(|(name, f)| (name.as_str(), f))
230 .collect();
231
232 let package_id = interface
233 .package
234 .ok_or_else(|| anyhow!("Anonymous interface imports are not supported yet"))?;
235 let package = self
236 .resolve
237 .packages
238 .get(package_id)
239 .ok_or_else(|| anyhow!("Could not find package of imported interface {name}"))?;
240 let package_name = &package.name;
241
242 Ok(ImportedInterface {
243 package_name: Some(package_name),
244 name: name.to_string(),
245 functions,
246 interface: Some(interface),
247 interface_id: Some(*interface_id),
248 })
249 }
250}
251
252pub struct ImportedInterface<'a> {
253 package_name: Option<&'a PackageName>,
254 name: String,
255 functions: Vec<(&'a str, &'a Function)>,
256 interface: Option<&'a Interface>,
257 interface_id: Option<InterfaceId>,
258}
259
260impl<'a> ImportedInterface<'a> {
261 pub fn module_name(&self) -> anyhow::Result<String> {
262 let package_name = self
263 .package_name
264 .ok_or_else(|| anyhow!("imported interface has no package name"))?;
265 let interface_name = &self.name;
266
267 Ok(format!(
268 "{}_{}",
269 package_name.to_string().to_snake_case(),
270 interface_name.to_snake_case()
271 ))
272 }
273
274 pub fn rust_interface_name(&self) -> Ident {
275 let interface_name = format!("Js{}Module", self.name.to_upper_camel_case());
276 Ident::new(&interface_name, Span::call_site())
277 }
278
279 pub fn name_and_interface(&self) -> Option<(&str, &Interface)> {
280 self.interface
281 .map(|interface| (self.name.as_str(), interface))
282 }
283
284 pub fn fully_qualified_interface_name(&self) -> String {
285 if let Some(package_name) = &self.package_name {
286 package_name.interface_id(&self.name)
287 } else {
288 self.name.clone()
289 }
290 }
291
292 pub fn interface_stack(&self) -> VecDeque<InterfaceId> {
293 self.interface_id.iter().cloned().collect()
294 }
295}
296
297fn copy_wit_directory(wit: &Utf8Path, output: &Utf8Path) -> anyhow::Result<()> {
299 fs_extra::dir::create(output, true)
300 .context("Failed to create and erase output WIT directory")?;
301 fs_extra::dir::copy(wit, output, &CopyOptions::new().content_only(true))
302 .context("Failed to copy WIT directory")?;
303
304 Ok(())
305}
306
307fn copy_js_modules(js_modules: &[JsModuleSpec], output: &Utf8Path) -> anyhow::Result<()> {
309 for module in js_modules {
310 if let EmbeddingMode::EmbedFile(source) = &module.mode {
311 let filename = module.file_name();
312 let js_dest = output.join("src").join(filename);
313 std::fs::copy(source, js_dest)
314 .context(format!("Failed to copy JavaScript module {}", module.name))?;
315 }
316 }
317 Ok(())
318}
319
320fn uses_composition(js_module_spec: &[JsModuleSpec]) -> bool {
322 js_module_spec
323 .iter()
324 .any(|m| matches!(m.mode, EmbeddingMode::Composition))
325}