use anyhow::{Context, Result};
use colored::*;
use std::fs;
use std::path::PathBuf;
pub fn create_project(name: &str, platforms: &str, path: Option<PathBuf>) -> Result<()> {
let project_dir = path.unwrap_or_else(|| PathBuf::from(name));
println!("{}", "🚀 Creating new JFFI app...".bright_green().bold());
println!(" Name: {}", name.bright_cyan());
println!(" Platforms: {}", platforms.bright_cyan());
println!();
let platform_list: Vec<&str> = if platforms == "multi" {
vec!["ios", "android", "macos", "windows", "linux", "web"]
} else {
platforms.split(',').map(|s| s.trim()).collect()
};
create_project_structure(&project_dir, name, &platform_list)?;
println!();
println!("{}", "✅ Project created successfully!".bright_green().bold());
println!();
println!("Next steps:");
println!(" cd {}", name);
println!(" jffi build --platform {}", platform_list[0]);
println!(" jffi run --platform {}", platform_list[0]);
println!();
Ok(())
}
fn create_project_structure(dir: &PathBuf, name: &str, platforms: &[&str]) -> Result<()> {
fs::create_dir_all(dir).context("Failed to create project directory")?;
create_workspace_cargo_toml(dir, platforms)?;
create_core_crate(dir, name)?;
create_ffi_crate(dir, name)?;
if platforms.contains(&"web") {
create_ffi_web_crate(dir, name)?;
}
for platform in platforms {
create_platform_dir(dir, name, platform)?;
}
create_config_file(dir, name, platforms)?;
create_makefile(dir, platforms)?;
create_readme(dir, name, platforms)?;
Ok(())
}
fn create_workspace_cargo_toml(dir: &PathBuf, platforms: &[&str]) -> Result<()> {
let members = if platforms.contains(&"web") {
r#"["core", "ffi", "ffi-web"]"#
} else {
r#"["core", "ffi"]"#
};
let cargo_toml = format!(r#"[workspace]
members = {}
resolver = "2"
"#, members);
fs::write(dir.join("Cargo.toml"), cargo_toml)?;
Ok(())
}
fn create_core_crate(dir: &PathBuf, name: &str) -> Result<()> {
let core_dir = dir.join("core");
fs::create_dir_all(core_dir.join("src"))?;
let cargo_toml = format!(r#"[package]
name = "{}-core"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = {{ version = "1.0", features = ["derive"] }}
"#, name);
fs::write(core_dir.join("Cargo.toml"), cargo_toml)?;
let lib_rs = r#"use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Item {
pub id: String,
pub title: String,
pub completed: bool,
}
pub struct App {
items: Vec<Item>,
}
impl App {
pub fn new() -> Self {
Self { items: Vec::new() }
}
pub fn add_item(&mut self, id: String, title: String) {
self.items.push(Item {
id,
title,
completed: false,
});
}
pub fn toggle_item(&mut self, id: &str) {
if let Some(item) = self.items.iter_mut().find(|i| i.id == id) {
item.completed = !item.completed;
}
}
pub fn delete_item(&mut self, id: &str) {
self.items.retain(|i| i.id != id);
}
pub fn get_items(&self) -> &[Item] {
&self.items
}
}
impl Default for App {
fn default() -> Self {
Self::new()
}
}
"#;
fs::write(core_dir.join("src/lib.rs"), lib_rs)?;
println!(" {} core/", "✓".green());
Ok(())
}
fn create_ffi_crate(dir: &PathBuf, name: &str) -> Result<()> {
let ffi_dir = dir.join("ffi");
fs::create_dir_all(ffi_dir.join("src"))?;
let cargo_toml = format!(r#"[package]
name = "{}-ffi"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
[[bin]]
name = "uniffi-bindgen"
path = "uniffi-bindgen.rs"
[dependencies]
{}-core = {{ path = "../core" }}
uniffi = {{ version = "0.31.0", features = ["cli"] }}
[build-dependencies]
uniffi = {{ version = "0.31.0", features = ["build"] }}
"#, name, name);
fs::write(ffi_dir.join("Cargo.toml"), cargo_toml)?;
let uniffi_bindgen_rs = r#"fn main() {
uniffi::uniffi_bindgen_main()
}
"#;
fs::write(ffi_dir.join("uniffi-bindgen.rs"), uniffi_bindgen_rs)?;
let module_name = name.replace("-", "_");
let lib_rs = format!(r#"use {}_core::{{App, Item}};
use std::sync::Mutex;
#[derive(uniffi::Record)]
pub struct ItemViewModel {{
pub id: String,
pub title: String,
pub completed: bool,
}}
impl From<&Item> for ItemViewModel {{
fn from(item: &Item) -> Self {{
Self {{
id: item.id.clone(),
title: item.title.clone(),
completed: item.completed,
}}
}}
}}
#[derive(uniffi::Object)]
pub struct FfiApp {{
app: Mutex<App>,
}}
#[uniffi::export]
impl FfiApp {{
#[uniffi::constructor]
pub fn new() -> Self {{
Self {{
app: Mutex::new(App::new()),
}}
}}
pub fn add_item(&self, id: String, title: String) -> Vec<ItemViewModel> {{
let mut app = self.app.lock().unwrap();
app.add_item(id, title);
app.get_items().iter().map(ItemViewModel::from).collect()
}}
pub fn toggle_item(&self, id: String) -> Vec<ItemViewModel> {{
let mut app = self.app.lock().unwrap();
app.toggle_item(&id);
app.get_items().iter().map(ItemViewModel::from).collect()
}}
pub fn delete_item(&self, id: String) -> Vec<ItemViewModel> {{
let mut app = self.app.lock().unwrap();
app.delete_item(&id);
app.get_items().iter().map(ItemViewModel::from).collect()
}}
pub fn get_items(&self) -> Vec<ItemViewModel> {{
let app = self.app.lock().unwrap();
app.get_items().iter().map(ItemViewModel::from).collect()
}}
}}
uniffi::setup_scaffolding!();
"#, module_name);
fs::write(ffi_dir.join("src/lib.rs"), lib_rs)?;
println!(" {} ffi/", "✓".green());
Ok(())
}
fn create_ffi_web_crate(dir: &PathBuf, name: &str) -> Result<()> {
let ffi_web_dir = dir.join("ffi-web");
fs::create_dir_all(ffi_web_dir.join("src"))?;
let cargo_toml = format!(r#"[package]
name = "{}-ffi-web"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
{}-core = {{ path = "../core" }}
wasm-bindgen = "0.2"
serde = {{ version = "1.0", features = ["derive"] }}
serde-wasm-bindgen = "0.6"
[profile.release]
opt-level = "z"
lto = true
"#, name, name);
fs::write(ffi_web_dir.join("Cargo.toml"), cargo_toml)?;
let module_name = name.replace("-", "_");
let lib_rs = format!(r#"use {}_core::{{App, Item}};
use wasm_bindgen::prelude::*;
use serde::{{Serialize, Deserialize}};
#[derive(Serialize, Deserialize)]
pub struct ItemViewModel {{
pub id: String,
pub title: String,
pub completed: bool,
}}
impl From<&Item> for ItemViewModel {{
fn from(item: &Item) -> Self {{
Self {{
id: item.id.clone(),
title: item.title.clone(),
completed: item.completed,
}}
}}
}}
#[wasm_bindgen]
pub struct FfiApp {{
app: App,
}}
#[wasm_bindgen]
impl FfiApp {{
#[wasm_bindgen(constructor)]
pub fn new() -> Self {{
Self {{
app: App::new(),
}}
}}
pub fn add_item(&mut self, id: String, title: String) -> JsValue {{
self.app.add_item(id, title);
let items: Vec<ItemViewModel> = self.app.get_items().iter().map(ItemViewModel::from).collect();
serde_wasm_bindgen::to_value(&items).unwrap()
}}
pub fn toggle_item(&mut self, id: String) -> JsValue {{
self.app.toggle_item(&id);
let items: Vec<ItemViewModel> = self.app.get_items().iter().map(ItemViewModel::from).collect();
serde_wasm_bindgen::to_value(&items).unwrap()
}}
pub fn delete_item(&mut self, id: String) -> JsValue {{
self.app.delete_item(&id);
let items: Vec<ItemViewModel> = self.app.get_items().iter().map(ItemViewModel::from).collect();
serde_wasm_bindgen::to_value(&items).unwrap()
}}
pub fn get_items(&self) -> JsValue {{
let items: Vec<ItemViewModel> = self.app.get_items().iter().map(ItemViewModel::from).collect();
serde_wasm_bindgen::to_value(&items).unwrap()
}}
}}
"#, module_name);
fs::write(ffi_web_dir.join("src/lib.rs"), lib_rs)?;
println!(" {} ffi-web/", "✓".green());
Ok(())
}
fn create_platform_dir(dir: &PathBuf, name: &str, platform: &str) -> Result<()> {
let platforms_dir = dir.join("platforms");
fs::create_dir_all(&platforms_dir)?;
match platform {
"ios" => crate::templates::ios::create_ios_project(&platforms_dir, name)?,
"android" => crate::templates::android::create_android_project(&platforms_dir, name)?,
"macos" => crate::templates::macos::create_macos_project(&platforms_dir, name)?,
"windows" => crate::templates::windows::create_windows_project(&platforms_dir, name)?,
"linux" => crate::templates::linux::create_linux_project(&platforms_dir, name)?,
"web" => crate::templates::web::create_web_project(&platforms_dir, name)?,
_ => println!(" {} Unknown platform: {}", "✗".red(), platform),
}
Ok(())
}
fn create_config_file(dir: &PathBuf, name: &str, platforms: &[&str]) -> Result<()> {
let config = format!(r#"[package]
name = "{}"
version = "0.1.0"
[platforms]
enabled = {:?}
[platforms.ios]
deployment_target = "16.0"
bundle_id = "com.example.{}"
[platforms.android]
min_sdk = 26
package = "com.example.{}"
[platforms.macos]
deployment_target = "13.0"
[platforms.windows]
min_version = "10.0.19041.0"
[platforms.linux]
gtk_version = "4.0"
[platforms.web]
target = "es2020"
"#, name, platforms, name.replace("-", ""), name.replace("-", ""));
fs::write(dir.join("jffi.toml"), config)?;
println!(" {} jffi.toml", "✓".green());
Ok(())
}
fn create_makefile(dir: &PathBuf, platforms: &[&str]) -> Result<()> {
let first_platform = platforms.first().unwrap_or(&"ios");
let makefile = format!(r#".PHONY: help build run dev clean
help:
@echo "UniFFI App - Build Commands"
@echo ""
@echo " make build PLATFORM=<platform> - Build for platform"
@echo " make run PLATFORM=<platform> - Run on platform"
@echo " make dev PLATFORM=<platform> - Watch mode"
@echo " make clean - Clean build artifacts"
@echo ""
@echo "Available platforms: {}"
@echo "Default platform: {}"
PLATFORM ?= {}
build:
@uniffi-app build --platform $(PLATFORM)
run:
@uniffi-app run --platform $(PLATFORM)
dev:
@uniffi-app dev --platform $(PLATFORM)
clean:
@cargo clean
@echo "✅ Cleaned build artifacts"
"#, platforms.join(", "), first_platform, first_platform);
fs::write(dir.join("Makefile"), makefile)?;
println!(" {} Makefile", "✓".green());
Ok(())
}
fn create_readme(dir: &PathBuf, name: &str, platforms: &[&str]) -> Result<()> {
let readme = format!(r#"# {}
Cross-platform app built with Rust + UniFFI
## Platforms
{}
## Quick Start
```bash
# Build for your platform
uniffi-app build --platform {}
# Run the app
uniffi-app run --platform {}
# Development mode (auto-rebuild)
uniffi-app dev --platform {}
```
## Project Structure
- `core/` - Business logic (pure Rust)
- `ffi/` - FFI layer (UniFFI exports)
- `platforms/` - Platform-specific UIs
## Development
Edit your business logic in `core/src/lib.rs`. The FFI bindings will be automatically regenerated.
## Adding Features
1. Add logic to `core/src/lib.rs`
2. Expose via FFI in `ffi/src/lib.rs`
3. Rebuild: `uniffi-app build --platform <platform>`
4. Update UI in `platforms/<platform>/`
Built with [UniFFI Framework](https://github.com/mozilla/uniffi-rs)
"#, name,
platforms.iter().map(|p| format!("- {}", p)).collect::<Vec<_>>().join("\n"),
platforms[0], platforms[0], platforms[0]);
fs::write(dir.join("README.md"), readme)?;
println!(" {} README.md", "✓".green());
Ok(())
}