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