427 lines
15 KiB
Rust
427 lines
15 KiB
Rust
|
use anyhow::Context;
|
||
|
use dialoguer::{Confirm, Input, Select};
|
||
|
use std::{ffi::OsString, fs, process::exit};
|
||
|
|
||
|
use crate::{
|
||
|
category::Category,
|
||
|
colors::*,
|
||
|
deps::print_missing_deps,
|
||
|
package_manager::PackageManager,
|
||
|
theme::ColorfulTheme,
|
||
|
};
|
||
|
|
||
|
mod category;
|
||
|
mod cli;
|
||
|
mod colors;
|
||
|
mod deps;
|
||
|
mod lte;
|
||
|
mod manifest;
|
||
|
mod package_manager;
|
||
|
mod template;
|
||
|
mod theme;
|
||
|
|
||
|
pub mod internal {
|
||
|
//! Re-export of create-tauri-app internals
|
||
|
//!
|
||
|
//! ## Warning
|
||
|
//!
|
||
|
//! This is meant to be used internally only so use at your own risk
|
||
|
//! and expect APIs to break without a prior notice.
|
||
|
pub mod package_manager {
|
||
|
pub use crate::package_manager::*;
|
||
|
}
|
||
|
|
||
|
pub mod template {
|
||
|
pub use crate::template::*;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pub fn run<I, A>(args: I, bin_name: Option<String>, detected_manager: Option<String>)
|
||
|
where
|
||
|
I: IntoIterator<Item = A>,
|
||
|
A: Into<OsString> + Clone,
|
||
|
{
|
||
|
let _ = ctrlc::set_handler(move || {
|
||
|
eprint!("\x1b[?25h");
|
||
|
exit(0);
|
||
|
});
|
||
|
if let Err(e) = try_run(args, bin_name, detected_manager) {
|
||
|
eprintln!("{BOLD}{RED}error{RESET}: {e:#}");
|
||
|
exit(1);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
fn try_run<I, A>(
|
||
|
args: I,
|
||
|
bin_name: Option<String>,
|
||
|
detected_manager: Option<String>,
|
||
|
) -> anyhow::Result<()>
|
||
|
where
|
||
|
I: IntoIterator<Item = A>,
|
||
|
A: Into<OsString> + Clone,
|
||
|
{
|
||
|
let detected_manager = detected_manager.and_then(|p| p.parse::<PackageManager>().ok());
|
||
|
let args = cli::parse(args.into_iter().map(Into::into).collect(), bin_name)?;
|
||
|
let defaults = cli::Args::default();
|
||
|
let cli::Args {
|
||
|
skip,
|
||
|
mobile,
|
||
|
alpha,
|
||
|
manager,
|
||
|
project_name,
|
||
|
template,
|
||
|
} = args;
|
||
|
let cwd = std::env::current_dir()?;
|
||
|
|
||
|
// Allow `--mobile` to only be used with `--alpha` for now
|
||
|
// TODO: remove this limitation once tauri@v2 is stable
|
||
|
if let Some(mobile) = mobile {
|
||
|
if mobile && !alpha {
|
||
|
eprintln!(
|
||
|
"{BOLD}{RED}error{RESET}: `{GREEN}--mobile{RESET}` option is only available if `{GREEN}--alpha{RESET}` option is also used"
|
||
|
);
|
||
|
exit(1);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Project name used for the project directory name and productName in tauri.conf.json
|
||
|
// and if valid, it will also be used in Cargo.toml, Package.json ...etc
|
||
|
let project_name = match project_name {
|
||
|
Some(name) => name,
|
||
|
None => {
|
||
|
if skip {
|
||
|
defaults
|
||
|
.project_name
|
||
|
.context("default project_name not set")?
|
||
|
} else {
|
||
|
Input::<String>::with_theme(&ColorfulTheme::default())
|
||
|
.with_prompt("Project name")
|
||
|
.default("tauri-app".into())
|
||
|
.interact_text()?
|
||
|
.trim()
|
||
|
.into()
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
let target_dir = cwd.join(&project_name);
|
||
|
|
||
|
// Package name used in Cargo.toml, Package.json ...etc
|
||
|
let package_name = if is_valid_pkg_name(&project_name) {
|
||
|
project_name.clone()
|
||
|
} else {
|
||
|
let valid_name = to_valid_pkg_name(&project_name);
|
||
|
if skip {
|
||
|
valid_name
|
||
|
} else {
|
||
|
Input::<String>::with_theme(&ColorfulTheme::default())
|
||
|
.with_prompt("Package name")
|
||
|
.default(valid_name.clone())
|
||
|
.with_initial_text(valid_name)
|
||
|
.validate_with(|input: &String| {
|
||
|
if is_valid_pkg_name(input) {
|
||
|
Ok(())
|
||
|
} else {
|
||
|
Err("Package name should only include lowercase alphanumeric character and hyphens \"-\" and doesn't start with numbers")
|
||
|
}
|
||
|
})
|
||
|
.interact_text()?
|
||
|
.trim().to_string()
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Confirm deleting the target project directory if not empty
|
||
|
if target_dir.exists() && target_dir.read_dir()?.next().is_some() {
|
||
|
let overrwite = if skip {
|
||
|
false
|
||
|
} else {
|
||
|
Confirm::with_theme(&ColorfulTheme::default())
|
||
|
.with_prompt(format!(
|
||
|
"{} directory is not empty, do you want to overwrite?",
|
||
|
if target_dir == cwd {
|
||
|
"Current directory".to_string()
|
||
|
} else {
|
||
|
target_dir
|
||
|
.file_name()
|
||
|
.unwrap()
|
||
|
.to_string_lossy()
|
||
|
.to_string()
|
||
|
}
|
||
|
))
|
||
|
.default(false)
|
||
|
.interact()?
|
||
|
};
|
||
|
if !overrwite {
|
||
|
eprintln!("{BOLD}{RED}✘{RESET} Operation Cancelled");
|
||
|
exit(1);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Prompt for category if a package manger is not passed on the command line
|
||
|
let category = if manager.is_none() && !skip {
|
||
|
// Filter managers if a template is passed on the command line
|
||
|
let managers = PackageManager::ALL.to_vec();
|
||
|
let managers = args
|
||
|
.template
|
||
|
.map(|t| {
|
||
|
managers
|
||
|
.iter()
|
||
|
.copied()
|
||
|
.filter(|p| p.templates_no_flavors().contains(&t.without_flavor()))
|
||
|
.collect::<Vec<_>>()
|
||
|
})
|
||
|
.unwrap_or(managers);
|
||
|
|
||
|
// Filter categories based on the detected package mangers
|
||
|
let categories = Category::ALL.to_vec();
|
||
|
let mut categories = categories
|
||
|
.into_iter()
|
||
|
.filter(|c| c.package_managers().iter().any(|p| managers.contains(p)))
|
||
|
.collect::<Vec<_>>();
|
||
|
|
||
|
// sort categories so the most relevant category
|
||
|
// based on the auto-detected package manager is selected first
|
||
|
categories.sort_by(|a, b| {
|
||
|
detected_manager
|
||
|
.map(|p| b.package_managers().contains(&p))
|
||
|
.unwrap_or(false)
|
||
|
.cmp(
|
||
|
&detected_manager
|
||
|
.map(|p| a.package_managers().contains(&p))
|
||
|
.unwrap_or(false),
|
||
|
)
|
||
|
});
|
||
|
|
||
|
// If only one category is detected, skip prompt
|
||
|
if categories.len() == 1 {
|
||
|
Some(categories[0])
|
||
|
} else {
|
||
|
let index = Select::with_theme(&ColorfulTheme::default())
|
||
|
.with_prompt("Choose which language to use for your frontend")
|
||
|
.items(&categories)
|
||
|
.default(0)
|
||
|
.interact()?;
|
||
|
Some(categories[index])
|
||
|
}
|
||
|
} else {
|
||
|
None
|
||
|
};
|
||
|
|
||
|
// Package manager which will be used for rendering the template
|
||
|
// and the after-render instructions
|
||
|
let pkg_manager = match manager {
|
||
|
Some(manager) => manager,
|
||
|
None => {
|
||
|
if skip {
|
||
|
defaults.manager.context("default manager not set")?
|
||
|
} else {
|
||
|
let category = category.context("category shouldn't be None at this point")?;
|
||
|
|
||
|
let mut managers = category.package_managers().to_owned();
|
||
|
// sort managers so the auto-detected package manager is selected first
|
||
|
managers.sort_by(|a, b| {
|
||
|
detected_manager
|
||
|
.map(|p| p == *b)
|
||
|
.unwrap_or(false)
|
||
|
.cmp(&detected_manager.map(|p| p == *a).unwrap_or(false))
|
||
|
});
|
||
|
// If only one package manager is detected, skip prompt
|
||
|
if managers.len() == 1 {
|
||
|
managers[0]
|
||
|
} else {
|
||
|
let index = Select::with_theme(&ColorfulTheme::default())
|
||
|
.with_prompt("Choose your package manager")
|
||
|
.items(&managers)
|
||
|
.default(0)
|
||
|
.interact()?;
|
||
|
managers[index]
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
let templates_no_flavors = pkg_manager.templates_no_flavors();
|
||
|
|
||
|
// Template to render
|
||
|
let template = match template {
|
||
|
Some(template) => template,
|
||
|
None => {
|
||
|
if skip {
|
||
|
defaults.template.context("default template not set")?
|
||
|
} else {
|
||
|
let index = Select::with_theme(&ColorfulTheme::default())
|
||
|
.with_prompt("Choose your UI template")
|
||
|
.items(
|
||
|
&templates_no_flavors
|
||
|
.iter()
|
||
|
.map(|t| t.select_text())
|
||
|
.collect::<Vec<_>>(),
|
||
|
)
|
||
|
.default(0)
|
||
|
.interact()?;
|
||
|
|
||
|
let template = templates_no_flavors[index];
|
||
|
|
||
|
// Prompt for flavors if the template has more than one flavor
|
||
|
let flavors = template.flavors(pkg_manager);
|
||
|
if let Some(flavors) = flavors {
|
||
|
let index = Select::with_theme(&ColorfulTheme::default())
|
||
|
.with_prompt("Choose your UI flavor")
|
||
|
.items(flavors)
|
||
|
.default(0)
|
||
|
.interact()?;
|
||
|
template.from_flavor(flavors[index])
|
||
|
} else {
|
||
|
template
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// Prompt for wether to bootstrap a mobile-friendly tauri project
|
||
|
// This should only be prompted if `--alpha` is used on the command line and `--mobile` wasn't.
|
||
|
// TODO: remove this limitation once tauri@v2 is stable
|
||
|
let mobile = match mobile {
|
||
|
Some(mobile) => mobile,
|
||
|
None => {
|
||
|
Confirm::with_theme(&ColorfulTheme::default())
|
||
|
.with_prompt("Would you like to setup the project for mobile as well?")
|
||
|
.default(false)
|
||
|
.interact()?
|
||
|
// if skip || !alpha {
|
||
|
// defaults.mobile.context("default mobile not set")?
|
||
|
// } else {
|
||
|
// Confirm::with_theme(&ColorfulTheme::default())
|
||
|
// .with_prompt("Would you like to setup the project for mobile as well?")
|
||
|
// .default(false)
|
||
|
// .interact()?
|
||
|
// }
|
||
|
}
|
||
|
};
|
||
|
|
||
|
// If the package manager and the template are specified on the command line
|
||
|
// then almost all prompts are skipped so we need to make sure that the combination
|
||
|
// is valid, otherwise, we error and exit
|
||
|
if !pkg_manager.templates().contains(&template) {
|
||
|
eprintln!(
|
||
|
"{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: [{}]" ,
|
||
|
templates_no_flavors.iter().map(|e|format!("{GREEN}{e}{RESET}")).collect::<Vec<_>>().join(", "),
|
||
|
template.possible_package_managers().iter().map(|e|format!("{GREEN}{e}{RESET}")).collect::<Vec<_>>().join(", "),
|
||
|
);
|
||
|
exit(1);
|
||
|
}
|
||
|
|
||
|
// Remove the target dir contents before rendering the template
|
||
|
// SAFETY: Upon reaching this line, the user already accepted to overwrite
|
||
|
if target_dir.exists() {
|
||
|
#[inline(always)]
|
||
|
fn clean_dir(dir: &std::path::PathBuf) -> anyhow::Result<()> {
|
||
|
for entry in fs::read_dir(dir)?.flatten() {
|
||
|
let path = entry.path();
|
||
|
if entry.file_type()?.is_dir() {
|
||
|
if entry.file_name() != ".git" {
|
||
|
clean_dir(&path)?;
|
||
|
std::fs::remove_dir(path)?;
|
||
|
}
|
||
|
} else {
|
||
|
fs::remove_file(path)?;
|
||
|
}
|
||
|
}
|
||
|
Ok(())
|
||
|
}
|
||
|
clean_dir(&target_dir)?;
|
||
|
} else {
|
||
|
let _ = fs::create_dir_all(&target_dir);
|
||
|
}
|
||
|
|
||
|
// Render the template
|
||
|
template.render(
|
||
|
&target_dir,
|
||
|
pkg_manager,
|
||
|
&project_name,
|
||
|
&package_name,
|
||
|
alpha,
|
||
|
mobile,
|
||
|
)?;
|
||
|
|
||
|
// Print post-render instructions
|
||
|
println!();
|
||
|
print!("Template created!");
|
||
|
print_missing_deps(pkg_manager, template, alpha);
|
||
|
if target_dir != cwd {
|
||
|
println!(
|
||
|
" cd {}",
|
||
|
if project_name.contains(' ') {
|
||
|
format!("\"{project_name}\"")
|
||
|
} else {
|
||
|
project_name.clone()
|
||
|
}
|
||
|
);
|
||
|
}
|
||
|
if let Some(cmd) = pkg_manager.install_cmd() {
|
||
|
println!(" {cmd}");
|
||
|
}
|
||
|
if !mobile {
|
||
|
println!(" {} run", pkg_manager.run_cmd());
|
||
|
println!();
|
||
|
println!("For Release, run:");
|
||
|
println!(" {} run --release", pkg_manager.run_cmd());
|
||
|
println!();
|
||
|
println!("For Small Release, run:");
|
||
|
println!(" {} run --profile=small", pkg_manager.run_cmd());
|
||
|
} else {
|
||
|
println!(" {} makepad android --abi=all install-toolchain", pkg_manager.run_cmd());
|
||
|
#[cfg(target_os = "macos")]
|
||
|
println!(" {} makepad apple ios install-toolchain", pkg_manager.run_cmd());
|
||
|
|
||
|
println!();
|
||
|
println!("For Desktop development, run:");
|
||
|
println!(" {} run", pkg_manager.run_cmd());
|
||
|
println!();
|
||
|
println!("For Release, run:");
|
||
|
println!(" {} run --release", pkg_manager.run_cmd());
|
||
|
println!();
|
||
|
println!("For Small Release, run:");
|
||
|
println!(" {} run --profile=small", pkg_manager.run_cmd());
|
||
|
println!();
|
||
|
println!("For Android development, run:");
|
||
|
println!(" {} makepad android run", pkg_manager.run_cmd());
|
||
|
#[cfg(target_os = "macos")]
|
||
|
{
|
||
|
println!();
|
||
|
println!("For iOS Simulator development, run:");
|
||
|
println!(" {} makepad --org-id=123456 --org={}.com --app={} run-sim --release", pkg_manager.run_cmd(), project_name, project_name);
|
||
|
println!();
|
||
|
println!("For iOS Device development, run:");
|
||
|
println!(" {} makepad --org-id=123456 --org={}.com --app={} run-device {}", pkg_manager.run_cmd(), project_name, project_name, project_name);
|
||
|
}
|
||
|
}
|
||
|
println!();
|
||
|
Ok(())
|
||
|
}
|
||
|
|
||
|
fn is_valid_pkg_name(project_name: &str) -> bool {
|
||
|
let mut chars = project_name.chars().peekable();
|
||
|
!project_name.is_empty()
|
||
|
&& !chars.peek().map(|c| c.is_ascii_digit()).unwrap_or_default()
|
||
|
&& !chars.any(|ch| !(ch.is_alphanumeric() || ch == '-' || ch == '_') || ch.is_uppercase())
|
||
|
}
|
||
|
|
||
|
fn to_valid_pkg_name(project_name: &str) -> String {
|
||
|
let ret = project_name
|
||
|
.trim()
|
||
|
.to_lowercase()
|
||
|
.replace([':', ';', ' ', '~'], "-")
|
||
|
.replace(['.', '\\', '/'], "");
|
||
|
|
||
|
let ret = ret
|
||
|
.chars()
|
||
|
.skip_while(|ch| ch.is_ascii_digit() || *ch == '-')
|
||
|
.collect::<String>();
|
||
|
|
||
|
if ret.is_empty() {
|
||
|
"tauri-app".to_string()
|
||
|
} else {
|
||
|
ret
|
||
|
}
|
||
|
}
|