1use crate::conversions::generate_conversions;
2use crate::exports::generate_export_impls;
3use crate::imports::generate_import_modules;
4use crate::skeleton::{
5 copy_skeleton_lock, copy_skeleton_sources, generate_app_manifest, generate_cargo_toml,
6};
7use crate::wit::add_get_script_import;
8use anyhow::{Context, anyhow};
9use camino::{Utf8Path, Utf8PathBuf};
10use heck::{ToSnakeCase, ToUpperCamelCase};
11use proc_macro2::{Ident, Span};
12use std::cell::RefCell;
13use std::collections::{BTreeSet, VecDeque};
14use wit_parser::{
15 Function, Interface, InterfaceId, PackageId, PackageName, PackageSourceMap, Resolve, TypeDef,
16 TypeId, TypeOwner, WorldId, WorldItem,
17};
18
19mod conversions;
20mod exports;
21mod imports;
22mod javascript;
23mod rust_bindgen;
24mod skeleton;
25mod types;
26mod typescript;
27mod wit;
28
29pub(crate) fn write_if_changed(
32 path: impl AsRef<std::path::Path>,
33 contents: impl AsRef<[u8]>,
34) -> std::io::Result<()> {
35 let path = path.as_ref();
36 let contents = contents.as_ref();
37 if let Ok(existing) = std::fs::read(path)
38 && existing == contents
39 {
40 return Ok(());
41 }
42 std::fs::write(path, contents)
43}
44
45pub(crate) fn copy_if_changed(
47 src: impl AsRef<std::path::Path>,
48 dst: impl AsRef<std::path::Path>,
49) -> std::io::Result<()> {
50 let src = src.as_ref();
51 let dst = dst.as_ref();
52 let src_contents = std::fs::read(src)?;
53 if let Ok(existing) = std::fs::read(dst)
54 && existing == src_contents
55 {
56 return Ok(());
57 }
58 std::fs::write(dst, src_contents)
59}
60
61#[derive(Debug, Clone)]
63pub enum EmbeddingMode {
64 EmbedFile(Utf8PathBuf),
66 Composition,
68}
69
70#[derive(Debug, Clone)]
72pub struct JsModuleSpec {
73 pub name: String,
74 pub mode: EmbeddingMode,
75}
76
77impl JsModuleSpec {
78 pub fn file_name(&self) -> String {
79 self.name.replace('/', "_") + ".js"
80 }
81}
82
83pub fn generate_wrapper_crate(
97 wit: &Utf8Path,
98 js_modules: &[JsModuleSpec],
99 output: &Utf8Path,
100 world: Option<&str>,
101) -> anyhow::Result<()> {
102 std::fs::create_dir_all(output).context("Failed to create output directory")?;
104 std::fs::create_dir_all(output.join("src")).context("Failed to create output/src directory")?;
105 std::fs::create_dir_all(output.join("src").join("modules"))
106 .context("Failed to create output/src/modules directory")?;
107
108 let context = GeneratorContext::new(output, wit, world)?;
110
111 generate_cargo_toml(&context)?;
113
114 copy_skeleton_lock(context.output).context("Failed to copy skeleton Cargo.lock")?;
116
117 generate_app_manifest(&context)?;
119
120 copy_skeleton_sources(context.output).context("Failed to copy skeleton sources")?;
122
123 copy_wit_directory(wit, &context.output.join("wit"))
125 .context("Failed to copy WIT package to output directory")?;
126
127 if uses_composition(js_modules) {
128 add_get_script_import(&context.output.join("wit"), world)
129 .context("Failed to add get-script import to the WIT world")?;
130 }
131
132 copy_js_modules(js_modules, context.output)
134 .context("Failed to copy JavaScript module to output directory")?;
135
136 generate_export_impls(&context, js_modules)
138 .context("Failed to generate the component export implementations")?;
139
140 generate_import_modules(&context).context("Failed to generate the component import modules")?;
142
143 generate_conversions(&context)
146 .context("Failed to generate the IntoJs and FromJs typeclass instances")?;
147
148 Ok(())
149}
150
151pub fn generate_dts(
155 wit: &Utf8Path,
156 output: &Utf8Path,
157 world: Option<&str>,
158) -> anyhow::Result<Vec<Utf8PathBuf>> {
159 std::fs::create_dir_all(output).context("Failed to create output directory")?;
161
162 let context = GeneratorContext::new(output, wit, world)?;
164
165 let mut result = Vec::new();
166 result.extend(
167 typescript::generate_export_module(&context)
168 .context("Failed to generate the TypeScript module definition for the exports")?,
169 );
170
171 result.extend(typescript::generate_import_modules(&context).context(
173 "Failed to generate the TypeScript module definitions for the imported modules",
174 )?);
175
176 Ok(result)
177}
178
179struct GeneratorContext<'a> {
180 output: &'a Utf8Path,
181 wit_source_path: &'a Utf8Path,
182 resolve: Resolve,
183 root_package: PackageId,
184 world: WorldId,
185 source_map: PackageSourceMap,
186 visited_types: RefCell<BTreeSet<TypeId>>,
187 world_name: String,
188 types: wit_bindgen_core::Types,
189}
190
191impl<'a> GeneratorContext<'a> {
192 fn new(output: &'a Utf8Path, wit: &'a Utf8Path, world: Option<&str>) -> anyhow::Result<Self> {
193 let mut resolve = Resolve::default();
194 let (root_package, source_map) = resolve
195 .push_path(wit)
196 .context("Failed to resolve WIT package")?;
197 let world = resolve
198 .select_world(root_package, world)
199 .context("Failed to select WIT world")?;
200
201 let world_name = resolve.worlds[world].name.clone();
202
203 let mut types = wit_bindgen_core::Types::default();
204 types.analyze(&resolve);
205
206 Ok(Self {
207 output,
208 wit_source_path: wit,
209 resolve,
210 root_package,
211 world,
212 source_map,
213 visited_types: RefCell::new(BTreeSet::new()),
214 world_name,
215 types,
216 })
217 }
218
219 fn root_package_name(&self) -> String {
220 self.resolve.packages[self.root_package].name.to_string()
221 }
222
223 fn record_visited_type(&self, type_id: TypeId) {
224 self.visited_types.borrow_mut().insert(type_id);
225 }
226
227 fn is_exported_interface(&self, interface_id: InterfaceId) -> bool {
228 let world = &self.resolve.worlds[self.world];
229 world
230 .exports
231 .iter()
232 .any(|(_, item)| matches!(item, WorldItem::Interface { id, .. } if id == &interface_id))
233 }
234
235 fn is_exported_type(&self, type_id: TypeId) -> bool {
236 if let Some(typ) = self.resolve.types.get(type_id) {
237 match &typ.owner {
238 TypeOwner::World(world_id) => {
239 if world_id == &self.world {
240 let world = &self.resolve.worlds[self.world];
241 world
242 .exports
243 .iter()
244 .any(|(_, item)| matches!(item, WorldItem::Type(id) if id == &type_id))
245 } else {
246 false
247 }
248 }
249 TypeOwner::Interface(interface_id) => self.is_exported_interface(*interface_id),
250 TypeOwner::None => false,
251 }
252 } else {
253 false
254 }
255 }
256
257 fn bindgen_type_info(&self, type_id: TypeId) -> wit_bindgen_core::TypeInfo {
258 self.types.get(type_id)
259 }
260
261 fn get_imported_interface(
262 &self,
263 interface_id: &InterfaceId,
264 ) -> anyhow::Result<ImportedInterface<'_>> {
265 let interface = &self.resolve.interfaces[*interface_id];
266 let name = interface
267 .name
268 .as_ref()
269 .ok_or_else(|| anyhow!("Interface import does not have a name"))?
270 .as_str();
271
272 let functions = interface
273 .functions
274 .iter()
275 .map(|(name, f)| (name.as_str(), f))
276 .collect();
277
278 let package_id = interface
279 .package
280 .ok_or_else(|| anyhow!("Anonymous interface imports are not supported yet"))?;
281 let package = self
282 .resolve
283 .packages
284 .get(package_id)
285 .ok_or_else(|| anyhow!("Could not find package of imported interface {name}"))?;
286 let package_name = &package.name;
287
288 Ok(ImportedInterface {
289 package_name: Some(package_name),
290 name: name.to_string(),
291 functions,
292 interface: Some(interface),
293 interface_id: Some(*interface_id),
294 })
295 }
296
297 fn typ(&self, type_id: TypeId) -> anyhow::Result<&TypeDef> {
298 self.resolve
299 .types
300 .get(type_id)
301 .ok_or_else(|| anyhow!("Unknown type id: {type_id:?}"))
302 }
303}
304
305pub struct ImportedInterface<'a> {
306 package_name: Option<&'a PackageName>,
307 name: String,
308 functions: Vec<(&'a str, &'a Function)>,
309 interface: Option<&'a Interface>,
310 interface_id: Option<InterfaceId>,
311}
312
313impl<'a> ImportedInterface<'a> {
314 pub fn module_name(&self) -> anyhow::Result<String> {
315 let package_name = self
316 .package_name
317 .ok_or_else(|| anyhow!("imported interface has no package name"))?;
318 let interface_name = &self.name;
319
320 Ok(format!(
321 "{}_{}",
322 package_name.to_string().to_snake_case(),
323 interface_name.to_snake_case()
324 ))
325 }
326
327 pub fn rust_interface_name(&self) -> Ident {
328 let interface_name = format!("Js{}Module", self.name.to_upper_camel_case());
329 Ident::new(&interface_name, Span::call_site())
330 }
331
332 pub fn name_and_interface(&self) -> Option<(&str, &Interface)> {
333 self.interface
334 .map(|interface| (self.name.as_str(), interface))
335 }
336
337 pub fn fully_qualified_interface_name(&self) -> String {
338 if let Some(package_name) = &self.package_name {
339 package_name.interface_id(&self.name)
340 } else {
341 self.name.clone()
342 }
343 }
344
345 pub fn interface_stack(&self) -> VecDeque<InterfaceId> {
346 self.interface_id.iter().cloned().collect()
347 }
348}
349
350fn copy_wit_directory(wit: &Utf8Path, output: &Utf8Path) -> anyhow::Result<()> {
352 std::fs::create_dir_all(output)?;
353 copy_dir_if_changed(wit.as_std_path(), output.as_std_path())
354 .context("Failed to copy WIT directory")?;
355 Ok(())
356}
357
358fn copy_dir_if_changed(src: &std::path::Path, dst: &std::path::Path) -> std::io::Result<()> {
359 std::fs::create_dir_all(dst)?;
360 for entry in std::fs::read_dir(src)? {
361 let entry = entry?;
362 let src_path = entry.path();
363 let dst_path = dst.join(entry.file_name());
364 if src_path.is_dir() {
365 copy_dir_if_changed(&src_path, &dst_path)?;
366 } else {
367 copy_if_changed(&src_path, &dst_path)?;
368 }
369 }
370 Ok(())
371}
372
373fn copy_js_modules(js_modules: &[JsModuleSpec], output: &Utf8Path) -> anyhow::Result<()> {
375 for module in js_modules {
376 if let EmbeddingMode::EmbedFile(source) = &module.mode {
377 let filename = module.file_name();
378 let js_dest = output.join("src").join(filename);
379 copy_if_changed(source, js_dest)
380 .context(format!("Failed to copy JavaScript module {}", module.name))?;
381 }
382 }
383 Ok(())
384}
385
386fn uses_composition(js_module_spec: &[JsModuleSpec]) -> bool {
388 js_module_spec
389 .iter()
390 .any(|m| matches!(m.mode, EmbeddingMode::Composition))
391}