use crate::cli::{Cli, TypeFormat, TypesyncArgs, TypesyncCommands};
use crate::config_builders;
use evenframe_core::{
config::EvenframeConfig,
error::Result,
types::ForeignTypeRegistry,
typesync::{
arktype::generate_arktype_type_string,
config::{FileNamingConvention, OutputMode},
effect::{generate_effect_schema_for_types, generate_effect_schema_string},
file_grouping::{FileOutputPlan, compute_file_grouping},
flatbuffers::generate_flatbuffers_schema_string,
import_resolver::{
barrel_filename, format_imports, generate_barrel_file, resolve_imports,
type_name_to_filename,
},
macroforge::{
compute_extra_imports, compute_macro_import_line, generate_macroforge_for_types,
generate_macroforge_type_string,
},
protobuf::generate_protobuf_schema_string,
},
};
use std::collections::BTreeSet;
use std::path::Path;
use tracing::{debug, error, info, warn};
pub async fn run(_cli: &Cli, args: TypesyncArgs) -> Result<()> {
info!("Starting type generation");
let config = match EvenframeConfig::new() {
Ok(cfg) => {
info!("Configuration loaded successfully");
cfg
}
Err(e) => {
error!("Failed to load configuration: {}", e);
return Err(e);
}
};
let build_config = config_builders::BuildConfig::from_toml()?;
let (enums, tables, objects) = config_builders::build_all_configs(&build_config)?;
let (enums, tables, objects) = config_builders::filter_for_typesync(enums, tables, objects);
let structs = config_builders::merge_tables_and_objects(&tables, &objects);
let registry = ForeignTypeRegistry::from_config(&config.general.foreign_types);
info!(
"Found {} enums, {} tables, {} objects",
enums.len(),
tables.len(),
objects.len()
);
let output_mode = if args.per_file {
OutputMode::PerFile
} else {
config.typesync.output.mode
};
let barrel_file = config.typesync.output.barrel_file;
let file_naming = config.typesync.output.file_naming;
let file_extension = &config.typesync.output.file_extension;
let array_style = config.typesync.output.array_style;
if let Some(cmd) = args.command {
match cmd {
TypesyncCommands::Arktype(arktype_args) => {
let output_path = arktype_args
.output
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| format!("{}arktype.ts", config.typesync.output_path));
if output_mode == OutputMode::PerFile {
warn!(
"ArkType does not support per-file output (scope requires all types in one file). Falling back to single-file mode."
);
}
generate_arktype(&structs, &enums, &output_path, ®istry)?;
}
TypesyncCommands::Effect(effect_args) => {
let output_path = effect_args
.output
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| format!("{}bindings.ts", config.typesync.output_path));
match output_mode {
OutputMode::Single => {
generate_effect(&structs, &enums, &output_path, ®istry)?
}
OutputMode::PerFile => generate_effect_per_file(EffectPerFileArgs {
structs: &structs,
enums: &enums,
base_output_path: &config.typesync.output_path,
subdir: "effect",
barrel_file,
naming: file_naming,
file_ext: file_extension,
registry: ®istry,
})?,
}
}
TypesyncCommands::Macroforge(macroforge_args) => {
let output_path = macroforge_args
.output
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| format!("{}macroforge.ts", config.typesync.output_path));
match output_mode {
OutputMode::Single => {
generate_macroforge(&structs, &enums, &output_path, array_style, ®istry)?
}
OutputMode::PerFile => generate_macroforge_per_file(MacroforgePerFileArgs {
structs: &structs,
enums: &enums,
base_output_path: &config.typesync.output_path,
barrel_file,
naming: file_naming,
file_ext: file_extension,
array_style,
registry: ®istry,
})?,
}
}
TypesyncCommands::Flatbuffers(fbs_args) => {
let output_path = fbs_args
.output
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| format!("{}schema.fbs", config.typesync.output_path));
let namespace = fbs_args
.namespace
.or(config.typesync.flatbuffers_namespace.clone());
generate_flatbuffers(
&structs,
&enums,
&output_path,
namespace.as_deref(),
®istry,
)?;
}
TypesyncCommands::Protobuf(proto_args) => {
let output_path = proto_args
.output
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| format!("{}schema.proto", config.typesync.output_path));
let package = proto_args
.package
.or(config.typesync.protobuf_package.clone());
let import_validate = if proto_args.no_import_validate {
false
} else if proto_args.import_validate {
true
} else {
config.typesync.protobuf_import_validate
};
generate_protobuf(
&structs,
&enums,
&output_path,
package.as_deref(),
import_validate,
®istry,
)?;
}
}
return Ok(());
}
let mut formats_to_generate: BTreeSet<TypeFormat> = BTreeSet::new();
if let Some(ref formats) = args.formats {
formats_to_generate.extend(formats.iter().cloned());
} else {
if config.typesync.should_generate_arktype_types {
formats_to_generate.insert(TypeFormat::Arktype);
}
if config.typesync.should_generate_effect_types {
formats_to_generate.insert(TypeFormat::Effect);
}
if config.typesync.should_generate_macroforge_types {
formats_to_generate.insert(TypeFormat::Macroforge);
}
if config.typesync.should_generate_flatbuffers_types {
formats_to_generate.insert(TypeFormat::Flatbuffers);
}
if config.typesync.should_generate_protobuf_types {
formats_to_generate.insert(TypeFormat::Protobuf);
}
}
if let Some(ref skip) = args.skip {
for format in skip {
formats_to_generate.remove(format);
}
}
for format in &formats_to_generate {
match format {
TypeFormat::Arktype => {
if output_mode == OutputMode::PerFile {
warn!(
"ArkType does not support per-file output (scope requires all types in one file). Falling back to single-file mode."
);
}
let path = format!("{}arktype.ts", config.typesync.output_path);
generate_arktype(&structs, &enums, &path, ®istry)?;
}
TypeFormat::Effect => match output_mode {
OutputMode::Single => {
let path = format!("{}bindings.ts", config.typesync.output_path);
generate_effect(&structs, &enums, &path, ®istry)?;
}
OutputMode::PerFile => {
generate_effect_per_file(EffectPerFileArgs {
structs: &structs,
enums: &enums,
base_output_path: &config.typesync.output_path,
subdir: "effect",
barrel_file,
naming: file_naming,
file_ext: file_extension,
registry: ®istry,
})?;
}
},
TypeFormat::Macroforge => match output_mode {
OutputMode::Single => {
let path = format!("{}macroforge.ts", config.typesync.output_path);
generate_macroforge(&structs, &enums, &path, array_style, ®istry)?;
}
OutputMode::PerFile => {
generate_macroforge_per_file(MacroforgePerFileArgs {
structs: &structs,
enums: &enums,
base_output_path: &config.typesync.output_path,
barrel_file,
naming: file_naming,
file_ext: file_extension,
array_style,
registry: ®istry,
})?;
}
},
TypeFormat::Flatbuffers => {
let path = format!("{}schema.fbs", config.typesync.output_path);
generate_flatbuffers(
&structs,
&enums,
&path,
config.typesync.flatbuffers_namespace.as_deref(),
®istry,
)?;
}
TypeFormat::Protobuf => {
let path = format!("{}schema.proto", config.typesync.output_path);
generate_protobuf(
&structs,
&enums,
&path,
config.typesync.protobuf_package.as_deref(),
config.typesync.protobuf_import_validate,
®istry,
)?;
}
}
}
info!(
"Type generation complete. Generated {} format(s)",
formats_to_generate.len()
);
Ok(())
}
fn generate_arktype(
structs: &std::collections::BTreeMap<String, evenframe_core::types::StructConfig>,
enums: &std::collections::BTreeMap<String, evenframe_core::types::TaggedUnion>,
output_path: &str,
registry: &ForeignTypeRegistry,
) -> Result<()> {
info!("Generating ArkType types to {}", output_path);
let content = generate_arktype_type_string(structs, enums, false, registry);
let full_content = format!(
"import {{ scope }} from 'arktype';\n\n{}\n\n export const validator = scope({{\n ...bindings.export(),\n}}).export();",
content
);
std::fs::write(output_path, full_content)?;
debug!("ArkType types written successfully");
Ok(())
}
fn generate_effect(
structs: &std::collections::BTreeMap<String, evenframe_core::types::StructConfig>,
enums: &std::collections::BTreeMap<String, evenframe_core::types::TaggedUnion>,
output_path: &str,
registry: &ForeignTypeRegistry,
) -> Result<()> {
info!("Generating Effect schemas to {}", output_path);
let content = generate_effect_schema_string(structs, enums, false, registry);
let full_content = format!("import {{ Schema }} from \"effect\";\n\n{}", content);
std::fs::write(output_path, full_content)?;
debug!("Effect schemas written successfully");
Ok(())
}
struct EffectPerFileArgs<'a> {
structs: &'a std::collections::BTreeMap<String, evenframe_core::types::StructConfig>,
enums: &'a std::collections::BTreeMap<String, evenframe_core::types::TaggedUnion>,
base_output_path: &'a str,
subdir: &'a str,
barrel_file: bool,
naming: FileNamingConvention,
file_ext: &'a str,
registry: &'a ForeignTypeRegistry,
}
fn generate_effect_per_file(args: EffectPerFileArgs<'_>) -> Result<()> {
let EffectPerFileArgs {
structs,
enums,
base_output_path,
subdir,
barrel_file,
naming,
file_ext,
registry,
} = args;
let plan = compute_file_grouping(structs, enums);
let dir = Path::new(base_output_path).join(subdir);
std::fs::create_dir_all(&dir)?;
cleanup_obsolete_files(&dir, &plan, naming, file_ext)?;
info!(
"Generating Effect schemas (per-file) to {} ({} files)",
dir.display(),
plan.groups.len()
);
for group in &plan.groups {
let imports = resolve_imports(group, &plan, structs, enums, naming, file_ext);
let type_names = group.all_types();
let body = generate_effect_schema_for_types(&type_names, structs, enums, registry);
let mut file_content = String::new();
file_content.push_str("import { Schema } from \"effect\";\n");
let import_lines = format_imports(&imports);
if !import_lines.is_empty() {
file_content.push_str(&import_lines);
file_content.push('\n');
}
file_content.push('\n');
file_content.push_str(&body);
let filename = type_name_to_filename(&group.primary_type, naming);
let file_path = dir.join(format!("{}{}", filename, file_ext));
std::fs::write(&file_path, file_content)?;
debug!("Written {}", file_path.display());
}
if barrel_file {
let barrel_content = generate_barrel_file(&plan, naming, file_ext);
let barrel_path = dir.join(barrel_filename(file_ext));
std::fs::write(&barrel_path, barrel_content)?;
debug!("Written barrel file {}", barrel_path.display());
}
info!("Effect per-file generation complete");
Ok(())
}
fn generate_macroforge(
structs: &std::collections::BTreeMap<String, evenframe_core::types::StructConfig>,
enums: &std::collections::BTreeMap<String, evenframe_core::types::TaggedUnion>,
output_path: &str,
array_style: evenframe_core::typesync::config::ArrayStyle,
registry: &ForeignTypeRegistry,
) -> Result<()> {
info!("Generating Macroforge types to {}", output_path);
let content = generate_macroforge_type_string(structs, enums, false, array_style, registry);
std::fs::write(output_path, content)?;
debug!("Macroforge types written successfully");
Ok(())
}
struct MacroforgePerFileArgs<'a> {
structs: &'a std::collections::BTreeMap<String, evenframe_core::types::StructConfig>,
enums: &'a std::collections::BTreeMap<String, evenframe_core::types::TaggedUnion>,
base_output_path: &'a str,
barrel_file: bool,
naming: FileNamingConvention,
file_ext: &'a str,
array_style: evenframe_core::typesync::config::ArrayStyle,
registry: &'a ForeignTypeRegistry,
}
fn generate_macroforge_per_file(args: MacroforgePerFileArgs<'_>) -> Result<()> {
let MacroforgePerFileArgs {
structs,
enums,
base_output_path,
barrel_file,
naming,
file_ext,
array_style,
registry,
} = args;
let plan = compute_file_grouping(structs, enums);
let dir = Path::new(base_output_path);
std::fs::create_dir_all(dir)?;
cleanup_obsolete_files(dir, &plan, naming, file_ext)?;
info!(
"Generating Macroforge types (per-file) to {} ({} files)",
dir.display(),
plan.groups.len()
);
for group in &plan.groups {
let imports = resolve_imports(group, &plan, structs, enums, naming, file_ext);
let type_names = group.all_types();
let body =
generate_macroforge_for_types(&type_names, structs, enums, array_style, registry);
let mut file_content = String::new();
if let Some(macro_import) = compute_macro_import_line(&type_names, structs, enums) {
file_content.push_str(¯o_import);
file_content.push('\n');
}
let extra_imports = compute_extra_imports(&type_names, structs, enums, registry);
for import_line in &extra_imports {
file_content.push_str(import_line);
file_content.push('\n');
}
let import_lines = format_imports(&imports);
if !import_lines.is_empty() {
file_content.push_str(&import_lines);
file_content.push('\n');
}
if !file_content.is_empty() {
file_content.push('\n');
}
file_content.push_str(&body);
let filename = type_name_to_filename(&group.primary_type, naming);
let file_path = dir.join(format!("{}{}", filename, file_ext));
std::fs::write(&file_path, file_content)?;
debug!("Written {}", file_path.display());
}
if barrel_file {
let barrel_content = generate_barrel_file(&plan, naming, file_ext);
let barrel_path = dir.join(barrel_filename(file_ext));
std::fs::write(&barrel_path, barrel_content)?;
debug!("Written barrel file {}", barrel_path.display());
}
info!("Macroforge per-file generation complete");
Ok(())
}
fn generate_flatbuffers(
structs: &std::collections::BTreeMap<String, evenframe_core::types::StructConfig>,
enums: &std::collections::BTreeMap<String, evenframe_core::types::TaggedUnion>,
output_path: &str,
namespace: Option<&str>,
registry: &ForeignTypeRegistry,
) -> Result<()> {
info!("Generating FlatBuffers schema to {}", output_path);
let content = generate_flatbuffers_schema_string(structs, enums, namespace, registry);
std::fs::write(output_path, content)?;
debug!("FlatBuffers schema written successfully");
Ok(())
}
fn generate_protobuf(
structs: &std::collections::BTreeMap<String, evenframe_core::types::StructConfig>,
enums: &std::collections::BTreeMap<String, evenframe_core::types::TaggedUnion>,
output_path: &str,
package: Option<&str>,
import_validate: bool,
registry: &ForeignTypeRegistry,
) -> Result<()> {
info!("Generating Protocol Buffers schema to {}", output_path);
let content =
generate_protobuf_schema_string(structs, enums, package, import_validate, registry);
std::fs::write(output_path, content)?;
debug!("Protocol Buffers schema written successfully");
Ok(())
}
fn cleanup_obsolete_files(
dir: &Path,
plan: &FileOutputPlan,
naming: FileNamingConvention,
file_ext: &str,
) -> Result<()> {
let mut expected: BTreeSet<String> = plan
.groups
.iter()
.map(|g| {
format!(
"{}{}",
type_name_to_filename(&g.primary_type, naming),
file_ext
)
})
.collect();
expected.insert(barrel_filename(file_ext));
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return Ok(()),
};
for entry in entries.flatten() {
let file_name = entry.file_name().to_string_lossy().to_string();
if file_name.ends_with(file_ext) && !expected.contains(&file_name) {
info!("Removing obsolete file: {}", entry.path().display());
std::fs::remove_file(entry.path())?;
}
}
Ok(())
}