1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
use crate::codegen::naming::to_node_name;
use crate::core::backend::GeneratedFile;
use crate::core::config::{NodeCapsuleTypeConfig, ResolvedCrateConfig};
use crate::core::ir::ApiSurface;
use std::collections::HashMap;
use std::path::PathBuf;
pub(super) fn generate(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
// Collect all exported names from the native module.
// These are used to construct named re-exports that work with both CJS and ESM.
let mut value_names = std::collections::BTreeSet::new();
let mut type_names = std::collections::BTreeSet::new();
// Collect struct and class types (skip traits and capsule types)
let capsule_types: HashMap<String, NodeCapsuleTypeConfig> = config
.node
.as_ref()
.map(|c| c.capsule_types.clone())
.unwrap_or_default();
for typ in api.types.iter() {
// The napi binding filters out Builder/Update DTOs (see the
// filters at lines 382/643/735/776) — exclude them here too so
// the typescript wrapper does not re-export names that the
// native module never emits.
if !typ.is_trait
&& !capsule_types.contains_key(&typ.name)
&& !typ.name.ends_with("Builder")
&& !typ.name.ends_with("Update")
{
if typ.is_opaque {
value_names.insert(typ.name.clone());
} else {
type_names.insert(typ.name.clone());
}
}
}
// Collect enums
for enum_def in &api.enums {
if enum_def.variants.iter().any(|v| !v.fields.is_empty()) {
type_names.insert(enum_def.name.clone());
} else {
value_names.insert(enum_def.name.clone());
}
}
// Runtime classes/enums can also be used as types. Keep type-only
// exports disjoint from value exports so the wrapper barrel never emits
// the same name in both export groups.
for name in &value_names {
type_names.remove(name);
}
// Collect functions (with snake_case → camelCase conversion for JS naming)
let exclude_functions: ahash::AHashSet<String> = config
.node
.as_ref()
.map(|c| c.exclude_functions.iter().cloned().collect())
.unwrap_or_default();
for func in &api.functions {
if !exclude_functions.contains(&func.name) {
value_names.insert(to_node_name(&func.name));
}
}
// Include trait-bridge register/unregister/clear functions
for bridge in &config.trait_bridges {
if let Some(name) = bridge.register_fn.as_deref() {
value_names.insert(to_node_name(name));
}
if let Some(name) = bridge.unregister_fn.as_deref() {
value_names.insert(to_node_name(name));
}
if let Some(name) = bridge.clear_fn.as_deref() {
value_names.insert(to_node_name(name));
}
}
// Generate TypeScript re-export file using explicit named re-exports.
// This works with both CJS and ESM because we use `export { name } from "module"`
// which TypeScript understands regardless of the underlying CJS/ESM implementation.
let package_name = config.node_package_name();
let mut lines = vec![];
// Export runtime values (functions, classes, enums) as regular exports.
if !value_names.is_empty() {
lines.push("export {".to_string());
for name in &value_names {
lines.push(format!(" {name},"));
}
lines.push(format!("}} from \"{}\";", package_name));
lines.push("".to_string());
}
// Export types as type-only exports (TypeScript 4.5+)
if !type_names.is_empty() {
lines.push("export type {".to_string());
for name in &type_names {
lines.push(format!(" {name},"));
}
lines.push(format!("}} from \"{}\";", package_name));
lines.push("".to_string());
}
// Note: custom_modules (if any) are not included in this wrapper because:
// 1. packages/typescript/src/ is the wrapper package, which only re-exports from the native binding
// 2. custom modules (e.g., helpers) are shipped in dist/ by the build system, not generated by alef
// 3. including them here would require them to exist in src/, which they don't
let content = lines.join("\n");
// Output path: packages/typescript/src/index.ts
let output_path = PathBuf::from("packages/typescript/src/index.ts");
Ok(vec![GeneratedFile {
path: output_path,
content,
generated_header: true,
}])
}