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}