1use super::RapidCommand;
2use crate::{
3 cli::{current_directory, Config},
4 constants::BOLT_EMOJI,
5 tui::{indent, logo, rapid_logo},
6};
7use clap::{Arg, ArgAction, ArgMatches, Command};
8use colorful::{Color, Colorful};
9use include_dir::{include_dir, Dir};
10use log::error;
11use requestty::{prompt, prompt_one, Answer, Question};
12use spinach::Spinach;
13use std::{
14 fs::remove_dir_all,
15 path::PathBuf,
16 process::{exit, Command as StdCommand},
17 thread, time,
18};
19
20static PROJECT_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/templates/server");
22static REMIX_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/templates/remix");
23static NEXTJS_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/templates/nextjs");
24static REMIX_WITHOUT_CLERK_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/templates/remix-without-clerk");
25
26pub struct New {}
27
28impl RapidCommand for New {
29 fn cmd() -> clap::Command {
30 Command::new("new")
31 .about("Creates a new Rapid project at the current working directory!")
32 .arg(
33 Arg::new("remix")
34 .long("remix")
35 .required(false)
36 .conflicts_with("nextjs")
37 .conflicts_with("server")
38 .action(ArgAction::SetTrue)
39 .value_name("REMIX")
40 .help("Scaffolds a fullstack Rapid project with remix!"),
41 )
42 .arg(
43 Arg::new("nextjs")
44 .long("nextjs")
45 .required(false)
46 .conflicts_with("server")
47 .conflicts_with("remix")
48 .action(ArgAction::SetTrue)
49 .value_name("NEXTJS")
50 .help("Scaffolds a fullstack Rapid project with Nextjs!"),
51 )
52 .arg(
53 Arg::new("server")
54 .long("server")
55 .required(false)
56 .conflicts_with("nextjs")
57 .conflicts_with("remix")
58 .action(ArgAction::SetTrue)
59 .value_name("SERVER")
60 .help("Scaffolds a server-side only Rapid project!"),
61 )
62 }
63
64 fn execute(_: &Config, args: &ArgMatches) -> Result<(), crate::cli::CliError<'static>> {
65 println!("{}", logo());
66 parse_new_args(args);
67 Ok(())
68 }
69}
70
71pub fn parse_new_args(args: &ArgMatches) {
72 let current_working_directory = current_directory();
74
75 let is_remix = args.get_one::<bool>("remix").unwrap_or(&true);
76 let is_server = args.get_one::<bool>("server").unwrap_or(&false);
77 let is_nextjs = args.get_one::<bool>("nextjs").unwrap_or(&false);
78
79 match (is_remix, is_server, is_nextjs) {
80 (false, false, true) => {
81 init_nextjs_template(current_working_directory);
82 }
83 (true, false, false) => {
84 init_remix_template(current_working_directory);
85 }
86 (false, true, false) => {
87 init_server_template(current_working_directory, "server");
88 }
89 (false, false, false) => {
90 init_remix_template(current_working_directory);
91 }
92 _ => unreachable!(),
93 }
94}
95
96pub fn init_remix_template(current_working_directory: PathBuf) {
98 let project_name = prompt_one(
100 Question::input("project_name")
101 .message("What will your project be called?")
102 .default("my-app")
103 .build(),
104 )
105 .expect("Error: Could not scaffold project. Please try again!");
106
107 let project_name = project_name.as_string().unwrap();
108
109 if !project_name.chars().all(|x| x.is_alphanumeric() || x == '-' || x == '_') {
111 error!("Aborting...your project name may only contain alphanumeric characters along with '-' and '_'...");
112 exit(64);
113 }
114
115 let path = current_working_directory.join(project_name);
116
117 if path.exists() {
119 let force = prompt_one(
120 Question::confirm("force_delete")
121 .message("Your specified directory is not empty and has files currently in it, do you want to overwrite?")
122 .default(false)
123 .build(),
124 )
125 .expect("Error: Could not scaffold project. Please try again!");
126
127 match !force.as_bool().unwrap() {
128 true => {
129 exit(64);
130 }
131 false => {
132 remove_dir_all(&path).expect("Error: Could not scaffold project. The specified directory must be empty. Please try again!");
133 }
134 }
135 }
136
137 let manager_choices = vec!["pnpm", "npm", "yarn"];
139
140 println!("{}", indent(1));
141
142 let package_manager = requestty::Question::select("packageManagerSelect")
143 .message("Which package manager would you like to use?:")
144 .choices(manager_choices)
145 .page_size(6)
146 .build();
147
148 let package_manager = prompt(vec![package_manager]).expect("Error: Could not scaffold project. Please try again!");
149
150 let package_manager = match package_manager.get("packageManagerSelect") {
151 Some(Answer::ListItem(choice)) => choice.text.clone(),
152 _ => {
153 error!(
154 "Aborting...an error occurred while trying to parse package manager selection. Please try again!"
155 );
156 exit(64);
157 }
158 };
159
160 println!("{}", indent(1));
161
162 let _ = vec!["Clerk (authentication)"];
165
166 let tech_choices =
167 requestty::Question::multi_select("What technologies would you like included?").choice_with_default("Clerk (authentication)", true);
168
169 let tech_choices = prompt_one(tech_choices).expect("Error: Could not scaffold project. Please try again!");
170
171 let tech_choices = match tech_choices {
172 Answer::ListItems(choices) => choices,
173 _ => {
174 error!(
175 "{}",
176 "aborting...an error occurred while trying to parse technology choices. Please try again!"
177 );
178 exit(64);
179 }
180 };
181
182 let should_include_clerk = tech_choices.iter().any(|x| x.text == "Clerk (authentication)");
183
184 println!("{}", indent(1));
185
186 let loading = Spinach::new(format!("{}", "Initializing a new Rapid Remix application..".color(Color::LightCyan)));
187
188 StdCommand::new("sh")
190 .current_dir(current_directory())
191 .arg("-c")
192 .arg(format!("mkdir {}", project_name))
193 .spawn()
194 .unwrap()
195 .wait()
196 .expect("Error: Could not scaffold project. Please try again!");
197
198 StdCommand::new("sh")
200 .current_dir(current_directory().join(project_name))
201 .arg("-c")
202 .arg(format!("git init --quiet"))
203 .spawn()
204 .unwrap()
205 .wait()
206 .expect("Error: Could not scaffold project. Please try again!");
207
208 if !should_include_clerk {
210 REMIX_WITHOUT_CLERK_DIR
211 .extract(current_working_directory.join(project_name).clone())
212 .unwrap();
213 } else {
214 REMIX_DIR.extract(current_working_directory.join(project_name).clone()).unwrap();
215 }
216
217 StdCommand::new("sh")
219 .current_dir(current_directory().join(project_name))
220 .arg("-c")
221 .arg(format!("mv Cargo__toml Cargo.toml"))
222 .spawn()
223 .unwrap()
224 .wait()
225 .expect("Error: Could not scaffold project. Please try again!");
226
227 let timeout = time::Duration::from_millis(1000);
229 thread::sleep(timeout);
230
231 loading.succeed("Initialized!");
233
234 let loading = Spinach::new(format!("{}", "Installing dependencies...".color(Color::LightCyan)));
236
237 StdCommand::new("sh")
238 .current_dir(current_directory().join(project_name))
239 .arg("-c")
240 .arg(format!("{} install > /dev/null 2>&1", package_manager))
241 .spawn()
242 .unwrap()
243 .wait()
244 .expect("Error: Could not install project dependencies!");
245
246 loading.succeed("Installed dependencies!");
247
248 println!(
249 "\n\n{} {} {} {}",
250 format!("{}", rapid_logo()).bold(),
251 "Success".bg_blue().color(Color::White).bold(),
252 BOLT_EMOJI,
253 "Welcome to your new Rapid application with Remix!"
254 );
255
256 println!(
257 "{} {} {} {} {}",
258 "\n\nš".bold(),
259 "Next Steps".bg_blue().color(Color::White).bold(),
260 BOLT_EMOJI,
261 format!("\n\ncd {}", project_name).bold(),
262 "\nrapid run".bold()
263 );
264}
265
266pub fn init_server_template(current_working_directory: PathBuf, _: &str) {
267 let project_name = prompt_one(
269 Question::input("project_name")
270 .message("What will your project be called?")
271 .default("my-app")
272 .build(),
273 )
274 .expect("Error: Could not scaffold project. Please try again!");
275
276 let project_name = project_name.as_string().unwrap();
277
278 if !project_name.chars().all(|x| x.is_alphanumeric() || x == '-' || x == '_') {
280 error!("aborting...your project name may only contain alphanumeric characters along with '-' and '_'...");
281 exit(64);
282 }
283
284 let path = current_working_directory.join(project_name);
285
286 if path.exists() {
288 let force = prompt_one(
289 Question::confirm("force_delete")
290 .message("Your specified directory is not empty and has files currently in it, do you want to overwrite?")
291 .default(false)
292 .build(),
293 )
294 .expect("Error: Could not scaffold project. Please try again!");
295
296 match !force.as_bool().unwrap() {
297 true => {
298 exit(64);
299 }
300 false => {
301 remove_dir_all(&path).expect("Error: Could not scaffold project. The specified directory must be empty. Please try again!");
302 }
303 }
304 }
305
306 println!("{}", indent(1));
307
308 let loading = Spinach::new(format!("{}", "Initializing a new Rapid server application..".color(Color::LightCyan)));
309
310 StdCommand::new("sh")
312 .current_dir(current_directory())
313 .arg("-c")
314 .arg(format!("cargo new {} --quiet", project_name))
315 .spawn()
316 .unwrap()
317 .wait()
318 .expect("Error: Could not scaffold project. Please try again!");
319
320 StdCommand::new("sh")
321 .current_dir(current_directory().join(project_name))
322 .arg("-c")
323 .arg("cargo add rapid-web futures-util include_dir --quiet")
324 .spawn()
325 .unwrap()
326 .wait()
327 .expect("Error: Could not scaffold project. Please try again!");
328
329 remove_dir_all(current_working_directory.join(format!("{}/src", project_name))).unwrap();
331
332 PROJECT_DIR.extract(current_working_directory.join(project_name).clone()).unwrap();
334
335 let timeout = time::Duration::from_millis(675);
337 thread::sleep(timeout);
338
339 loading.stop();
341
342 println!(
343 "\n\n{} {} {} {}",
344 format!("{}", rapid_logo()).bold(),
345 "Success".bg_blue().color(Color::White).bold(),
346 BOLT_EMOJI,
347 "Welcome to your new rapid-web server application!"
348 );
349
350 println!(
351 "{} {} {} {} {}",
352 "\n\nš".bold(),
353 "Next Steps".bg_blue().color(Color::White).bold(),
354 BOLT_EMOJI,
355 format!("\n\ncd {}", project_name).bold(),
356 "\nrapid run".bold()
357 );
358}
359
360
361pub fn init_nextjs_template(current_working_directory: PathBuf) {
362 let project_name = prompt_one(
364 Question::input("project_name")
365 .message("What will your project be called?")
366 .default("my-app")
367 .build(),
368 )
369 .expect("Error: Could not scaffold project. Please try again!");
370
371 let project_name = project_name.as_string().unwrap();
372
373 if !project_name.chars().all(|x| x.is_alphanumeric() || x == '-' || x == '_') {
375 error!("aborting...your project name may only contain alphanumeric characters along with '-' and '_'...");
376 exit(64);
377 }
378
379 let path = current_working_directory.join(project_name);
380
381 if path.exists() {
383 let force = prompt_one(
384 Question::confirm("force_delete")
385 .message("Your specified directory is not empty and has files currently in it, do you want to overwrite?")
386 .default(false)
387 .build(),
388 )
389 .expect("Error: Could not scaffold project. Please try again!");
390
391 match !force.as_bool().unwrap() {
392 true => {
393 exit(64);
394 }
395 false => {
396 remove_dir_all(&path).expect("Error: Could not scaffold project. The specified directory must be empty. Please try again!");
397 }
398 }
399 }
400
401 let manager_choices = vec!["pnpm", "npm", "yarn"];
403
404 println!("{}", indent(1));
405
406 let package_manager = requestty::Question::select("packageManagerSelect")
407 .message("Which package manager would you like to use?:")
408 .choices(manager_choices)
409 .page_size(6)
410 .build();
411
412 let package_manager = prompt(vec![package_manager]).expect("Error: Could not scaffold project. Please try again!");
413
414 let package_manager = match package_manager.get("packageManagerSelect") {
415 Some(Answer::ListItem(choice)) => choice.text.clone(),
416 _ => {
417 println!(
418 "{}",
419 "Aborting...an error occurred while trying to parse package manager selection. Please try again!"
420 .bold()
421 .color(Color::Red)
422 );
423 exit(64);
424 }
425 };
426
427 println!("{}", indent(1));
428
429 let loading = Spinach::new(format!("{}", "Initializing a new Rapid Nextjs application..".color(Color::LightCyan)));
430
431 StdCommand::new("sh")
433 .current_dir(current_directory())
434 .arg("-c")
435 .arg(format!("mkdir {}", project_name))
436 .spawn()
437 .unwrap()
438 .wait()
439 .expect("Error: Could not scaffold project. Please try again!");
440
441 StdCommand::new("sh")
443 .current_dir(current_directory().join(project_name))
444 .arg("-c")
445 .arg(format!("git init --quiet"))
446 .spawn()
447 .unwrap()
448 .wait()
449 .expect("Error: Could not scaffold project. Please try again!");
450
451 NEXTJS_DIR.extract(current_working_directory.join(project_name).clone()).unwrap();
453
454 StdCommand::new("sh")
456 .current_dir(current_directory().join(project_name))
457 .arg("-c")
458 .arg(format!("mv Cargo__toml Cargo.toml"))
459 .spawn()
460 .unwrap()
461 .wait()
462 .expect("Error: Could not scaffold project. Please try again!");
463
464 let timeout = time::Duration::from_millis(1000);
466 thread::sleep(timeout);
467
468 loading.succeed("Initialized!");
470
471 let loading = Spinach::new(format!("{}", "Installing dependencies...".color(Color::LightCyan)));
473
474 StdCommand::new("sh")
475 .current_dir(current_directory().join(project_name))
476 .arg("-c")
477 .arg(format!("{} install > /dev/null 2>&1", package_manager))
478 .spawn()
479 .unwrap()
480 .wait()
481 .expect("Error: Could not install project dependencies!");
482
483 loading.succeed("Installed dependencies!");
484
485 println!(
486 "\n\n{} {} {} {}",
487 format!("{}", rapid_logo()).bold(),
488 "Success".bg_blue().color(Color::White).bold(),
489 BOLT_EMOJI,
490 "Welcome to your new Rapid application with Nextjs!"
491 );
492
493 println!(
494 "{} {} {} {} {} {}",
495 "\n\nš".bold(),
496 "Next Steps".bg_blue().color(Color::White).bold(),
497 BOLT_EMOJI,
498 format!("\n\ncd {}", project_name).bold(),
499 "\nrapid run".bold(),
500 format!("\n{} run dev", package_manager).bold()
501 );
502}