use crate::backends::dart::template_env;
use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{ApiSurface, TypeRef};
use std::path::PathBuf;
fn api_has_ahash_param(api: &ApiSurface) -> bool {
api.functions.iter().any(|f| f.params.iter().any(|p| p.map_is_ahash))
}
fn type_has_json(t: &TypeRef) -> bool {
match t {
TypeRef::Json => true,
TypeRef::Optional(inner) | TypeRef::Vec(inner) => type_has_json(inner),
TypeRef::Map(k, v) => type_has_json(k) || type_has_json(v),
_ => false,
}
}
fn type_references_excluded_named(
t: &TypeRef,
excluded_type_paths: &std::collections::HashMap<String, String>,
) -> bool {
match t {
TypeRef::Named(name) => excluded_type_paths.contains_key(name),
TypeRef::Optional(inner) | TypeRef::Vec(inner) => type_references_excluded_named(inner, excluded_type_paths),
TypeRef::Map(k, v) => {
type_references_excluded_named(k, excluded_type_paths)
|| type_references_excluded_named(v, excluded_type_paths)
}
_ => false,
}
}
fn api_has_trait_bridge_excluded_carrier(api: &ApiSurface, config: &ResolvedCrateConfig) -> bool {
config
.trait_bridges
.iter()
.filter(|cfg| !cfg.exclude_languages.iter().any(|l| l == "dart"))
.filter_map(|cfg| api.types.iter().find(|t| t.name == cfg.trait_name && t.is_trait))
.flat_map(|trait_def| trait_def.methods.iter())
.filter(|m| m.trait_source.is_none())
.any(|m| {
type_references_excluded_named(&m.return_type, &api.excluded_type_paths)
|| m.params
.iter()
.any(|p| type_references_excluded_named(&p.ty, &api.excluded_type_paths))
})
}
fn api_has_json_or_enum_field(api: &ApiSurface) -> bool {
if api
.types
.iter()
.flat_map(|t| t.fields.iter())
.any(|f| type_has_json(&f.ty))
|| api
.functions
.iter()
.any(|f| f.params.iter().any(|p| type_has_json(&p.ty)) || type_has_json(&f.return_type))
{
return true;
}
let enum_names: std::collections::HashSet<&str> = api.enums.iter().map(|e| e.name.as_str()).collect();
fn type_ref_contains_enum(t: &TypeRef, enum_names: &std::collections::HashSet<&str>) -> bool {
match t {
TypeRef::Named(name) => enum_names.contains(name.as_str()),
TypeRef::Optional(inner) | TypeRef::Vec(inner) => type_ref_contains_enum(inner, enum_names),
TypeRef::Map(k, v) => type_ref_contains_enum(k, enum_names) || type_ref_contains_enum(v, enum_names),
_ => false,
}
}
api.types
.iter()
.filter(|t| !t.is_trait && !t.is_opaque)
.flat_map(|t| t.fields.iter())
.any(|f| type_ref_contains_enum(&f.ty, &enum_names))
|| api.functions.iter().any(|f| {
f.params.iter().any(|p| type_ref_contains_enum(&p.ty, &enum_names))
|| type_ref_contains_enum(&f.return_type, &enum_names)
})
}
#[allow(dead_code)]
fn api_has_json_field(api: &ApiSurface) -> bool {
api.types
.iter()
.flat_map(|t| t.fields.iter())
.any(|f| type_has_json(&f.ty))
|| api
.functions
.iter()
.any(|f| f.params.iter().any(|p| type_has_json(&p.ty)) || type_has_json(&f.return_type))
}
pub(crate) fn emit_cargo_toml(
rust_dir: &str,
api: &ApiSurface,
config: &ResolvedCrateConfig,
source_crate_name: &str,
) -> GeneratedFile {
let crate_name = config.name.as_str();
let version = &api_version(config);
let frb_version = crate::backends::dart::naming::dart_frb_version(config);
let core_crate_dir = config.core_crate_for_language(crate::core::config::extras::Language::Dart);
let dart_override = config.dart.as_ref().and_then(|c| c.core_crate_override.as_deref());
let core_dep_key: String = match dart_override {
Some(name) => name.to_string(),
None => source_crate_name.to_string(),
};
let same_as_workspace = dart_override.is_none() && core_crate_dir == *crate_name && config.workspace_root.is_none();
let core_path = if same_as_workspace {
"../../..".to_string()
} else {
format!("../../../crates/{core_crate_dir}")
};
let features = config.features_for_language(crate::core::config::extras::Language::Dart);
let features_block = if features.is_empty() {
String::new()
} else {
let list = features
.iter()
.map(|f| format!("\"{f}\""))
.collect::<Vec<_>>()
.join(", ");
format!(", features = [{list}]")
};
let package_rename_block = if dart_override.is_none() && core_dep_key != crate_name {
format!(", package = \"{crate_name}\"")
} else {
String::new()
};
let has_trait_bridges = config.trait_bridges.iter().any(|b| {
!b.exclude_languages.iter().any(|l| l == "dart")
&& api.types.iter().any(|t| t.name == b.trait_name && t.is_trait)
});
let trait_bridge_deps = if has_trait_bridges {
"async-trait = \"0.1\"\n"
} else {
""
};
let workspace_extra = config.extra_deps_for_language(crate::core::config::extras::Language::Dart);
let mut workspace_dep_lines: Vec<String> = workspace_extra
.iter()
.map(|(name, value)| {
if let Some(s) = value.as_str() {
format!("{name} = \"{s}\"")
} else {
format!("{name} = {value}")
}
})
.collect();
workspace_dep_lines.sort();
let has_trait_bridge_excluded_carrier = api_has_trait_bridge_excluded_carrier(api, config);
let needs_serde_json = api_has_json_or_enum_field(api) || has_trait_bridge_excluded_carrier;
let serde_json_dep = if needs_serde_json { "serde_json = \"1\"\n" } else { "" };
let needs_serde_derive = has_trait_bridge_excluded_carrier;
let serde_dep = if needs_serde_derive {
"serde = { version = \"1\", features = [\"derive\"] }\n"
} else {
""
};
let needs_ahash = api_has_ahash_param(api);
let ahash_dep = if needs_ahash { "ahash = \"0.8\"\n" } else { "" };
let has_streaming = config
.adapters
.iter()
.any(|a| matches!(a.pattern, crate::core::config::extras::AdapterPattern::Streaming));
let futures_util_dep = if has_streaming { "futures-util = \"0.3\"\n" } else { "" };
let has_services = !api.services.is_empty();
let tokio_dep = if has_streaming || has_trait_bridges || has_services {
"tokio = { version = \"1\", features = [\"rt-multi-thread\", \"sync\"] }\n"
} else {
""
};
let target_overrides = config
.dart
.as_ref()
.map(|c| c.target_dep_overrides.as_slice())
.unwrap_or(&[]);
let frb_line = format!("flutter_rust_bridge = \"={frb_version}\"");
let mut dep_lines: Vec<String> = Vec::new();
if target_overrides.is_empty() {
dep_lines.push(format!(
"{core_dep_key} = {{ path = \"{core_path}\"{package_rename_block}{features_block} }}"
));
}
dep_lines.push(frb_line);
for dep in [
ahash_dep,
serde_dep,
serde_json_dep,
futures_util_dep,
tokio_dep,
trait_bridge_deps,
] {
let trimmed = dep.trim_end_matches('\n');
if !trimmed.is_empty() {
dep_lines.push(trimmed.to_string());
}
}
dep_lines.extend(workspace_dep_lines);
dep_lines.sort_by(|a, b| {
let key = |line: &str| line.split('=').next().unwrap_or("").trim().to_string();
key(a).cmp(&key(b))
});
let extra_deps = if dep_lines.is_empty() {
String::new()
} else {
format!("{}\n", dep_lines.join("\n"))
};
let license = config
.scaffold
.as_ref()
.and_then(|s| s.license.as_deref())
.unwrap_or("MIT");
let mut machete_ignored: Vec<String> = std::iter::once(core_dep_key.clone())
.chain(workspace_extra.keys().cloned())
.collect();
if api_has_ahash_param(api) {
machete_ignored.push("ahash".to_string());
}
machete_ignored.sort();
machete_ignored.dedup();
let machete_ignored_list = machete_ignored
.iter()
.map(|n| format!("\"{n}\""))
.collect::<Vec<_>>()
.join(", ");
let (core_dep_line, target_override_blocks) = if target_overrides.is_empty() {
(String::new(), String::new())
} else {
let neg_cfg = if target_overrides.len() == 1 {
target_overrides[0].cfg.clone()
} else {
let any = target_overrides
.iter()
.map(|o| o.cfg.as_str())
.collect::<Vec<_>>()
.join(", ");
format!("any({any})")
};
let mut blocks = template_env::render(
"rust_cargo_target_dependency.rs.jinja",
minijinja::context! {
cfg => format!("not({neg_cfg})"),
core_dep_key => core_dep_key.as_str(),
core_path => core_path.as_str(),
package_rename_block => package_rename_block.as_str(),
default_block => "",
features_block => features_block.as_str(),
},
);
for override_entry in target_overrides {
let feat_list = override_entry
.features
.iter()
.map(|f| format!("\"{f}\""))
.collect::<Vec<_>>()
.join(", ");
let feats_block = if feat_list.is_empty() {
String::new()
} else {
format!(", features = [{feat_list}]")
};
let default_block = if override_entry.default_features {
String::new()
} else {
", default-features = false".to_string()
};
blocks.push_str(&template_env::render(
"rust_cargo_target_dependency.rs.jinja",
minijinja::context! {
cfg => override_entry.cfg.as_str(),
core_dep_key => core_dep_key.as_str(),
core_path => core_path.as_str(),
package_rename_block => package_rename_block.as_str(),
default_block => default_block.as_str(),
features_block => feats_block.as_str(),
},
));
}
(String::new(), blocks)
};
let content = template_env::render(
"rust_cargo_toml.rs.jinja",
minijinja::context! {
crate_name => crate_name,
version => version.as_str(),
license => license,
machete_ignored_list => machete_ignored_list.as_str(),
core_dep_line => core_dep_line.as_str(),
frb_version => frb_version.as_str(),
extra_deps => extra_deps.as_str(),
target_override_blocks => target_override_blocks.as_str(),
},
);
GeneratedFile {
path: PathBuf::from(format!("{rust_dir}/Cargo.toml")),
content,
generated_header: false,
}
}
pub(crate) fn emit_build_rs(rust_dir: &str, package_name: &str, module_name: &str, stem: &str) -> GeneratedFile {
let loader_patch = render_loader_patch_fn(package_name, module_name, stem);
let content = template_env::render(
"rust_build_rs.rs.jinja",
minijinja::context! {
loader_patch => loader_patch.as_str(),
},
);
GeneratedFile {
path: PathBuf::from(format!("{rust_dir}/build.rs")),
content,
generated_header: false,
}
}
fn render_loader_patch_fn(package_name: &str, module_name: &str, stem: &str) -> String {
let dart_replacement = dart_init_prologue_replacement(package_name, module_name, stem);
template_env::render(
"rust_loader_patch_fn.rs.jinja",
minijinja::context! {
module_name => module_name,
dart_replacement => dart_replacement.as_str(),
},
)
}
fn dart_init_prologue_replacement(package_name: &str, module_name: &str, stem: &str) -> String {
template_env::render(
"dart_init_prologue_replacement.jinja",
minijinja::context! {
package_name => package_name,
module_name => module_name,
stem => stem,
},
)
}
pub(crate) fn emit_frb_yaml(rust_dir: &str, module_name: &str) -> GeneratedFile {
let content = template_env::render(
"flutter_rust_bridge_yaml.jinja",
minijinja::context! {
module_name => module_name,
},
);
GeneratedFile {
path: PathBuf::from(format!("{rust_dir}/flutter_rust_bridge.yaml")),
content,
generated_header: false,
}
}
fn api_version(config: &ResolvedCrateConfig) -> String {
config.resolved_version().unwrap_or_else(|| "0.1.0".to_string())
}
#[cfg(test)]
mod build_rs_tests {
use super::*;
#[test]
fn emitted_build_rs_is_valid_rust() {
let file = emit_build_rs(
"packages/dart/rust",
"sample_router",
"sample_router",
"sample_router_dart",
);
syn::parse_file(&file.content).expect("generated build.rs must be valid Rust");
}
#[test]
fn emitted_build_rs_patches_published_loader_after_codegen() {
let file = emit_build_rs(
"packages/dart/rust",
"sample_router",
"sample_router",
"sample_router_dart",
);
assert!(
file.content.contains("patch_published_loader();"),
"build.rs must invoke the loader patch after codegen"
);
assert!(
file.content.contains("fn patch_published_loader()"),
"build.rs must define the loader patch"
);
assert!(
file.content
.contains(r#"../lib/src/sample_router_bridge_generated/frb_generated.dart"#),
"build.rs must target the generated frb dart file"
);
assert!(
file.content
.contains("Isolate.resolvePackageUri(Uri.parse('package:sample_router/sample_router.dart'))"),
"build.rs replacement must resolve the package URI"
);
assert!(
file.content
.contains("externalLibrary ??= await _alefResolveExternalLibrary();"),
"build.rs replacement must prefer the package-relative library"
);
}
#[test]
fn emitted_build_rs_runs_dart_format_after_patch() {
let file = emit_build_rs(
"packages/dart/rust",
"sample_router",
"sample_router",
"sample_router_dart",
);
assert!(
file.content.contains("Command::new(\"dart\")")
&& file.content.contains("\"format\"")
&& file.content.contains("FRB_GENERATED_DART"),
"build.rs must run `dart format` on the patched frb_generated.dart"
);
}
#[test]
fn emitted_build_rs_handles_loader_patch_write_error() {
let file = emit_build_rs(
"packages/dart/rust",
"sample_router",
"sample_router",
"sample_router_dart",
);
assert!(
file.content
.contains("if let Err(err) = std::fs::write(path, &patched)")
&& file
.content
.contains("cargo:warning=failed to write published-loader patch: {err}")
&& file.content.contains("return;"),
"emitted build.rs must handle loader patch write errors"
);
}
}