//! 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;
mod ts_union;
pub(crate) use ts_union::docs_ts_type_for_untagged_enum;
pub mod types;
mod cargo;
use crate::backends::wasm::type_map::WasmMapper;
use crate::codegen::builder::RustFileBuilder;
use crate::codegen::{generators, shared};
use crate::core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile, PostBuildStep};
use crate::core::config::{Language, ResolvedCrateConfig, resolve_output_layout};
use crate::core::ir::{ApiSurface, ReceiverKind, TypeRef};
use ahash::{AHashMap, AHashSet};
use regex::Regex;
use std::path::PathBuf;
use cargo::gen_cargo_toml;
/// The emitted wasm crate's directory layout.
///
/// One derivation shared by the file-path side (`lib.rs`, `Cargo.toml`) and the manifest's own
/// core-crate `path = "..."` dependency. When `gen_cargo_toml` hard-coded `../{core_crate_dir}`
/// instead, the two disagreed for any layout that is not a `crates/` sibling pair: the manifest
/// pointed at a `crates/<core>` that the emitted tree does not contain, and cargo failed to read
/// it before compiling anything. ~keep
pub(super) fn wasm_output_layout(config: &ResolvedCrateConfig) -> crate::core::config::OutputLayout {
resolve_output_layout(config.output_paths.get("wasm"), &config.name, "crates/{name}-wasm/src/")
}
use cfg::{
cfg_condition_enabled, collect_cfg_features, field_references_excluded_type, first_unknown_named_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,
};
/// Why a symbol is or is not callable from a WASM snippet.
///
/// `UnknownSymbol` stays distinct from `NotExported` because collapsing them misdirects the
/// reader: a typo'd `overrides.wasm.function` then reads as a capability gap in the target,
/// sending someone to audit the wasm backend for a name that was only ever misspelled in
/// config. That misdirection already cost one diagnostic cycle on this exact gate. ~keep
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum WasmCallability {
Callable,
/// A symbol by this name exists, but the target does not export it.
NotExported,
/// Nothing in the API surface or the bridge registry answers to this name under either the
/// Rust or the JavaScript spelling.
UnknownSymbol,
}
pub(crate) fn wasm_callability(
function_name: &str,
functions: &[crate::core::ir::FunctionDef],
config: &ResolvedCrateConfig,
) -> WasmCallability {
if function_is_callable(function_name, functions, config) {
return WasmCallability::Callable;
}
match rust_identity_for_wasm_symbol(function_name, functions, config) {
Some(_) => WasmCallability::NotExported,
None => WasmCallability::UnknownSymbol,
}
}
/// Whether a WASM *snippet* or *test* may call `function_name`.
///
/// Deliberately wider than [`function_is_exported`], which answers a codegen question — "should
/// the plain-function generator emit a wrapper for this?" — and returns `false` for trait-bridge
/// register/unregister/clear functions precisely because the trait-bridge generator emits them
/// instead. Those functions are exported all the same, so a caller asking "is this callable?"
/// must not reuse the codegen predicate.
///
/// Prefer [`wasm_callability`] when the answer feeds a diagnostic: this predicate cannot say
/// whether a `false` means the target dropped the function or the name resolved to nothing.
pub(crate) fn function_is_callable(
function_name: &str,
functions: &[crate::core::ir::FunctionDef],
config: &ResolvedCrateConfig,
) -> bool {
let Some(identity) = rust_identity_for_wasm_symbol(function_name, functions, config) else {
return false;
};
// An explicit exclusion outranks the bridge: the symbol is not emitted at all.
if config
.wasm
.as_ref()
.is_some_and(|wasm| wasm.exclude_functions.iter().any(|name| name == identity))
{
return false;
}
if crate::codegen::generators::trait_bridge::is_trait_bridge_managed_fn(identity, &config.trait_bridges) {
return true;
}
function_is_exported(identity, functions, config)
}
/// Resolve the Rust identity behind a symbol that may be spelled the way JavaScript sees it.
///
/// wasm-bindgen exports every symbol under `js_name = to_camel_case(rust_name)`, so a
/// `[e2e.calls.<name>.overrides.wasm] function` legitimately names `clearRerankerBackends` for a
/// Rust `clear_reranker_backends`. Every question below is keyed on the Rust identity, so matching
/// the configured spelling against those keys directly reports a symbol the target does export as
/// missing. Trait-bridge registry operations are searched alongside `functions` because the
/// trait-bridge generator emits them and they need not appear in the plain function surface.
///
/// Returns `None` when nothing the target could export answers to `symbol` under either spelling.
fn rust_identity_for_wasm_symbol<'a>(
symbol: &str,
functions: &'a [crate::core::ir::FunctionDef],
config: &'a ResolvedCrateConfig,
) -> Option<&'a str> {
// The Rust spelling is matched first so a crate that happens to export both `foo_bar` and
// `fooBar` resolves an exact name to itself rather than to whichever came first. ~keep
wasm_export_candidates(functions, config)
.find(|candidate| *candidate == symbol)
.or_else(|| {
wasm_export_candidates(functions, config)
.find(|candidate| crate::codegen::generators::trait_bridge::to_camel_case(candidate) == symbol)
})
}
/// Every Rust identity the WASM target could export: the plain function surface plus the
/// registry operations the trait-bridge generator emits.
fn wasm_export_candidates<'a>(
functions: &'a [crate::core::ir::FunctionDef],
config: &'a ResolvedCrateConfig,
) -> impl Iterator<Item = &'a str> {
let bridge_registry_fns = config
.trait_bridges
.iter()
.flat_map(|bridge| {
[
bridge.register_fn.as_deref(),
bridge.unregister_fn.as_deref(),
bridge.clear_fn.as_deref(),
]
})
.flatten();
functions
.iter()
.map(|function| function.name.as_str())
.chain(bridge_registry_fns)
}
pub(crate) fn function_is_exported(
function_name: &str,
functions: &[crate::core::ir::FunctionDef],
config: &ResolvedCrateConfig,
) -> bool {
if config
.wasm
.as_ref()
.is_some_and(|wasm| wasm.exclude_functions.iter().any(|name| name == function_name))
{
return false;
}
if crate::codegen::generators::trait_bridge::is_trait_bridge_managed_fn(function_name, &config.trait_bridges) {
return false;
}
let enabled_features = config.features_for_language(Language::Wasm);
let core_import = config.core_import_for_language(Language::Wasm);
let source_remaps = config
.wasm
.as_ref()
.map(|wasm| &wasm.source_crate_remaps)
.into_iter()
.flatten()
.map(|name| name.replace('-', "_"))
.collect::<AHashSet<_>>();
let dropped_crates = config
.wasm
.as_ref()
.map(|wasm| &wasm.exclude_extra_dependencies)
.into_iter()
.flatten()
.map(|name| name.replace('-', "_"))
.filter(|name| name != &core_import && !source_remaps.contains(name))
.collect::<AHashSet<_>>();
functions.iter().any(|function| {
function.name == function_name
&& !is_gated_behind_disabled_feature(&function.cfg, enabled_features)
&& !dropped_crates.contains(&function.rust_path.split("::").next().unwrap_or("").replace('-', "_"))
})
}
/// 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,
}
}
/// Prepend a visible marker comment listing fields dropped from `item` because they reference a
/// type with no generated wasm binding (see `first_unknown_named_type` in `cfg.rs`).
///
/// The comment is emitted directly above the struct so the omission is discoverable by reading
/// the generated source, not just by grepping build logs for the accompanying `tracing::warn!`
/// (nothing may be silently omitted from a binding).
fn prepend_unknown_type_omission_marker(omissions: Option<&Vec<(String, String)>>, item: String) -> String {
let Some(omissions) = omissions else {
return item;
};
let mut marker = String::from(
"// ALEF-OMITTED: the field(s) below were dropped from this WASM binding\n\
// because their Rust type has no generated wasm-bindgen representation.\n",
);
for (field_name, type_name) in omissions {
marker.push_str(&format!(
"// - field `{field_name}`: type `{type_name}` is not part of the bound wasm API surface\n"
));
}
format!("{marker}{item}")
}
/// Types for which `methods::gen_method` emits a self-delegating
/// `{core_import}::{type_name}::from(self.clone()).{method}(..)` call for at least one method.
///
/// That delegation form is a hard requirement on `impl From<Wasm{Type}> for {core}::{Type}`
/// existing (see `methods.rs`). The reverse (`binding -> core`) conversion is otherwise only
/// emitted for types in `input_type_names(api)` — types reachable as a function/method
/// parameter, directly or transitively through struct fields. A struct that is only ever
/// *returned* (never taken as a parameter, directly or transitively) but that also has an
/// auto-delegated instance method — e.g. `PageRange::page_count(&self)` — falls through that
/// gap: `input_type_names` has no reason to include it, yet `gen_method` still needs the
/// reverse impl to compile the delegation. Mirrors the exact branching `gen_method` uses to
/// decide between self-delegation and the opaque mutex-lock path, so the two stay in sync.
fn types_needing_self_delegation_reverse_impl(api: &ApiSurface, opaque_types: &AHashSet<String>) -> AHashSet<String> {
let mut needed = AHashSet::default();
for typ in api.types.iter().filter(|t| !t.is_trait) {
let has_mut_methods = typ
.methods
.iter()
.any(|m| matches!(m.receiver.as_ref(), Some(ReceiverKind::RefMut)));
let is_opaque_type = opaque_types.contains(&typ.name);
for method in &typ.methods {
if method.is_static {
continue;
}
let is_ref_mut_receiver = matches!(method.receiver.as_ref(), Some(ReceiverKind::RefMut));
// Mirrors gen_method: this path calls `self.inner.lock().unwrap().{method}(..)`
// directly on the core value held by the opaque wrapper — no `From` impl needed.
if is_opaque_type && has_mut_methods && !is_ref_mut_receiver {
continue;
}
let delegates_via_self_conversion = if method.is_async {
// gen_method's async branch always builds `core_call` via self-delegation
// (or the mutex path excluded above), regardless of `can_delegate`.
true
} else if is_ref_mut_receiver && has_mut_methods {
!method.sanitized
&& method
.params
.iter()
.all(|p| !p.sanitized && shared::is_delegatable_param(&p.ty, opaque_types))
&& shared::is_opaque_delegatable_type(&method.return_type)
} else {
shared::can_auto_delegate(method, opaque_types)
};
if delegates_via_self_conversion {
needed.insert(typ.name.clone());
break;
}
}
}
needed
}
/// Fix up `<field>: Default::default().map(Box::new),` lines left behind by the shared
/// binding->core `From` conversion generator (`crate::codegen::conversions`, shared with every
/// other backend) when a field's type is a payload-carrying enum (a `#[serde(tag = "type")]`
/// enum with struct variants).
///
/// wasm_bindgen only supports fieldless, C-style enums, so `gen_struct` (this backend, see
/// `types.rs`) drops any field referencing such an enum from the generated Wasm struct entirely.
/// The shared conversion generator does not know the field was dropped: it still emits a value
/// for it and falls back to `Default::default()`. For an `Option<Box<T>>` field the generic
/// Option<Box<_>> wrapper then unconditionally appends `.map(Box::new)`, producing
/// `Default::default().map(Box::new)` -- a value that is always `None` (`Option::default()` is
/// `None` for every `T`, and `None.map(Box::new)` is `None`), but whose type `T` rustc cannot
/// infer (E0282), since nothing in the expression pins it down.
///
/// Replacing the whole expression with the equivalent literal `None` is behavior-preserving and
/// compiles; the comment documents *why* the field is always `None` on wasm for anyone reading
/// the generated binding.
fn fix_dropped_payload_enum_option_fields(content: String) -> String {
let Ok(dropped_boxed_option_field) =
Regex::new(r"(?m)^(?P<indent>[ \t]*)(?P<field>\w+): Default::default\(\)\.map\(Box::new\),$")
else {
return content;
};
dropped_boxed_option_field
.replace_all(&content, |caps: ®ex::Captures<'_>| {
format!(
"{indent}// ALEF-OMITTED: `{field}` is always None on wasm -- its Rust type is a \
payload-carrying enum, which wasm_bindgen cannot represent.\n\
{indent}{field}: None,",
indent = &caps["indent"],
field = &caps["field"],
)
})
.into_owned()
}
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>> {
crate::codegen::config_gen::validate_rust_default_functions(api)?;
// 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 api = &crate::backends::ir_order::with_sorted_items(api).with_deduped_functions();
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 = ts_union::wasm_exclude_types(config);
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());
}
// See `enums::register_untagged_data_enum_overrides` for why. ~keep
let untagged_data_enum_names = enums::register_untagged_data_enum_overrides(api, &mut type_overrides);
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());
}
}
// Captured before the move: `known_type_names` below needs the override keys, and
// `WasmMapper::new` takes the map by value.
let override_type_names: Vec<String> = type_overrides.keys().cloned().collect();
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;
// Detect fields that reference a type with no generated wasm binding: neither a
// `TypeDef`/`EnumDef` present in the (already cfg-filtered) API surface nor an explicit
// `type_overrides` entry. `WasmMapper::named` (see `type_map.rs`) maps every
// `TypeRef::Named` unconditionally to `"{prefix}{name}"` with no existence check, so
// left alone this would silently emit a reference to a `Wasm*` struct that is never
// generated — a dangling-type compile failure the consumer only discovers by running
// `wasm-pack build`, not by reading the generated source. Route such fields through the
// same exclusion machinery as cfg-gated fields, but warn loudly and mark the omission in
// the generated source instead of dropping it in silence.
let mut known_type_names: AHashSet<String> = api.types.iter().map(|t| t.name.clone()).collect();
known_type_names.extend(api.enums.iter().map(|e| e.name.clone()));
known_type_names.extend(override_type_names.iter().cloned());
let mut unknown_type_omissions: AHashMap<String, Vec<(String, String)>> = AHashMap::default();
for typ in api.types.iter().filter(|t| !t.is_opaque && !t.is_trait) {
if exclude_types.contains(&typ.name) {
continue;
}
for field in shared::binding_fields(&typ.fields) {
if field_references_excluded_type(&field.ty, &exclude_types) {
continue;
}
let Some(unknown_name) = first_unknown_named_type(&field.ty, &known_type_names) else {
continue;
};
let unknown_name = unknown_name.to_string();
tracing::warn!(
struct_name = %typ.name,
field_name = %field.name,
referenced_type = %unknown_name,
"wasm backend: field references a type with no generated wasm binding; omitting field"
);
if !exclude_types.contains(&unknown_name) {
exclude_types.push(unknown_name.clone());
}
unknown_type_omissions
.entry(typ.name.clone())
.or_default()
.push((field.name.clone(), unknown_name));
}
}
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);
}
for attribute in crate::codegen::shared::format_crate_attributes(&config.crate_attributes) {
builder.add_inner_attribute(&attribute);
}
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> = ts_union::wasm_opaque_type_names(api, &exclude_types);
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;");
}
// Names whose fields are stored as `JsValue` and bridged through `serde_wasm_bindgen`,
// for both `gen_struct` below and `wasm_conv_config`. ~keep
//
// `untagged_union_text_types` members are excluded even when they are untagged data
// enums: that opt-in already pinned their `type_overrides` entry to `String`, which is
// what the accessor/constructor emitter reads. Leaving them here too would store the
// field as `JsValue` while the getter and setter still say `String` -- a struct that
// does not compile. The text opt-in is the more specific signal, so it wins on both
// sides. ~keep
let jsvalue_bridged_enum_names: AHashSet<String> = api
.enums
.iter()
.filter(|e| {
!exclude_types.contains(&e.name)
&& !text_field_enum_names.contains(&e.name)
&& (enums::is_tagged_data_enum(e) || enums::is_untagged_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) && !untagged_data_enum_names.contains(&e.name))
.cloned()
.collect();
// Built up front (structs are emitted before enums below) so `gen_struct_methods` can
// type an untagged-enum-typed field's getter/setter as the real structural union instead
// of bare `JsValue`. `untagged_ts_plan.plans` is consumed later, in the enum loop, to
// keep emission order (structs, then enums) unchanged. ~keep
let mut untagged_ts_plan = ts_union::build_untagged_enum_ts_plan_for_api(
api,
&exclude_types,
&opaque_types,
&text_field_enum_names,
&prefix,
);
if !untagged_ts_plan.custom_section.is_empty() {
builder.add_item(&untagged_ts_plan.custom_section);
}
let untagged_ts_value_types = ts_union::value_type_names(&untagged_ts_plan);
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 {
// A type that dropped a field is NOT core-to-binding convertible: the
// delegating `Default` impl is `<core::T as Default>::default().into()`, which
// needs a `From<core::T>` that can carry every field across. The omitted field has
// no binding representation to carry it into, so the conversion cannot exist and
// the struct must fall back to `#[derive(Default)]` on the fields that remain.
let is_core_to_binding_convertible = core_to_binding_convertible_for_structs.contains(&typ.name)
&& !unknown_type_omissions.contains_key(&typ.name);
// gen_struct gates #[derive(Default)] and the delegating Default impl on
let struct_code = gen_struct(
typ,
&mapper,
&exclude_types,
&core_import,
&prefix,
&jsvalue_bridged_enum_names,
&source_remaps_borrowed,
is_core_to_binding_convertible,
);
builder.add_item(&prepend_unknown_type_omission_marker(
unknown_type_omissions.get(&typ.name),
struct_code,
));
builder.add_item(&gen_struct_methods(
typ,
&mapper,
&exclude_types,
&core_import,
&opaque_types,
&methods_enums,
&prefix,
&mutex_types,
&streaming_item_types,
&untagged_ts_value_types,
));
}
}
for enum_def in &api.enums {
if exclude_types.contains(&enum_def.name) {
continue;
}
// No `Wasm{Enum}` type for an untagged data enum — see `register_untagged_data_enum_overrides`. ~keep
if let Some(plan) = untagged_ts_plan.plans.remove(&enum_def.name) {
builder.add_item(&plan.extern_type_declaration);
} else if !enums::is_untagged_data_enum(enum_def) {
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
&& !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 jsvalue_bridged_enum_names.is_empty() {
None
} else {
Some(&jsvalue_bridged_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);
let self_delegating_types = types_needing_self_delegation_reverse_impl(api, &opaque_types);
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) || self_delegating_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_untagged_data_enum(e) {
// No `Wasm{Enum}` type to write a `From` impl against; see the `gen_enum` skip above. ~keep
} else 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();
content = fix_dropped_payload_enum_option_fields(content);
content = forward_trait_bridge_builder_fields(content, &config.trait_bridges);
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);
}
}
}
}
// `gen_cargo_toml` emits no `[lib] path`, so cargo resolves the library at
// `<crate root>/src/lib.rs`; both paths must therefore come from one derivation of the
// output path, and taking the crate root to be its parent is right only when the
// configured path is `src`-suffixed. ~keep
let layout = wasm_output_layout(config);
let cargo_toml_path = layout.root.join("Cargo.toml");
Ok(vec![
GeneratedFile {
path: layout.src.join("lib.rs"),
content,
generated_header: false,
},
GeneratedFile {
// The wasm crate builds its own manifest instead of going through
// `scaffold::scaffold`, so it needs the version floor applied here or it
// stays the one regenerated binding manifest that can still downgrade a
// dependency the consumer has already moved forward. ~keep
content: crate::scaffold::version_floor::floor_manifest(
&gen_cargo_toml(api, config),
config,
&cargo_toml_path,
),
path: cargo_toml_path,
generated_header: true,
},
])
}
fn generate_service_api(
&self,
api: &ApiSurface,
config: &ResolvedCrateConfig,
) -> anyhow::Result<Vec<GeneratedFile>> {
let sorted_api = crate::backends::ir_order::with_sorted_items(api);
Ok(service_api::gen_service_files(&sorted_api, config))
}
fn build_config(&self) -> Option<BuildConfig> {
Some(BuildConfig {
tool: "wasm-pack",
crate_suffix: "-wasm",
build_dep: BuildDependency::None,
post_build: vec![],
})
}
fn build_config_with_config(&self, config: &ResolvedCrateConfig) -> Option<BuildConfig> {
let mut build_config = self.build_config()?;
// `wasm-pack build --target nodejs --out-dir pkg/nodejs` (see `build_command_for`'s
// "wasm-pack" arm) writes its own `package.json` derived from the wasm crate's
// `Cargo.toml`, not from `config.wasm_package_name()`. Every `file:` dependency and
// `require()`/`import` specifier the wasm e2e codegen emits uses
// `wasm_package_name()` (see `ResolvedCrateConfig::wasm_crate_path`), so rewrite the
// built artifact's declared name to match after every build — otherwise the
// specifier names a package the directory does not declare. Verified against a
// consumer whose e2e manifest depends on a scoped `@scope/<crate>-wasm` name resolved
// by a `file:` path into `crates/<crate>-wasm/pkg/nodejs`, while that directory
// declares the bare crate name `<crate>-wasm`.
//
// This must resolve the crate directory exactly as `build_command_for` does —
// `[crates.output] wasm` first (minus a trailing `src`, which is where the *generated
// sources* land, not the crate root), then the `package_dir` default formula. Deriving
// it any other way lets the build write `pkg/nodejs` under one directory while this
// step looks under another, and a missing file is only debug-logged, so the rewrite
// would silently never fire. ~keep
let wasm_crate_dir = config
.explicit_output
.wasm
.as_deref()
.map(|output| {
if output.file_name().is_some_and(|name| name == "src") {
output.parent().unwrap_or(output)
} else {
output
}
})
.map_or_else(|| PathBuf::from(config.package_dir(Language::Wasm)), PathBuf::from);
let package_json_path = wasm_crate_dir.join("pkg/nodejs/package.json");
build_config.post_build.push(PostBuildStep::RewriteWasmPackageName {
package_json_path,
package_name: config.wasm_package_name(),
});
Some(build_config)
}
}
fn forward_trait_bridge_builder_fields(
mut content: String,
trait_bridges: &[crate::core::config::TraitBridgeConfig],
) -> String {
for bridge in 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!(".{}({}.map(|v| (*v.inner).clone()))", field_name, param_name);
content = content.replace(&pattern, &replacement);
}
}
content
}
#[cfg(test)]
mod tests;
#[cfg(test)]
mod untagged_enum_tests;