Skip to main content

create_janustack/
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::{JanudocsSubTemplate, Template},
9    utils::colors::*,
10};
11
12pub mod utils;
13mod args;
14mod package_manager;
15mod template;
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("\nWelcome to Janustack\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 = "janustack-project";
56    let project_name = match project_name {
57        Some(name) => to_valid_pkg_name(&name),
58        None => loop {
59            let input = prompts::input(
60                "Enter the name for your new project (relative to current directory)\n",
61                Some(default_project_name),
62                false,
63            )?
64            .trim()
65            .to_string();
66            if !is_valid_pkg_name(&input) {
67                eprintln!(
68                    "{BOLD}{RED}✘{RESET} Invalid project name: {BOLD}{YELLOW}{input}{RESET}, {}",
69                    "package name should only include lowercase alphanumeric character and hyphens \"-\" and doesn't start with numbers"
70                );
71                default_project_name = to_valid_pkg_name(&input).leak();
72                continue;
73            };
74            break input;
75        },
76    };
77    let target_dir = cwd.join(&project_name);
78
79    if target_dir.exists() && target_dir.read_dir()?.next().is_some() {
80        let overwrite = force
81            || prompts::confirm(
82                &format!(
83                    "{} directory is not empty, do you want to overwrite?",
84                    if target_dir == cwd {
85                        "Current".to_string()
86                    } else {
87                        target_dir
88                            .file_name()
89                            .unwrap()
90                            .to_string_lossy()
91                            .to_string()
92                    }
93                ),
94                false,
95            )?;
96        if !overwrite {
97            eprintln!("{BOLD}{RED}✘{RESET} Directory is not empty, Operation Cancelled");
98            exit(1);
99        }
100    };
101
102    let pkg_manager = manager.unwrap_or(match detected_manager {
103        Some(manager) => manager,
104        None => defaults.manager.context("default manager not set")?,
105    });
106
107    let templates_no_flavors = pkg_manager.templates_no_flavors();
108
109    let template = match template {
110        Some(template) => template,
111        None => {
112            let selected_template =
113                prompts::select("Select a framework:", &templates_no_flavors, Some(0))?.unwrap();
114
115            match selected_template {
116                Template::Janudocs(None) => {
117                    let sub_templates =
118                        vec![JanudocsSubTemplate::React, JanudocsSubTemplate::Solid];
119
120                    let sub_template =
121                        prompts::select("Select an Janudocs template:", &sub_templates, Some(0))?
122                            .unwrap();
123
124                    Template::Janudocs(Some(*sub_template))
125                }
126                _ => *selected_template,
127            }
128        }
129    };
130
131    if target_dir.exists() {
132        #[inline(always)]
133        fn clean_dir(dir: &std::path::PathBuf) -> anyhow::Result<()> {
134            for entry in fs::read_dir(dir)?.flatten() {
135                let path = entry.path();
136                if entry.file_type()?.is_dir() {
137                    if entry.file_name() != ".git" {
138                        clean_dir(&path)?;
139                        std::fs::remove_dir(path)?;
140                    }
141                } else {
142                    fs::remove_file(path)?;
143                }
144            }
145            Ok(())
146        }
147        clean_dir(&target_dir)?;
148    } else {
149        fs::create_dir_all(&target_dir)?;
150    }
151
152    // Render the template
153    template.render(&target_dir, pkg_manager, &project_name, &project_name)?;
154
155    handle_brand_text("\nNext steps:\n");
156
157    if target_dir != cwd {
158        handle_brand_text(&format!(
159            "1. cd {} \n",
160            if project_name.contains(' ') {
161                format!("\"{project_name}\"")
162            } else {
163                project_name.to_string()
164            }
165        ));
166    }
167    if let Some(cmd) = pkg_manager.install_cmd() {
168        handle_brand_text(&format!("2. {cmd}\n"));
169    }
170    handle_brand_text(&format!("3. {}\n", get_run_cmd(&pkg_manager, &template)));
171    
172    handle_brand_text("\nUpdate all dependencies:\n");
173    handle_brand_text(&format!("{} pons -r\n", pkg_manager.update_cmd()));
174    
175    handle_brand_text("\nLike create-janustack? Give a star on GitHub:\n");
176    handle_brand_text(&format!("https://github.com/janustack/create-janustack"));
177
178    Ok(())
179}
180
181fn is_valid_pkg_name(project_name: &str) -> bool {
182    let mut chars = project_name.chars().peekable();
183    !project_name.is_empty()
184        && !chars.peek().map(|c| c.is_ascii_digit()).unwrap_or_default()
185        && !chars.any(|ch| !(ch.is_alphanumeric() || ch == '-' || ch == '_') || ch.is_uppercase())
186}
187
188fn to_valid_pkg_name(project_name: &str) -> String {
189    let ret = project_name
190        .trim()
191        .to_lowercase()
192        .replace([':', ';', ' ', '~'], "-")
193        .replace(['.', '\\', '/'], "");
194
195    let ret = ret
196        .chars()
197        .skip_while(|ch| ch.is_ascii_digit() || *ch == '-')
198        .collect::<String>();
199
200    if ret.is_empty() || !is_valid_pkg_name(&ret) {
201        "janustack-project".to_string()
202    } else {
203        ret
204    }
205}
206
207fn get_run_cmd(pkg_manager: &PackageManager, template: &Template) -> &'static str {
208    match template {
209        _ => pkg_manager.default_cmd(),
210    }
211}