create_tauri_app/
lib.rs

1// Copyright 2019-2022 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5use anyhow::Context;
6use dialoguer::{Confirm, Input, Select};
7use std::{ffi::OsString, fs, process::exit};
8
9use crate::{
10    args::TauriVersion,
11    category::Category,
12    deps::print_missing_deps,
13    package_manager::PackageManager,
14    utils::{colors::*, dialoguer_theme::ColorfulTheme},
15};
16
17mod args;
18mod category;
19mod deps;
20mod manifest;
21mod package_manager;
22mod template;
23mod utils;
24
25pub mod internal {
26    //! Re-export of create-tauri-app internals
27    //!
28    //! ## Warning
29    //!
30    //! This is meant to be used internally only so use at your own risk
31    //! and expect APIs to break without a prior notice.
32    pub mod package_manager {
33        pub use crate::package_manager::*;
34    }
35
36    pub mod template {
37        pub use crate::template::*;
38    }
39}
40
41pub fn run<I, A>(args: I, bin_name: Option<String>, detected_manager: Option<String>)
42where
43    I: IntoIterator<Item = A>,
44    A: Into<OsString> + Clone,
45{
46    let _ = ctrlc::set_handler(move || {
47        eprint!("\x1b[?25h");
48        exit(0);
49    });
50    if let Err(e) = try_run(args, bin_name, detected_manager) {
51        eprintln!("{BOLD}{RED}error{RESET}: {e:#}");
52        exit(1);
53    }
54}
55
56fn try_run<I, A>(
57    args: I,
58    bin_name: Option<String>,
59    detected_manager: Option<String>,
60) -> anyhow::Result<()>
61where
62    I: IntoIterator<Item = A>,
63    A: Into<OsString> + Clone,
64{
65    let detected_manager = detected_manager.and_then(|p| p.parse::<PackageManager>().ok());
66    let args = args::parse(args.into_iter().map(Into::into).collect(), bin_name)?;
67    let defaults = args::Args::default();
68    let args::Args {
69        skip,
70        tauri_version,
71        manager,
72        project_name,
73        template,
74        force,
75        identifier,
76    } = args;
77    let cwd = std::env::current_dir()?;
78
79    // Project name used for the project directory name and productName in tauri.conf.json
80    // and if valid, it will also be used in Cargo.toml, Package.json ...etc
81    let project_name = match project_name {
82        Some(name) => name,
83        None => {
84            let default = defaults
85                .project_name
86                .context("default project_name not set")?;
87            if skip {
88                default
89            } else {
90                Input::<String>::with_theme(&ColorfulTheme::default())
91                    .with_prompt("Project name")
92                    .default(default)
93                    .interact_text()?
94                    .trim()
95                    .into()
96            }
97        }
98    };
99
100    let target_dir = cwd.join(&project_name);
101
102    // Package name used in Cargo.toml, Package.json ...etc
103    let package_name = if utils::is_valid_pkg_name(&project_name) {
104        project_name.clone()
105    } else {
106        let valid_name = utils::to_valid_pkg_name(&project_name);
107        if skip {
108            valid_name
109        } else {
110            Input::<String>::with_theme(&ColorfulTheme::default())
111            .with_prompt("Package name")
112            .default(valid_name.clone())
113            .with_initial_text(valid_name)
114            .validate_with(|input: &String| {
115                if utils::is_valid_pkg_name(input) {
116                    Ok(())
117                } else {
118                    Err("Package name should only include lowercase alphanumeric character and hyphens \"-\" and doesn't start with numbers")
119                }
120            })
121            .interact_text()?
122            .trim().to_string()
123        }
124    };
125
126    let identifier = match identifier {
127        Some(name) => name,
128        None => {
129            let valid_user_name = utils::to_valid_pkg_name(&whoami::username());
130            let default = format!("com.{valid_user_name}.{package_name}");
131            if skip {
132                default
133            } else {
134                Input::<String>::with_theme(&ColorfulTheme::default())
135                    .with_prompt("Identifier")
136                    .default(default)
137                    .interact_text()?
138                    .trim()
139                    .into()
140            }
141        }
142    };
143
144    // Confirm deleting the target project directory if not empty
145    if target_dir.exists() && target_dir.read_dir()?.next().is_some() {
146        let overwrite = if force {
147            true
148        } else if skip {
149            false
150        } else {
151            Confirm::with_theme(&ColorfulTheme::default())
152                .with_prompt(format!(
153                    "{} directory is not empty, do you want to overwrite?",
154                    if target_dir == cwd {
155                        "Current".to_string()
156                    } else {
157                        target_dir
158                            .file_name()
159                            .unwrap()
160                            .to_string_lossy()
161                            .to_string()
162                    }
163                ))
164                .default(false)
165                .interact()?
166        };
167        if !overwrite {
168            eprintln!("{BOLD}{RED}✘{RESET} Directory is not empty, Operation Cancelled");
169            exit(1);
170        }
171    };
172
173    // Detect category if a package manger is not passed on the command line
174    let category = if manager.is_none() {
175        // Filter managers if a template is passed on the command line
176        let managers = PackageManager::ALL.to_vec();
177        let managers = template
178            .map(|t| {
179                managers
180                    .iter()
181                    .copied()
182                    .filter(|p| p.templates_no_flavors().contains(&t.without_flavor()))
183                    .collect::<Vec<_>>()
184            })
185            .unwrap_or(managers);
186
187        // Filter categories based on the detected package mangers
188        let categories = Category::ALL.to_vec();
189        let mut categories = categories
190            .into_iter()
191            .filter(|c| c.package_managers().iter().any(|p| managers.contains(p)))
192            .collect::<Vec<_>>();
193
194        // sort categories so the most relevant category
195        // based on the auto-detected package manager is selected first
196        categories.sort_by(|a, b| {
197            detected_manager
198                .map(|p| b.package_managers().contains(&p))
199                .unwrap_or(false)
200                .cmp(
201                    &detected_manager
202                        .map(|p| a.package_managers().contains(&p))
203                        .unwrap_or(false),
204                )
205        });
206
207        // Skip prompt, if only one category is detected or explicit skip requested by `-y/--yes` flag
208        if categories.len() == 1 || skip {
209            Some(categories[0])
210        } else {
211            let index = Select::with_theme(&ColorfulTheme::default())
212                .with_prompt("Choose which language to use for your frontend")
213                .items(&categories)
214                .default(0)
215                .interact()?;
216            Some(categories[index])
217        }
218    } else {
219        None
220    };
221
222    // Package manager which will be used for rendering the template
223    // and the after-render instructions
224    let pkg_manager = match manager {
225        Some(manager) => manager,
226        None => {
227            if let Some(category) = category {
228                let mut managers = category.package_managers().to_owned();
229                // sort managers so the auto-detected package manager is selected first
230                managers.sort_by(|a, b| {
231                    detected_manager
232                        .map(|p| p == *b)
233                        .unwrap_or(false)
234                        .cmp(&detected_manager.map(|p| p == *a).unwrap_or(false))
235                });
236                // Skip prompt, if only one package manager is detected or explicit skip requested by `-y/--yes` flag
237                if managers.len() == 1 || skip {
238                    managers[0]
239                } else {
240                    let index = Select::with_theme(&ColorfulTheme::default())
241                        .with_prompt("Choose your package manager")
242                        .items(&managers)
243                        .default(0)
244                        .interact()?;
245                    managers[index]
246                }
247            } else {
248                defaults.manager.context("default manager not set")?
249            }
250        }
251    };
252
253    let templates_no_flavors = pkg_manager.templates_no_flavors();
254
255    // Template to render
256    let template = match template {
257        Some(template) => template,
258        None => {
259            if skip {
260                defaults.template.context("default template not set")?
261            } else {
262                let index = Select::with_theme(&ColorfulTheme::default())
263                    .with_prompt("Choose your UI template")
264                    .items(
265                        &templates_no_flavors
266                            .iter()
267                            .map(|t| t.select_text())
268                            .collect::<Vec<_>>(),
269                    )
270                    .default(0)
271                    .interact()?;
272
273                let template = templates_no_flavors[index];
274
275                // Prompt for flavors if the template has more than one flavor
276                let flavors = template.flavors(pkg_manager);
277                if let Some(flavors) = flavors {
278                    let index = Select::with_theme(&ColorfulTheme::default())
279                        .with_prompt("Choose your UI flavor")
280                        .items(flavors)
281                        .default(0)
282                        .interact()?;
283                    template.from_flavor(flavors[index])
284                } else {
285                    template
286                }
287            }
288        }
289    };
290
291    // If the package manager and the template are specified on the command line
292    // then almost all prompts are skipped so we need to make sure that the combination
293    // is valid, otherwise, we error and exit
294    if !pkg_manager.templates().contains(&template) {
295        eprintln!(
296            "{BOLD}{RED}error{RESET}: the {GREEN}{template}{RESET} template is not suppported for the {GREEN}{pkg_manager}{RESET} package manager\n       possible templates for {GREEN}{pkg_manager}{RESET} are: [{}]\n       or maybe you meant to use another package manager\n       possible package managers for {GREEN}{template}{RESET} are: [{}]" ,
297            templates_no_flavors.iter().map(|e|format!("{GREEN}{e}{RESET}")).collect::<Vec<_>>().join(", "),
298            template.possible_package_managers().iter().map(|e|format!("{GREEN}{e}{RESET}")).collect::<Vec<_>>().join(", "),
299        );
300        exit(1);
301    }
302
303    // Remove the target dir contents before rendering the template
304    // SAFETY: Upon reaching this line, the user already accepted to overwrite
305    if target_dir.exists() {
306        #[inline(always)]
307        fn clean_dir(dir: &std::path::PathBuf) -> anyhow::Result<()> {
308            for entry in fs::read_dir(dir)?.flatten() {
309                let path = entry.path();
310                if entry.file_type()?.is_dir() {
311                    if entry.file_name() != ".git" {
312                        clean_dir(&path)?;
313                        std::fs::remove_dir(path)?;
314                    }
315                } else {
316                    fs::remove_file(path)?;
317                }
318            }
319            Ok(())
320        }
321        clean_dir(&target_dir)?;
322    } else {
323        let _ = fs::create_dir_all(&target_dir);
324    }
325
326    // Render the template
327    template.render(
328        &target_dir,
329        pkg_manager,
330        &project_name,
331        &package_name,
332        &identifier,
333        tauri_version,
334    )?;
335
336    // Print post-render instructions
337    println!();
338    print!("Template created!");
339    let has_missing = print_missing_deps(pkg_manager, template, tauri_version);
340    if has_missing {
341        let prereqs_url = match tauri_version {
342            TauriVersion::V1 => "https://v1.tauri.app/v1/guides/getting-started/prerequisites/",
343            TauriVersion::V2 => "https://tauri.app/start/prerequisites/",
344        };
345
346        println!("Make sure you have installed the prerequisites for your OS: {BLUE}{BOLD}{prereqs_url}{RESET}, then run:");
347    } else {
348        println!(" To get started run:")
349    }
350    if target_dir != cwd {
351        println!(
352            "  cd {}",
353            if project_name.contains(' ') {
354                format!("\"{project_name}\"")
355            } else {
356                project_name
357            }
358        );
359    }
360    if let Some(cmd) = pkg_manager.install_cmd() {
361        println!("  {cmd}");
362    }
363    if matches!(tauri_version, TauriVersion::V1) {
364        println!("  {} tauri dev", pkg_manager.run_cmd());
365    } else {
366        println!("  {} tauri android init", pkg_manager.run_cmd());
367        #[cfg(target_os = "macos")]
368        println!("  {} tauri ios init", pkg_manager.run_cmd());
369
370        println!();
371        println!("For Desktop development, run:");
372        println!("  {} tauri dev", pkg_manager.run_cmd());
373        println!();
374        println!("For Android development, run:");
375        println!("  {} tauri android dev", pkg_manager.run_cmd());
376        #[cfg(target_os = "macos")]
377        {
378            println!();
379            println!("For iOS development, run:");
380            println!("  {} tauri ios dev", pkg_manager.run_cmd());
381        }
382    }
383    println!();
384    Ok(())
385}