create_farm/
lib.rs

1use anyhow::Context;
2use clap::Parser;
3use std::{ffi::OsString, fs, process::exit};
4use utils::prompts;
5
6use crate::{
7  package_manager::PackageManager,
8  template::{ElectronSubTemplate, TauriSubTemplate, Template},
9  utils::colors::*,
10};
11
12mod args;
13mod package_manager;
14mod template;
15pub mod utils;
16
17pub fn run<I, A>(args: I, bin_name: Option<String>, detected_manager: Option<String>)
18where
19  I: IntoIterator<Item = A>,
20  A: Into<OsString> + Clone,
21{
22  if let Err(e) = run_cli(args, bin_name, detected_manager) {
23    println!();
24    eprintln!("\n {BOLD}{RED}error{RESET}: {e:#}\n");
25    exit(1);
26  }
27}
28
29fn run_cli<I, A>(
30  args: I,
31  bin_name: Option<String>,
32  detected_manager: Option<String>,
33) -> anyhow::Result<()>
34where
35  I: IntoIterator<Item = A>,
36  A: Into<OsString> + Clone,
37{
38  let detected_manager = detected_manager.and_then(|p| p.parse::<PackageManager>().ok());
39  // Clap will auto parse the `bin_name` as the first argument, so we need to add it to the args
40  let args = args::Args::parse_from(
41    std::iter::once(OsString::from(bin_name.unwrap_or_default()))
42      .chain(args.into_iter().map(Into::into)),
43  );
44
45  handle_brand_text("\n ⚡ Welcome To Farm ! \n");
46  let defaults = args::Args::default();
47  let args::Args {
48    manager,
49    project_name,
50    template,
51    force,
52  } = args;
53
54  let cwd = std::env::current_dir()?;
55  let mut default_project_name = "farm-project";
56  let project_name = match project_name {
57    Some(name) => to_valid_pkg_name(&name),
58    None => loop {
59      let input = prompts::input("Project name", Some(default_project_name), false)?
60        .trim()
61        .to_string();
62      if !is_valid_pkg_name(&input) {
63        eprintln!(
64          "{BOLD}{RED}✘{RESET} Invalid project name: {BOLD}{YELLOW}{input}{RESET}, package name should only include lowercase alphanumeric character and hyphens \"-\" and doesn't start with numbers",
65        );
66        default_project_name = to_valid_pkg_name(&input).leak();
67        continue;
68      };
69      break input;
70    },
71  };
72  let target_dir = cwd.join(&project_name);
73
74  if target_dir.exists() && target_dir.read_dir()?.next().is_some() {
75    let overwrite = force
76      || prompts::confirm(
77        &format!(
78          "{} directory is not empty, do you want to overwrite?",
79          if target_dir == cwd {
80            "Current".to_string()
81          } else {
82            target_dir
83              .file_name()
84              .unwrap()
85              .to_string_lossy()
86              .to_string()
87          }
88        ),
89        false,
90      )?;
91    if !overwrite {
92      eprintln!("{BOLD}{RED}✘{RESET} Directory is not empty, Operation Cancelled");
93      exit(1);
94    }
95  };
96
97  let pkg_manager = manager.unwrap_or(match detected_manager {
98    Some(manager) => manager,
99    None => defaults.manager.context("default manager not set")?,
100  });
101
102  let templates_no_flavors = pkg_manager.templates_no_flavors();
103
104  let template = match template {
105    Some(template) => template,
106    None => {
107      let selected_template =
108        prompts::select("Select a framework:", &templates_no_flavors, Some(0))?.unwrap();
109
110      match selected_template {
111        Template::Tauri(None) => {
112          let sub_templates = vec![
113            TauriSubTemplate::React,
114            TauriSubTemplate::Vue,
115            TauriSubTemplate::Svelte,
116            TauriSubTemplate::Vanilla,
117            TauriSubTemplate::Solid,
118            TauriSubTemplate::Preact,
119          ];
120
121          let sub_template =
122            prompts::select("Select a Tauri template:", &sub_templates, Some(0))?.unwrap();
123
124          Template::Tauri(Some(*sub_template))
125        }
126        Template::Tauri2(None) => {
127          let sub_templates = vec![
128            TauriSubTemplate::React,
129            TauriSubTemplate::Vue,
130            TauriSubTemplate::Svelte,
131            TauriSubTemplate::Vanilla,
132            TauriSubTemplate::Solid,
133            TauriSubTemplate::Preact,
134          ];
135
136          let sub_template =
137            prompts::select("Select a Tauri2 template:", &sub_templates, Some(0))?.unwrap();
138
139          Template::Tauri2(Some(*sub_template))
140        }
141        Template::Electron(None) => {
142          let sub_templates = vec![
143            ElectronSubTemplate::React,
144            ElectronSubTemplate::Vue,
145            ElectronSubTemplate::Svelte,
146            ElectronSubTemplate::Vanilla,
147            ElectronSubTemplate::Solid,
148            ElectronSubTemplate::Preact,
149          ];
150
151          let sub_template =
152            prompts::select("Select an Electron template:", &sub_templates, Some(0))?.unwrap();
153
154          Template::Electron(Some(*sub_template))
155        }
156        _ => *selected_template,
157      }
158    }
159  };
160
161  if target_dir.exists() {
162    #[inline(always)]
163    fn clean_dir(dir: &std::path::PathBuf) -> anyhow::Result<()> {
164      for entry in fs::read_dir(dir)?.flatten() {
165        let path = entry.path();
166        if entry.file_type()?.is_dir() {
167          if entry.file_name() != ".git" {
168            clean_dir(&path)?;
169            std::fs::remove_dir(path)?;
170          }
171        } else {
172          fs::remove_file(path)?;
173        }
174      }
175      Ok(())
176    }
177    clean_dir(&target_dir)?;
178  } else {
179    fs::create_dir_all(&target_dir)?;
180  }
181
182  // Render the template
183  template.render(&target_dir, pkg_manager, &project_name, &project_name)?;
184
185  handle_brand_text("\n >  Initial Farm Project created successfully ✨ ✨ \n");
186
187  if target_dir != cwd {
188    handle_brand_text(&format!(
189      "    cd {} \n",
190      if project_name.contains(' ') {
191        format!("\"{project_name}\"")
192      } else {
193        project_name.to_string()
194      }
195    ));
196  }
197  if let Some(cmd) = pkg_manager.install_cmd() {
198    handle_brand_text(&format!("    {cmd} \n"));
199  }
200  handle_brand_text(&format!("    {} \n", get_run_cmd(&pkg_manager, &template)));
201
202  Ok(())
203}
204
205fn is_valid_pkg_name(project_name: &str) -> bool {
206  let mut chars = project_name.chars().peekable();
207  !project_name.is_empty()
208    && !chars.peek().map(|c| c.is_ascii_digit()).unwrap_or_default()
209    && !chars.any(|ch| !(ch.is_alphanumeric() || ch == '-' || ch == '_') || ch.is_uppercase())
210}
211
212fn to_valid_pkg_name(project_name: &str) -> String {
213  let ret = project_name
214    .trim()
215    .to_lowercase()
216    .replace([':', ';', ' ', '~'], "-")
217    .replace(['.', '\\', '/'], "");
218
219  let ret = ret
220    .chars()
221    .skip_while(|ch| ch.is_ascii_digit() || *ch == '-')
222    .collect::<String>();
223
224  if ret.is_empty() || !is_valid_pkg_name(&ret) {
225    "farm-project".to_string()
226  } else {
227    ret
228  }
229}
230
231fn get_run_cmd(pkg_manager: &PackageManager, template: &Template) -> &'static str {
232  match template {
233    Template::Tauri(_) => match pkg_manager {
234      PackageManager::Pnpm => "pnpm tauri dev",
235      PackageManager::Yarn => "yarn tauri dev",
236      PackageManager::Npm => "npm run tauri dev",
237      PackageManager::Bun => "bun tauri dev",
238    },
239    _ => pkg_manager.default_cmd(),
240  }
241}