//! WASM (wasm-bindgen) backend: orchestration and `Backend` trait implementation.
mod cfg;
pub mod enums;
pub mod errors;
pub mod functions;
pub mod methods;
pub mod service_api;
pub mod types;
mod cargo;
use crate::backends::wasm::type_map::WasmMapper;
use crate::codegen::builder::RustFileBuilder;
use crate::codegen::generators;
use crate::core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile};
use crate::core::config::{Language, ResolvedCrateConfig, resolve_output_dir};
use crate::core::ir::{ApiSurface, TypeRef};
use ahash::AHashSet;
use std::path::PathBuf;
use cargo::gen_cargo_toml;
use cfg::{
cfg_condition_enabled, collect_cfg_features, field_references_excluded_type, is_gated_behind_disabled_feature,
};
use enums::gen_enum;
use errors::{gen_error_converter, gen_error_methods};
use functions::{gen_env_shims, gen_function_with_emitted_dtos};
use types::{
filter_cfg_fields_for_features, gen_opaque_struct, gen_opaque_struct_methods, gen_struct, gen_struct_methods,
};
/// Prepend `#[cfg(<pred>)]` to a code item when the source symbol carries a cfg predicate.
fn prepend_cfg(cfg: Option<&str>, item: String) -> String {
match cfg {
Some(pred) if !pred.is_empty() => format!("#[cfg({pred})]\n{item}"),
_ => item,
}
}
pub struct WasmBackend;
impl Backend for WasmBackend {
fn name(&self) -> &str {
"wasm"
}
fn language(&self) -> Language {
Language::Wasm
}
fn capabilities(&self) -> Capabilities {
Capabilities {
supports_async: true,
supports_classes: true,
supports_enums: true,
supports_option: true,
supports_result: true,
..Capabilities::default()
}
}
fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
// wrapper delegates to the core crate (which resolves the cfg) and emits no `#[cfg]` gate,
// so two same-named entries would otherwise produce duplicate `#[wasm_bindgen]` fns.
let deduped_api = api.with_deduped_functions();
let api = &deduped_api;
let wasm_config = config.wasm.as_ref();
let mut exclude_functions = wasm_config.map(|c| c.exclude_functions.clone()).unwrap_or_default();
let mut exclude_types = wasm_config.map(|c| c.exclude_types.clone()).unwrap_or_default();
// Simple newtype opaques (no generics in the path) DO wrap as `#[wasm_bindgen]` classes
exclude_types.extend(
config
.opaque_types
.iter()
.filter(|(_, path)| path.contains('<'))
.map(|(name, _)| name.clone()),
);
let text_field_enum_names: AHashSet<String> = config.untagged_union_text_types.iter().cloned().collect();
let mut type_overrides = wasm_config.map(|c| c.type_overrides.clone()).unwrap_or_default();
for name in &text_field_enum_names {
type_overrides
.entry(name.clone())
.or_insert_with(|| "String".to_string());
}
let env_shims = wasm_config.map(|c| c.env_shims.clone()).unwrap_or_default();
let prefix = config.wasm_type_prefix();
let enabled_features = config.features_for_language(Language::Wasm).to_vec();
for typ in &api.types {
if is_gated_behind_disabled_feature(&typ.cfg, &enabled_features) {
exclude_types.push(typ.name.clone());
}
}
for enum_def in &api.enums {
if is_gated_behind_disabled_feature(&enum_def.cfg, &enabled_features) {
exclude_types.push(enum_def.name.clone());
}
}
for func in &api.functions {
if is_gated_behind_disabled_feature(&func.cfg, &enabled_features) {
exclude_functions.push(func.name.clone());
}
}
let mapper = WasmMapper::new(type_overrides, prefix.clone());
let core_import = config.core_import_for_language(Language::Wasm);
let source_remap_pairs: Vec<(String, String)> = wasm_config
.map(|c| c.source_crate_remaps.clone())
.unwrap_or_default()
.into_iter()
.map(|orig| (orig.replace('-', "_"), core_import.clone()))
.collect();
let source_remaps_borrowed: Vec<(&str, &str)> = source_remap_pairs
.iter()
.map(|(o, n)| (o.as_str(), n.as_str()))
.collect();
let dropped_crates: AHashSet<String> = wasm_config
.map(|c| c.exclude_extra_dependencies.clone())
.unwrap_or_default()
.into_iter()
.map(|name| name.replace('-', "_"))
.filter(|underscored| {
underscored != &core_import && !source_remap_pairs.iter().any(|(orig, _)| orig == underscored)
})
.collect();
for typ in &api.types {
let crate_seg = typ.rust_path.split("::").next().unwrap_or("").replace('-', "_");
if dropped_crates.contains(&crate_seg) && !exclude_types.contains(&typ.name) {
exclude_types.push(typ.name.clone());
}
}
for enum_def in &api.enums {
let crate_seg = enum_def.rust_path.split("::").next().unwrap_or("").replace('-', "_");
if dropped_crates.contains(&crate_seg) && !exclude_types.contains(&enum_def.name) {
exclude_types.push(enum_def.name.clone());
}
}
for func in &api.functions {
let crate_seg = func.rust_path.split("::").next().unwrap_or("").replace('-', "_");
if dropped_crates.contains(&crate_seg) && !exclude_functions.contains(&func.name) {
exclude_functions.push(func.name.clone());
}
}
let dropped_error_names: Vec<String> = api
.errors
.iter()
.filter(|e| {
let crate_seg = e.rust_path.split("::").next().unwrap_or("").replace('-', "_");
dropped_crates.contains(&crate_seg)
})
.map(|e| e.name.clone())
.collect();
for name in dropped_error_names {
if !exclude_types.contains(&name) {
exclude_types.push(name);
}
}
// is treated as if it were `#[cfg]`-gated, so the binding struct omits it and
let exclude_fields_map = wasm_config.map(|c| c.exclude_fields.clone()).unwrap_or_default();
let api_owned;
let api: &ApiSurface = if exclude_fields_map.is_empty() {
api
} else {
api_owned = {
let mut cloned = api.clone();
for typ in &mut cloned.types {
if let Some(skip_list) = exclude_fields_map.get(&typ.name) {
let before = typ.fields.len();
typ.fields.retain(|field| !skip_list.iter().any(|s| s == &field.name));
if typ.fields.len() != before {
typ.has_stripped_cfg_fields = true;
}
}
}
cloned
};
&api_owned
};
let cfg_filtered_api = filter_cfg_fields_for_features(api, &enabled_features);
let api = &cfg_filtered_api;
let mut builder = RustFileBuilder::new().with_generated_header();
builder.add_inner_attribute(
"allow(dead_code, unused_imports, unused_variables, unreachable_patterns, missing_docs)",
);
builder.add_inner_attribute("allow(clippy::too_many_arguments, clippy::let_unit_value, clippy::needless_borrow, clippy::map_identity, clippy::just_underscores_and_digits, clippy::unused_unit, clippy::unnecessary_cast, clippy::unwrap_or_default, clippy::derivable_impls, clippy::redundant_field_names, clippy::needless_borrows_for_generic_args, clippy::unnecessary_fallible_conversions, clippy::useless_conversion, clippy::arc_with_non_send_sync, clippy::collapsible_if, clippy::clone_on_copy, clippy::should_implement_trait, clippy::await_holding_refcell_ref, clippy::new_without_default)");
if let Some(extra_attr) = crate::codegen::shared::format_extra_clippy_allows(
&config.extra_clippy_allows,
builder.inner_attributes_text(),
) {
builder.add_inner_attribute(&extra_attr);
}
builder.add_import("wasm_bindgen::prelude::*");
if let Some(modules) = wasm_config.map(|c| c.custom_rust_modules.as_slice()) {
for module in modules {
builder.add_item(&format!("pub mod {module};"));
builder.add_item(&format!("pub use {module}::*;"));
}
}
// so no explicit `use js_sys;` import is needed (clippy::single_component_path_imports).
for trait_path in generators::collect_trait_imports(api) {
builder.add_import(&trait_path);
}
if !env_shims.is_empty() {
builder.add_item(&gen_env_shims(&env_shims));
}
let opaque_types: AHashSet<String> = api
.types
.iter()
.filter(|t| t.is_opaque && !exclude_types.contains(&t.name))
.map(|t| t.name.clone())
.collect();
let mutex_types: AHashSet<String> = api
.types
.iter()
.filter(|t| t.is_opaque && !exclude_types.contains(&t.name) && generators::type_needs_mutex(t))
.map(|t| t.name.clone())
.collect();
if !opaque_types.is_empty() {
builder.add_import("std::sync::Arc");
if !mutex_types.is_empty() {
builder.add_import("std::sync::Mutex");
}
}
let bridge_type_aliases: AHashSet<String> = config
.trait_bridges
.iter()
.filter_map(|b| b.type_alias.clone())
.collect();
let mut opaque_names_vec: Vec<String> = opaque_types.iter().cloned().collect();
opaque_names_vec.extend(bridge_type_aliases.iter().cloned());
let opaque_names_set: AHashSet<String> = opaque_names_vec.iter().cloned().collect();
let adapter_bodies = crate::adapters::build_adapter_bodies(config, Language::Wasm)?;
let streaming_item_types: ahash::AHashMap<String, String> = config
.adapters
.iter()
.filter(|a| matches!(a.pattern, crate::core::config::AdapterPattern::Streaming))
.filter(|a| !a.skip_languages.iter().any(|l| l == "wasm"))
.filter_map(|a| {
let owner = a.owner_type.as_deref()?;
let item = a.item_type.as_deref()?;
Some((format!("{owner}.{}", a.name), item.to_string()))
})
.collect();
let wasm_skipped_methods: AHashSet<String> = config
.adapters
.iter()
.filter(|a| matches!(a.pattern, crate::core::config::AdapterPattern::Streaming))
.filter(|a| a.skip_languages.iter().any(|l| l == "wasm"))
.filter_map(|a| {
let owner = a.owner_type.as_deref()?;
Some(format!("{owner}.{}", a.name))
})
.collect();
for adapter in &config.adapters {
match adapter.pattern {
crate::core::config::AdapterPattern::Streaming => {
let key = crate::adapters::stream_struct_key(adapter);
if let Some(struct_code) = adapter_bodies.get(&key) {
builder.add_item(struct_code);
}
}
crate::core::config::AdapterPattern::CallbackBridge => {
let struct_key = format!("{}.__bridge_struct__", adapter.name);
let impl_key = format!("{}.__bridge_impl__", adapter.name);
if let Some(struct_code) = adapter_bodies.get(&struct_key) {
builder.add_item(struct_code);
}
if let Some(impl_code) = adapter_bodies.get(&impl_key) {
builder.add_item(impl_code);
}
}
_ => {}
}
}
// `#[wasm_bindgen]` entrypoints (e.g. `app_run`) are compiled and exported.
let has_wasm_services = api.services.iter().any(|svc| {
!config
.services
.iter()
.any(|sc| sc.owner_type == svc.name && sc.skip_languages.iter().any(|l| l == "wasm"))
});
if has_wasm_services {
builder.add_item("pub mod service;");
}
let tagged_data_enum_names: AHashSet<String> = api
.enums
.iter()
.filter(|e| !exclude_types.contains(&e.name) && enums::is_tagged_data_enum(e))
.map(|e| e.name.clone())
.collect();
let methods_enums: Vec<_> = api
.enums
.iter()
.filter(|e| !text_field_enum_names.contains(&e.name))
.cloned()
.collect();
let core_to_binding_convertible_for_structs =
crate::codegen::conversions::core_to_binding_convertible_types(api, &exclude_types);
for typ in api.types.iter().filter(|typ| !typ.is_trait) {
if exclude_types.contains(&typ.name) {
continue;
}
if typ.is_opaque {
builder.add_item(&gen_opaque_struct(typ, &core_import, &prefix));
builder.add_item(&gen_opaque_struct_methods(
typ,
&mapper,
&opaque_types,
&core_import,
&prefix,
&adapter_bodies,
&mutex_types,
&streaming_item_types,
&wasm_skipped_methods,
&config.trait_bridges,
));
// Client constructor — emit a #[wasm_bindgen(constructor)] impl
if let Some(ctor) = config.client_constructors.get(&typ.name) {
let struct_name = format!("{prefix}{}", typ.name);
let ctor_body = generators::gen_opaque_constructor(
ctor,
&typ.name,
&core_import,
"#[wasm_bindgen(constructor)]",
);
let ctor_impl = format!("#[wasm_bindgen]\nimpl {struct_name} {{\n{}}}", ctor_body);
builder.add_item(&ctor_impl);
}
} else {
let is_core_to_binding_convertible = core_to_binding_convertible_for_structs.contains(&typ.name);
// gen_struct gates #[derive(Default)] and the delegating Default impl on
builder.add_item(&gen_struct(
typ,
&mapper,
&exclude_types,
&core_import,
&prefix,
&tagged_data_enum_names,
&source_remaps_borrowed,
is_core_to_binding_convertible,
));
builder.add_item(&gen_struct_methods(
typ,
&mapper,
&exclude_types,
&core_import,
&opaque_types,
&methods_enums,
&prefix,
&mutex_types,
&streaming_item_types,
));
}
}
for enum_def in &api.enums {
if !exclude_types.contains(&enum_def.name) {
builder.add_item(&gen_enum(enum_def, &prefix));
}
}
let mut emitted_input_dtos = AHashSet::new();
let mut input_dto_code = String::new();
for func in &api.functions {
if !exclude_functions.contains(&func.name)
&& !crate::codegen::generators::trait_bridge::is_trait_bridge_managed_fn(
&func.name,
&config.trait_bridges,
)
{
let refs_excluded = func
.params
.iter()
.any(|p| field_references_excluded_type(&p.ty, &exclude_types))
|| field_references_excluded_type(&func.return_type, &exclude_types);
if !refs_excluded {
for p in &func.params {
if let TypeRef::Named(name) = &p.ty {
if !opaque_types.contains(name.as_str())
&& !emitted_input_dtos.contains(name.as_str())
&& let Some(type_def) = api.types.iter().find(|t| t.name == name.as_str())
&& functions::should_have_input_dto(type_def)
{
let non_deserializable_type_names: std::collections::HashSet<String> = api
.types
.iter()
.filter(|t| !t.has_serde || t.is_trait || t.is_opaque)
.map(|t| t.name.clone())
.collect();
let (dto_code, _dto_name) = functions::gen_input_dto_for_type_with_cfg(
name.as_str(),
&core_import,
type_def,
&exclude_types,
&enabled_features,
&non_deserializable_type_names,
);
if !dto_code.is_empty() {
input_dto_code.push_str(&dto_code);
input_dto_code.push_str("\n\n");
emitted_input_dtos.insert(name.clone());
}
}
}
}
}
}
}
if !input_dto_code.is_empty() {
builder.add_item(&input_dto_code);
}
for func in &api.functions {
if !exclude_functions.contains(&func.name) {
if crate::codegen::generators::trait_bridge::is_trait_bridge_managed_fn(
&func.name,
&config.trait_bridges,
) {
continue;
}
let refs_excluded = func
.params
.iter()
.any(|p| field_references_excluded_type(&p.ty, &exclude_types))
|| field_references_excluded_type(&func.return_type, &exclude_types);
if refs_excluded {
continue;
}
let bridge_param = crate::backends::wasm::trait_bridge::find_bridge_param(func, &config.trait_bridges);
let options_field_bridge =
crate::backends::wasm::trait_bridge::find_options_field_binding(func, &config.trait_bridges)
.filter(|(_, bridge_cfg)| {
let Some(field_name) = bridge_cfg.resolved_options_field() else {
return false;
};
let Some(options_type) = bridge_cfg.options_type.as_deref() else {
return false;
};
api.types
.iter()
.filter(|t| t.name == options_type)
.flat_map(|t| t.fields.iter())
.any(|f| f.cfg.is_none() && f.name == field_name)
});
if let Some((param_idx, bridge_cfg)) = bridge_param {
let item = crate::backends::wasm::trait_bridge::gen_bridge_function(
api,
func,
param_idx,
bridge_cfg,
&mapper,
&opaque_types,
&core_import,
&prefix,
);
let item = prepend_cfg(func.cfg.as_deref(), item);
builder.add_item(&item);
} else if let Some((param_idx, bridge_cfg)) = options_field_bridge {
let item = crate::backends::wasm::trait_bridge::gen_options_field_bridge_function(
api,
func,
param_idx,
bridge_cfg,
&mapper,
&opaque_types,
&core_import,
&prefix,
);
let item = prepend_cfg(func.cfg.as_deref(), item);
builder.add_item(&item);
} else {
let item = gen_function_with_emitted_dtos(
func,
&mapper,
&core_import,
&opaque_types,
&prefix,
&mutex_types,
api,
&emitted_input_dtos,
);
let item = prepend_cfg(func.cfg.as_deref(), item);
builder.add_item(&item);
}
}
}
for bridge_cfg in &config.trait_bridges {
if let Some(trait_type) = api.types.iter().find(|t| t.is_trait && t.name == bridge_cfg.trait_name) {
let bridge = crate::backends::wasm::trait_bridge::gen_trait_bridge(
trait_type,
bridge_cfg,
&core_import,
&config.error_type_name(),
&config.error_constructor_expr(),
api,
)?;
for imp in &bridge.imports {
builder.add_import(imp);
}
builder.add_item(&bridge.code);
}
}
let trait_bridge_arc_wrapper_field_names: Vec<String> = config
.trait_bridges
.iter()
.filter(|b| b.bind_via == crate::core::config::BridgeBinding::OptionsField)
.filter_map(|b| b.resolved_options_field().map(String::from))
.collect();
let wasm_conv_config = crate::codegen::conversions::ConversionConfig {
type_name_prefix: &prefix,
map_uses_jsvalue: true,
option_duration_on_defaults: true,
optionalize_defaults: false,
exclude_types: &exclude_types,
source_crate_remaps: &source_remaps_borrowed,
opaque_types: if opaque_names_set.is_empty() {
None
} else {
Some(&opaque_names_set)
},
trait_bridge_arc_wrapper_field_names: &trait_bridge_arc_wrapper_field_names,
tagged_data_enum_names: if tagged_data_enum_names.is_empty() {
None
} else {
Some(&tagged_data_enum_names)
},
text_field_enum_names: if text_field_enum_names.is_empty() {
None
} else {
Some(&text_field_enum_names)
},
..Default::default()
};
let convertible = crate::codegen::conversions::convertible_types(api);
let core_to_binding_convertible =
crate::codegen::conversions::core_to_binding_convertible_types(api, &exclude_types);
let input_types = crate::codegen::conversions::input_type_names(api);
for typ in api.types.iter().filter(|typ| !typ.is_trait) {
if exclude_types.contains(&typ.name) {
continue;
}
let is_strict = crate::codegen::conversions::can_generate_conversion(typ, &convertible);
let is_relaxed = crate::codegen::conversions::can_generate_conversion(typ, &core_to_binding_convertible);
if is_strict {
if input_types.contains(&typ.name) {
builder.add_item(&crate::codegen::conversions::gen_from_binding_to_core_cfg(
typ,
&core_import,
&wasm_conv_config,
));
}
builder.add_item(&crate::codegen::conversions::gen_from_core_to_binding_cfg(
typ,
&core_import,
&opaque_types,
&wasm_conv_config,
));
} else if is_relaxed {
builder.add_item(&crate::codegen::conversions::gen_from_core_to_binding_cfg(
typ,
&core_import,
&opaque_types,
&wasm_conv_config,
));
}
}
for e in &api.enums {
if !exclude_types.contains(&e.name) {
if enums::is_tagged_data_enum(e) {
if input_types.contains(&e.name) {
builder.add_item(&enums::gen_tagged_enum_binding_to_core(e, &core_import, &prefix));
}
builder.add_item(&enums::gen_tagged_enum_core_to_binding(e, &core_import, &prefix));
} else {
if input_types.contains(&e.name) && crate::codegen::conversions::can_generate_enum_conversion(e) {
builder.add_item(&crate::codegen::conversions::gen_enum_from_binding_to_core_cfg(
e,
&core_import,
&wasm_conv_config,
));
}
if crate::codegen::conversions::can_generate_enum_conversion_from_core(e) {
builder.add_item(&crate::codegen::conversions::gen_enum_from_core_to_binding_cfg(
e,
&core_import,
&wasm_conv_config,
));
}
}
}
}
for error in &api.errors {
if exclude_types.contains(&error.name) {
continue;
}
builder.add_item(&gen_error_converter(error, &core_import, &source_remaps_borrowed));
let methods_block = gen_error_methods(error, &core_import, &prefix);
if !methods_block.is_empty() {
builder.add_item(&methods_block);
}
}
let mut content = builder.build();
for bridge in &config.trait_bridges {
if let Some(field_name) = bridge.resolved_options_field() {
let param_name = bridge.param_name.as_deref().unwrap_or(field_name);
let pattern = format!(".{}({}.as_ref().map(|v| &v.inner))", field_name, param_name);
let replacement = format!(".{}(None)", field_name);
content = content.replace(&pattern, &replacement);
}
}
for bridge in &config.trait_bridges {
if bridge.bind_via != crate::core::config::BridgeBinding::OptionsField {
continue;
}
let (Some(options_type), Some(field_name)) =
(bridge.options_type.as_deref(), bridge.resolved_options_field())
else {
continue;
};
for variant in ["", "Update"] {
let binding_name = format!("Wasm{options_type}{variant}");
let core_path = format!("{core_import}::options::{options_type}{variant}");
let impl_header = format!("impl From<{binding_name}> for {core_path} {{");
if !content.contains(&impl_header) {
continue;
}
let patterns = &[
(" ", "\n "),
(" ", "\n "),
(" ", "\n "),
];
for (indent, newline_indent) in patterns {
let old_pattern =
format!("{indent}{field_name}: Default::default(),{newline_indent}..Default::default()");
let new_pattern = format!(
"{indent}{field_name}: val.{field_name}.map(|v| (*v.inner).clone()),{newline_indent}..Default::default()"
);
if content.contains(&old_pattern) {
content = content.replace(&old_pattern, &new_pattern);
}
}
}
}
let output_dir = resolve_output_dir(config.output_paths.get("wasm"), &config.name, "crates/{name}-wasm/src/");
let cargo_toml_path = PathBuf::from(&output_dir)
.parent()
.map(|p| p.join("Cargo.toml"))
.unwrap_or_else(|| PathBuf::from("Cargo.toml"));
Ok(vec![
GeneratedFile {
path: PathBuf::from(&output_dir).join("lib.rs"),
content,
generated_header: false,
},
GeneratedFile {
path: cargo_toml_path,
content: gen_cargo_toml(api, config),
generated_header: true,
},
])
}
fn generate_service_api(
&self,
api: &ApiSurface,
config: &ResolvedCrateConfig,
) -> anyhow::Result<Vec<GeneratedFile>> {
Ok(service_api::gen_service_files(api, config))
}
fn build_config(&self) -> Option<BuildConfig> {
Some(BuildConfig {
tool: "wasm-pack",
crate_suffix: "-wasm",
build_dep: BuildDependency::None,
post_build: vec![],
})
}
}
#[cfg(test)]
mod tests;