From 23ac9b1c1884d8070c21ba5a1a30249608185624 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jurij=20Juki=C4=87?= Date: Tue, 18 Nov 2025 15:49:59 +0100 Subject: [PATCH 1/4] aliasing --- src/build/caller_utils_ts_generator.rs | 132 +++++++++++++++++++- src/build/wit_generator.rs | 164 ++++++++++++++++++++----- 2 files changed, 260 insertions(+), 36 deletions(-) diff --git a/src/build/caller_utils_ts_generator.rs b/src/build/caller_utils_ts_generator.rs index 60da87cd..02a3cc63 100644 --- a/src/build/caller_utils_ts_generator.rs +++ b/src/build/caller_utils_ts_generator.rs @@ -269,6 +269,7 @@ struct WitTypes { records: Vec, variants: Vec, enums: Vec, + aliases: Vec<(String, String)>, } // Structure to hold types grouped by hyperapp @@ -278,6 +279,7 @@ struct HyperappTypes { records: Vec, variants: Vec, enums: Vec, + aliases: Vec<(String, String)>, } // Parse WIT file to extract function signatures, records, and variants @@ -292,6 +294,7 @@ fn parse_wit_file(file_path: &Path) -> Result { let mut records = Vec::new(); let mut variants = Vec::new(); let mut enums = Vec::new(); + let mut aliases = Vec::new(); // Simple parser for WIT files to extract record definitions let lines: Vec<_> = content.lines().collect(); @@ -300,8 +303,22 @@ fn parse_wit_file(file_path: &Path) -> Result { while i < lines.len() { let line = lines[i].trim(); + // Look for type aliases + if line.starts_with("type ") { + // Expect: type name = rhs + let rest = line + .trim_start_matches("type ") + .trim_end_matches(';') + .trim(); + if let Some(eq_pos) = rest.find('=') { + let name = strip_wit_escape(rest[..eq_pos].trim()).to_string(); + let rhs = rest[eq_pos + 1..].trim().to_string(); + debug!(alias = %name, rhs = %rhs, "Found alias"); + aliases.push((name, rhs)); + } + } // Look for record definitions - if line.starts_with("record ") { + else if line.starts_with("record ") { let record_name = line .trim_start_matches("record ") .trim_end_matches(" {") @@ -537,6 +554,7 @@ fn parse_wit_file(file_path: &Path) -> Result { records, variants, enums, + aliases, }) } @@ -898,6 +916,7 @@ pub fn create_typescript_caller_utils(base_dir: &Path, api_dir: &Path) -> Result records: Vec::new(), variants: Vec::new(), enums: Vec::new(), + aliases: Vec::new(), }; // Parse each WIT file for this hyperapp @@ -976,6 +995,7 @@ pub fn create_typescript_caller_utils(base_dir: &Path, api_dir: &Path) -> Result // Collect all types for this hyperapp hyperapp_data.records.extend(wit_types.records); + hyperapp_data.aliases.extend(wit_types.aliases); hyperapp_data.variants.extend(wit_types.variants); hyperapp_data.enums.extend(wit_types.enums); @@ -997,6 +1017,7 @@ pub fn create_typescript_caller_utils(base_dir: &Path, api_dir: &Path) -> Result || !hyperapp_data.records.is_empty() || !hyperapp_data.variants.is_empty() || !hyperapp_data.enums.is_empty() + || !hyperapp_data.aliases.is_empty() { hyperapp_types_map.insert(hyperapp_name.clone(), hyperapp_data); } @@ -1020,13 +1041,118 @@ pub fn create_typescript_caller_utils(base_dir: &Path, api_dir: &Path) -> Result )); ts_content.push_str(&format!("export namespace {} {{\n", hyperapp_name)); - // Add custom types (records, variants, and enums) for this hyperapp - if !hyperapp_data.records.is_empty() + // Emit fallback primitive aliases when WIT omitted them but usage exists. + // We scan all known types to discover referenced custom aliases. + let mut referenced: std::collections::HashSet = std::collections::HashSet::new(); + // Helper to extract rough tokens from a WIT type string + let mut collect_tokens = |ty: &str| { + let mut cur = String::new(); + for ch in ty.chars() { + if ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-' { + cur.push(ch); + } else { + if !cur.is_empty() { + referenced.insert(cur.clone()); + cur.clear(); + } + } + } + if !cur.is_empty() { + referenced.insert(cur); + } + }; + for rec in &hyperapp_data.records { + for f in &rec.fields { + collect_tokens(&f.wit_type); + } + } + for sig in &hyperapp_data.signatures { + for f in &sig.fields { + collect_tokens(&f.wit_type); + } + } + for var in &hyperapp_data.variants { + for case in &var.cases { + if let Some(ref dt) = case.data_type { + collect_tokens(dt); + } + } + } + + // Remove built-ins and generics + let builtins = [ + "s8", "u8", "s16", "u16", "s32", "u32", "s64", "u64", "f32", "f64", "bool", "char", + "string", "address", "list", "option", "result", "tuple", "_", + ]; + for b in builtins.iter() { + referenced.remove(*b); + } + + // Anything already provided via explicit aliases should not be duplicated + let provided_aliases: std::collections::HashSet = hyperapp_data + .aliases + .iter() + .map(|(n, _)| n.clone()) + .collect(); + + // Compute fallback aliases + let mut fallback_aliases: Vec<(String, String)> = Vec::new(); + for name in referenced { + if provided_aliases.contains(&name) { + continue; + } + if name.ends_with("-id") { + // Treat all *-id as string identifiers in TS + fallback_aliases.push((name.clone(), "string".to_string())); + continue; + } + match name.as_str() { + // Common collections that sometimes leak from WIT without a concrete alias + "hash-map" => fallback_aliases.push((name.clone(), "record".to_string())), + "hash-set" => fallback_aliases.push((name.clone(), "string[]".to_string())), + // serde_json::Value equivalent + "value" => fallback_aliases.push((name.clone(), "unknown".to_string())), + _ => {} + } + } + + // Add custom types (aliases, records, variants, and enums) for this hyperapp + if !hyperapp_data.aliases.is_empty() + || !hyperapp_data.records.is_empty() || !hyperapp_data.variants.is_empty() || !hyperapp_data.enums.is_empty() { + // First, emit primitive/fallback aliases if any + if !fallback_aliases.is_empty() { + ts_content.push_str("\n // Primitive type aliases used by this hyperapp (generated from WIT usage)\n"); + for (alias_name, rhs) in &fallback_aliases { + let ts_alias = to_pascal_case(alias_name); + let rhs_ts = match rhs.as_str() { + "record" => "Record".to_string(), + other => other.to_string(), + }; + ts_content.push_str(&format!(" export type {} = {}\n", ts_alias, rhs_ts)); + } + ts_content.push_str("\n"); + } + ts_content.push_str("\n // Custom Types\n"); + // Generate type aliases first so downstream types can reference them + for (alias_name, rhs) in &hyperapp_data.aliases { + let ts_alias = to_pascal_case(alias_name); + // Special-case: map WIT alias `value` to TS `unknown` for ergonomic JSON usage + let rhs_ts = if alias_name == "value" { + "unknown".to_string() + } else { + wit_type_to_typescript(rhs) + }; + ts_content.push_str(&format!(" export type {} = {}\n", ts_alias, rhs_ts)); + } + if !hyperapp_data.aliases.is_empty() { + ts_content.push_str("\n"); + } + // Generate enums first for enum_def in &hyperapp_data.enums { let enum_ts = generate_typescript_enum(enum_def); diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 9267f9ba..edc5e7df 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -229,6 +229,10 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result Ok("u32".to_string()), "i64" => Ok("s64".to_string()), "u64" => Ok("u64".to_string()), + // WIT 1.0 does not support 128-bit integers. Represent these as strings + // to preserve full precision across language boundaries. + "i128" => Ok("string".to_string()), + "u128" => Ok("string".to_string()), "f32" => Ok("f32".to_string()), "f64" => Ok("f64".to_string()), "usize" => Ok("u64".to_string()), @@ -313,31 +317,47 @@ fn rust_type_to_wit(ty: &Type, used_types: &mut HashSet) -> Result { - // if let syn::PathArguments::AngleBracketed(args) = - // &type_path.path.segments.last().unwrap().arguments - // { - // if args.args.len() >= 2 { - // if let ( - // Some(syn::GenericArgument::Type(key_ty)), - // Some(syn::GenericArgument::Type(val_ty)), - // ) = (args.args.first(), args.args.get(1)) - // { - // let key_type = rust_type_to_wit(key_ty, used_types)?; - // let val_type = rust_type_to_wit(val_ty, used_types)?; - // // For HashMaps, we'll generate a list of tuples where each tuple contains a key and value - // Ok(format!("list>", key_type, val_type)) - // } else { - // Ok("list>".to_string()) - // } - // } else { - // Ok("list>".to_string()) - // } - // } else { - // Ok("list>".to_string()) - // } - //} + "HashMap" | "BTreeMap" => { + if let syn::PathArguments::AngleBracketed(args) = + &type_path.path.segments.last().unwrap().arguments + { + if args.args.len() >= 2 { + if let ( + Some(syn::GenericArgument::Type(key_ty)), + Some(syn::GenericArgument::Type(val_ty)), + ) = (args.args.first(), args.args.get(1)) + { + let key_type = rust_type_to_wit(key_ty, used_types)?; + let val_type = rust_type_to_wit(val_ty, used_types)?; + // Defer alias/string validation to later verification. + // Generate a tuple-backed representation using the key type name + // (which may be an alias like `node-id`) and let the alias + // definition resolve to `string`. + Ok(format!("list>", key_type, val_type)) + } else { + bail!("Failed to parse HashMap generic arguments"); + } + } else { + bail!("HashMap requires two generic arguments "); + } + } else { + bail!("Failed to parse HashMap generic arguments"); + } + } + "HashSet" | "BTreeSet" => { + if let syn::PathArguments::AngleBracketed(args) = + &type_path.path.segments.last().unwrap().arguments + { + if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() { + let inner = rust_type_to_wit(inner_ty, used_types)?; + Ok(format!("list<{}>", inner)) + } else { + bail!("Failed to parse HashSet inner type"); + } + } else { + bail!("Failed to parse HashSet inner type"); + } + } custom => { // Validate custom type name validate_name(custom, "Type")?; @@ -720,6 +740,23 @@ fn collect_single_type_definition( return generate_enum_wit_definition(e, &name, &kebab_name, &mut dependencies) .map(|wit_def| Some((wit_def, dependencies))); } + Item::Type(t) => { + let alias_name = t.ident.to_string(); + // Skip internal types + if alias_name.contains("__") { + continue; + } + + let kebab_name = to_kebab_case(&alias_name); + if kebab_name != target_type_kebab { + continue; + } + + // Build alias: type = + let rhs = rust_type_to_wit(&t.ty, &mut dependencies)?; + let def = format!("type {} = {};", to_wit_ident(&kebab_name), rhs); + return Ok(Some((def, dependencies))); + } _ => {} } } @@ -765,13 +802,20 @@ fn generate_struct_wit_definition( } Ok(field_strings) } - syn::Fields::Unnamed(_) => { - bail!( - "Struct '{}' has unnamed (tuple-style) fields, which are not supported in WIT. \ - WIT only supports named fields in records. \ - Consider converting to a struct with named fields.", - name - ); + syn::Fields::Unnamed(fields) => { + // Support 1-tuple (newtype) structs by emitting a WIT type alias. + if fields.unnamed.len() == 1 { + let inner = &fields.unnamed[0]; + let wit_type = rust_type_to_wit(&inner.ty, dependencies)?; + return Ok(format!("type {} = {};", to_wit_ident(&kebab_name), wit_type)); + } else { + bail!( + "Struct '{}' has {} unnamed (tuple-style) fields, which are not supported in WIT. \ + Only newtype (single-field) tuple structs are supported as type aliases.", + name, + fields.unnamed.len() + ); + } } syn::Fields::Unit => { // Unit struct becomes an empty record @@ -1263,6 +1307,8 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result>(); let mut collected_types = HashSet::new(); + // Track every custom type referenced directly or via dependencies + let mut transitively_used_types: HashSet = HashSet::new(); // Iteratively collect type definitions and their dependencies while !types_to_collect.is_empty() { @@ -1285,6 +1331,7 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result = global_used_types.clone(); + all_used_types.extend(transitively_used_types.into_iter()); + + // Infer simple aliases for well-known leaf types that commonly appear but + // aren't defined as concrete structs/enums in Rust. These are injected + // ahead of explicit type definitions so downstream records can reference + // them. + let mut inferred_aliases: Vec = Vec::new(); + let mut inferred_types: HashSet = HashSet::new(); + + for used_type_name in &all_used_types { + // Skip primitives/built-ins and anything we already have a definition for + if is_wit_primitive_or_builtin(used_type_name) + || all_type_definitions.contains_key(used_type_name) + { + continue; + } + + // Normalize once + let t = used_type_name.as_str(); + + // 1) serde_json::Value shows up as `value`; define it as string in WIT so + // the type exists at the schema level (TS generator will map it to unknown + // for ergonomic JSON usage). + if t == "value" { + inferred_aliases.push(format!("type {} = string;", to_wit_ident("value"))); + inferred_types.insert("value".to_string()); + continue; + } + + // 2) Common identifier aliases (GroupId, ThreadId, etc.) appear as kebab-case *-id. + // We only alias those that aren't defined explicitly in source code. + if t.ends_with("-id") { + inferred_aliases.push(format!("type {} = string;", to_wit_ident(t))); + inferred_types.insert(t.to_string()); + continue; + } + } + // --- 4. Build dependency graph and topologically sort types --- debug!("Pass 4: Building type dependency graph"); @@ -1414,9 +1501,10 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result Result Date: Tue, 18 Nov 2025 16:06:14 +0100 Subject: [PATCH 2/4] remove fallback aliases --- src/build/caller_utils_ts_generator.rs | 89 -------------------------- 1 file changed, 89 deletions(-) diff --git a/src/build/caller_utils_ts_generator.rs b/src/build/caller_utils_ts_generator.rs index 02a3cc63..fac29b08 100644 --- a/src/build/caller_utils_ts_generator.rs +++ b/src/build/caller_utils_ts_generator.rs @@ -1041,101 +1041,12 @@ pub fn create_typescript_caller_utils(base_dir: &Path, api_dir: &Path) -> Result )); ts_content.push_str(&format!("export namespace {} {{\n", hyperapp_name)); - // Emit fallback primitive aliases when WIT omitted them but usage exists. - // We scan all known types to discover referenced custom aliases. - let mut referenced: std::collections::HashSet = std::collections::HashSet::new(); - // Helper to extract rough tokens from a WIT type string - let mut collect_tokens = |ty: &str| { - let mut cur = String::new(); - for ch in ty.chars() { - if ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-' { - cur.push(ch); - } else { - if !cur.is_empty() { - referenced.insert(cur.clone()); - cur.clear(); - } - } - } - if !cur.is_empty() { - referenced.insert(cur); - } - }; - for rec in &hyperapp_data.records { - for f in &rec.fields { - collect_tokens(&f.wit_type); - } - } - for sig in &hyperapp_data.signatures { - for f in &sig.fields { - collect_tokens(&f.wit_type); - } - } - for var in &hyperapp_data.variants { - for case in &var.cases { - if let Some(ref dt) = case.data_type { - collect_tokens(dt); - } - } - } - - // Remove built-ins and generics - let builtins = [ - "s8", "u8", "s16", "u16", "s32", "u32", "s64", "u64", "f32", "f64", "bool", "char", - "string", "address", "list", "option", "result", "tuple", "_", - ]; - for b in builtins.iter() { - referenced.remove(*b); - } - - // Anything already provided via explicit aliases should not be duplicated - let provided_aliases: std::collections::HashSet = hyperapp_data - .aliases - .iter() - .map(|(n, _)| n.clone()) - .collect(); - - // Compute fallback aliases - let mut fallback_aliases: Vec<(String, String)> = Vec::new(); - for name in referenced { - if provided_aliases.contains(&name) { - continue; - } - if name.ends_with("-id") { - // Treat all *-id as string identifiers in TS - fallback_aliases.push((name.clone(), "string".to_string())); - continue; - } - match name.as_str() { - // Common collections that sometimes leak from WIT without a concrete alias - "hash-map" => fallback_aliases.push((name.clone(), "record".to_string())), - "hash-set" => fallback_aliases.push((name.clone(), "string[]".to_string())), - // serde_json::Value equivalent - "value" => fallback_aliases.push((name.clone(), "unknown".to_string())), - _ => {} - } - } - // Add custom types (aliases, records, variants, and enums) for this hyperapp if !hyperapp_data.aliases.is_empty() || !hyperapp_data.records.is_empty() || !hyperapp_data.variants.is_empty() || !hyperapp_data.enums.is_empty() { - // First, emit primitive/fallback aliases if any - if !fallback_aliases.is_empty() { - ts_content.push_str("\n // Primitive type aliases used by this hyperapp (generated from WIT usage)\n"); - for (alias_name, rhs) in &fallback_aliases { - let ts_alias = to_pascal_case(alias_name); - let rhs_ts = match rhs.as_str() { - "record" => "Record".to_string(), - other => other.to_string(), - }; - ts_content.push_str(&format!(" export type {} = {}\n", ts_alias, rhs_ts)); - } - ts_content.push_str("\n"); - } - ts_content.push_str("\n // Custom Types\n"); // Generate type aliases first so downstream types can reference them From b23b2a81d3b295b7b371d5394db727b2ff78856d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jurij=20Juki=C4=87?= Date: Tue, 18 Nov 2025 17:01:52 +0100 Subject: [PATCH 3/4] simplify --- src/build/wit_generator.rs | 40 ++++++++------------------------------ 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index edc5e7df..81a73be0 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -1363,40 +1363,16 @@ fn process_rust_project(project_path: &Path, api_dir: &Path) -> Result = global_used_types.clone(); all_used_types.extend(transitively_used_types.into_iter()); - // Infer simple aliases for well-known leaf types that commonly appear but - // aren't defined as concrete structs/enums in Rust. These are injected - // ahead of explicit type definitions so downstream records can reference - // them. + // Minimal inference: only add alias for `value` when used. let mut inferred_aliases: Vec = Vec::new(); let mut inferred_types: HashSet = HashSet::new(); - - for used_type_name in &all_used_types { - // Skip primitives/built-ins and anything we already have a definition for - if is_wit_primitive_or_builtin(used_type_name) - || all_type_definitions.contains_key(used_type_name) - { - continue; - } - - // Normalize once - let t = used_type_name.as_str(); - - // 1) serde_json::Value shows up as `value`; define it as string in WIT so - // the type exists at the schema level (TS generator will map it to unknown - // for ergonomic JSON usage). - if t == "value" { - inferred_aliases.push(format!("type {} = string;", to_wit_ident("value"))); - inferred_types.insert("value".to_string()); - continue; - } - - // 2) Common identifier aliases (GroupId, ThreadId, etc.) appear as kebab-case *-id. - // We only alias those that aren't defined explicitly in source code. - if t.ends_with("-id") { - inferred_aliases.push(format!("type {} = string;", to_wit_ident(t))); - inferred_types.insert(t.to_string()); - continue; - } + if all_used_types.contains("value") && !all_type_definitions.contains_key("value") { + inferred_aliases.push( + "// Arbitrary JSON value; encoded as string for WIT 1.0 (TS: unknown, Rust: serde_json::Value)" + .to_string(), + ); + inferred_aliases.push(format!("type {} = string;", to_wit_ident("value"))); + inferred_types.insert("value".to_string()); } // --- 4. Build dependency graph and topologically sort types --- From 96d4c7cff7d516bfaa85189d54b9a10f65a3d43e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 18 Nov 2025 16:02:17 +0000 Subject: [PATCH 4/4] Format Rust code using rustfmt --- src/build/wit_generator.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/build/wit_generator.rs b/src/build/wit_generator.rs index 81a73be0..0eea8fdf 100644 --- a/src/build/wit_generator.rs +++ b/src/build/wit_generator.rs @@ -807,7 +807,11 @@ fn generate_struct_wit_definition( if fields.unnamed.len() == 1 { let inner = &fields.unnamed[0]; let wit_type = rust_type_to_wit(&inner.ty, dependencies)?; - return Ok(format!("type {} = {};", to_wit_ident(&kebab_name), wit_type)); + return Ok(format!( + "type {} = {};", + to_wit_ident(&kebab_name), + wit_type + )); } else { bail!( "Struct '{}' has {} unnamed (tuple-style) fields, which are not supported in WIT. \