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