1use std::collections::HashSet;
2
3use anyhow::{Context as _, Result, anyhow, bail, ensure};
4use heck::ToShoutySnakeCase;
5use ts_bindgen::ts_bindgen;
6use wasmtime_environ::component::{CanonicalOptions, ComponentTypesBuilder, StaticModuleIndex};
7use wasmtime_environ::wasmparser::WasmFeatures;
8use wasmtime_environ::{PrimaryMap, ScopeVec, Tunables};
9use wit_bindgen_core::wit_parser::Function;
10use wit_component::DecodedWasm;
11use wit_parser::{Package, Resolve, Stability, Type, TypeDefKind, TypeId, WorldId};
12
13mod core;
14mod files;
15mod transpile_bindgen;
16mod ts_bindgen;
17
18pub mod esm_bindgen;
19pub mod function_bindgen;
20pub mod names;
21pub mod source;
22
23pub mod intrinsics;
24use intrinsics::Intrinsic;
25
26use transpile_bindgen::transpile_bindgen;
27pub use transpile_bindgen::{
28 AsyncMode, BindingsMode, ExportKind, InstantiationMode, TranspileOpts,
29};
30
31fn enum_case_name(name: &str, screaming_snake_case: bool) -> String {
32 if screaming_snake_case {
33 name.to_shouty_snake_case()
34 } else {
35 name.to_string()
36 }
37}
38
39#[macro_export]
46macro_rules! uwrite {
47 ($dst:expr, $($arg:tt)*) => {
48 write!($dst, $($arg)*).unwrap()
49 };
50}
51
52#[macro_export]
59macro_rules! uwriteln {
60 ($dst:expr, $($arg:tt)*) => {
61 writeln!($dst, $($arg)*).unwrap()
62 };
63}
64
65pub struct Transpiled {
66 pub files: Vec<(String, Vec<u8>)>,
67 pub imports: Vec<String>,
68 pub exports: Vec<(String, transpile_bindgen::ExportKind)>,
69}
70
71pub struct ComponentInfo {
72 pub imports: Vec<String>,
73 pub exports: Vec<(String, transpile_bindgen::ExportKind)>,
74}
75
76pub fn generate_types(
77 name: &str,
78 resolve: Resolve,
79 world_id: WorldId,
80 mut opts: TranspileOpts,
81) -> Result<Vec<(String, Vec<u8>)>> {
82 normalize_namespace_object_options(&mut opts)?;
83 let mut files = files::Files::default();
84
85 ts_bindgen(name, &resolve, world_id, &opts, &mut files)
86 .context("failed to generate Typescript bindings")?;
87
88 let mut files_out: Vec<(String, Vec<u8>)> = Vec::new();
89 for (name, source) in files.iter() {
90 files_out.push((name.to_string(), source.to_vec()));
91 }
92 Ok(files_out)
93}
94
95#[cfg(feature = "transpile-bindgen")]
98pub fn transpile(component: &[u8], mut opts: TranspileOpts) -> Result<Transpiled> {
99 use wasmtime_environ::component::{Component, Translator};
100
101 normalize_namespace_object_options(&mut opts)?;
102 let name = opts.name.clone();
103 let mut files = files::Files::default();
104
105 let decoded = wit_component::decode(component)
112 .context("failed to extract interface information from component")?;
113
114 let (resolve, world_id) = match decoded {
115 DecodedWasm::WitPackage(_, _) => bail!("unexpected wit package as input"),
116 DecodedWasm::Component(resolve, world_id) => (resolve, world_id),
117 };
118
119 let scope = ScopeVec::new();
134 let tunables = Tunables::default_u32();
135
136 let mut features = WasmFeatures::WASM3
142 | WasmFeatures::WIDE_ARITHMETIC
143 | WasmFeatures::COMPONENT_MODEL
144 | WasmFeatures::CM_ASYNC
145 | WasmFeatures::CM_MORE_ASYNC_BUILTINS
146 | WasmFeatures::CM_ASYNC_STACKFUL
147 | WasmFeatures::CM_ERROR_CONTEXT
148 | WasmFeatures::CM_FIXED_LENGTH_LISTS
149 | WasmFeatures::CM_MAP
150 | WasmFeatures::CM_IMPLEMENTS;
151
152 if !opts.supports_wasm_exnref {
158 features = features.difference(WasmFeatures::EXCEPTIONS);
159 }
160
161 let mut validator = wasmtime_environ::wasmparser::Validator::new_with_features(features);
162
163 let mut types = ComponentTypesBuilder::new(&validator);
164
165 let (component, modules) = Translator::new(&tunables, &mut validator, &mut types, &scope)
166 .translate(component)
167 .map_err(|e| anyhow!(e).context("failed to translate component"))?;
168
169 let modules: PrimaryMap<StaticModuleIndex, core::Translation<'_>> = modules
170 .into_iter()
171 .map(|(_i, module)| core::Translation::new(module, opts.multi_memory))
172 .collect::<Result<_>>()?;
173
174 let wasmtime_component = Component::default();
175 let types = types.finish(&wasmtime_component);
176
177 for (i, module) in modules.iter() {
180 files.push(&core_file_name(&name, i.as_u32()), module.wasm());
181 }
182
183 if !opts.no_typescript {
184 ts_bindgen(&name, &resolve, world_id, &opts, &mut files)
185 .context("failed to generate Typescript bindings")?;
186 }
187
188 let (imports, exports) = transpile_bindgen(
189 &name, &component, &modules, &types.0, &resolve, world_id, opts, &mut files,
190 );
191
192 let mut files_out: Vec<(String, Vec<u8>)> = Vec::new();
193 for (name, source) in files.iter() {
194 files_out.push((name.to_string(), source.to_vec()));
195 }
196 Ok(Transpiled {
197 files: files_out,
198 imports,
199 exports,
200 })
201}
202
203fn normalize_namespace_object_options(opts: &mut TranspileOpts) -> Result<()> {
204 ensure!(
205 !(opts.use_namespace_objects && opts.variants_inline_cases),
206 "useNamespaceObjects cannot be combined with variantsInlineCases"
207 );
208 if opts.use_namespace_objects {
209 opts.flags_as_bigint = true;
210 }
211 Ok(())
212}
213
214fn core_file_name(name: &str, idx: u32) -> String {
215 let i_str = if idx == 0 {
216 String::from("")
217 } else {
218 (idx + 1).to_string()
219 };
220 format!("{name}.core{i_str}.wasm")
221}
222
223pub fn dealias(resolve: &Resolve, mut id: TypeId) -> TypeId {
224 loop {
225 match &resolve.types[id].kind {
226 TypeDefKind::Type(Type::Id(that_id)) => id = *that_id,
227 _ => break id,
228 }
229 }
230}
231
232fn feature_gate_allowed(
235 resolve: &Resolve,
236 package: &Package,
237 stability: &Stability,
238 item_name: &str,
239) -> Result<bool> {
240 Ok(match stability {
241 Stability::Unknown => true,
242 Stability::Stable { since, .. } => {
243 let Some(package_version) = package.name.version.as_ref() else {
244 return Ok(true);
247 };
248
249 ensure!(
250 package_version >= since,
251 "feature gate on [{item_name}] refers to an unreleased (future) package version [{since}] (current package version is [{package_version}])"
252 );
253
254 true
256 }
257 Stability::Unstable {
258 feature,
259 deprecated: _,
260 } => {
261 resolve.all_features || resolve.features.contains(feature)
264 }
265 })
266}
267
268pub fn get_thrown_type(
270 resolve: &Resolve,
271 return_type: Option<Type>,
272) -> Option<(Option<&Type>, Option<&Type>)> {
273 match return_type {
274 None => None,
275 Some(Type::Id(id)) => match &resolve.types[id].kind {
276 TypeDefKind::Result(r) => Some((r.ok.as_ref(), r.err.as_ref())),
277 _ => None,
278 },
279 _ => None,
280 }
281}
282
283pub(crate) fn is_async_fn(func: &Function, canon_opts: &CanonicalOptions) -> bool {
292 if canon_opts.async_ {
293 return true;
294 }
295 func.kind.is_async()
296}
297
298enum FunctionIdentifier<'a> {
300 Fn(&'a Function),
301 CanonFnName(&'a str),
302}
303
304pub(crate) fn requires_async_porcelain(
320 func: FunctionIdentifier<'_>,
321 id: &str,
322 async_funcs: &HashSet<String>,
323) -> bool {
324 let name = match func {
325 FunctionIdentifier::Fn(func) => func.name.as_str(),
326 FunctionIdentifier::CanonFnName(name) => name,
327 }
328 .trim_start_matches("[async]");
329
330 if async_funcs.contains(name) {
331 return true;
332 }
333
334 let qualified_name = format!("{id}#{name}");
335 if async_funcs.contains(&qualified_name) {
336 return true;
337 }
338
339 if let Some(pos) = id.find('@') {
340 let namespace = &id[..pos];
341 let namespaced_name = format!("{namespace}#{name}");
342
343 if async_funcs.contains(&namespaced_name) {
344 return true;
345 }
346 }
347 false
348}
349
350trait ManagesIntrinsics {
352 fn add_intrinsic(&mut self, intrinsic: Intrinsic);
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359
360 const FLAGS_WIT: &str = r#"
361 package test:flag-values;
362
363 interface api {
364 flags permissions {
365 read,
366 write,
367 }
368 roundtrip: func(value: permissions) -> permissions;
369 }
370
371 world flag-values {
372 export api;
373 }
374 "#;
375
376 fn flags_interface(flags_as_bigint: bool) -> String {
377 let mut resolve = Resolve::default();
378 let package = resolve.push_str("flags.wit", FLAGS_WIT).unwrap();
379 let world = resolve
380 .select_world(&[package], Some("flag-values"))
381 .unwrap();
382 let files = generate_types(
383 "flags",
384 resolve,
385 world,
386 TranspileOpts::builder()
387 .name("flags".into())
388 .flags_as_bigint(flags_as_bigint)
389 .build(),
390 )
391 .unwrap();
392 let (_, contents) = files
393 .into_iter()
394 .find(|(name, _)| name.ends_with("api.d.ts"))
395 .unwrap();
396 String::from_utf8(contents).unwrap()
397 }
398
399 #[test]
400 fn flags_types_are_objects_by_default() {
401 let source = flags_interface(false);
402 assert!(source.contains("export interface Permissions"));
403 assert!(!source.contains("export const Permissions"));
404 }
405
406 #[test]
407 fn flags_types_can_be_bigints_with_constants() {
408 let source = flags_interface(true);
409 assert!(source.contains("export type Permissions = bigint;"));
410 assert!(source.contains("export const Permissions: {"));
411 assert!(source.contains("readonly Read: bigint"));
412 assert!(source.contains("readonly Write: bigint"));
413 }
414}