use std::path::{Path, PathBuf};
use crate::actions::abstract_action::AbstractAction;
use crate::actions::{ActionInvocation, ActionKind, ActionSpec, action_spec};
use crate::commands::{Input, InputValue};
use crate::configuration::DEFAULT_COLLECTION;
use crate::package_managers::{PackageManager, PackageManagerClient};
use crate::runners::{Runner, RunnerCommand, RunnerFactory, RunnerKind};
use crate::schematics::{CollectionFactory, SchematicOption};
use crate::utils::normalize_to_kebab_or_snake_case;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct NewAction;
impl NewAction {
pub const fn new() -> Self {
Self
}
pub fn spec(&self) -> &'static ActionSpec {
action_spec(ActionKind::New).expect("new action spec")
}
pub fn handle_invocation(&self, inputs: Vec<Input>, options: Vec<Input>) -> ActionInvocation {
<Self as AbstractAction>::handle(self, inputs, options, Vec::new())
}
pub fn build_plan(
&self,
inputs: &[Input],
options: &[Input],
current_dir: impl AsRef<Path>,
) -> Result<NewActionPlan, String> {
build_new_action_plan(inputs, options, current_dir)
}
}
impl AbstractAction for NewAction {
fn kind(&self) -> ActionKind {
ActionKind::New
}
}
pub const DEFAULT_NEW_APPLICATION_NAME: &str = "nest-app";
pub const DEFAULT_GIT_IGNORE: &str = r#"# compiled output
/dist
/node_modules
/build
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
"#;
pub fn retrieve_cols() -> Option<usize> {
std::env::var("COLUMNS")
.ok()
.and_then(|value| value.parse::<usize>().ok())
.filter(|columns| *columns > 0)
.or(Some(80))
}
pub fn exit() -> ! {
std::process::exit(1)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NewActionPlan {
pub collection: String,
pub schematic: String,
pub schematic_options: Vec<SchematicOption>,
pub schematic_command: String,
pub project_directory: PathBuf,
pub package_manager: Option<PackageManager>,
pub install_command: Option<RunnerCommand>,
pub initialize_git_command: Option<RunnerCommand>,
pub create_git_ignore: bool,
pub git_ignore_content: Option<&'static str>,
pub print_collective: bool,
pub dry_run: bool,
pub missing_information: Vec<NewActionMissingInformation>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NewActionMissingInformation {
Name,
PackageManager,
}
pub fn build_new_action_plan(
inputs: &[Input],
options: &[Input],
current_dir: impl AsRef<Path>,
) -> Result<NewActionPlan, String> {
let name = string_value(find_input(inputs, "name"))
.filter(|value| !value.is_empty())
.unwrap_or_else(|| DEFAULT_NEW_APPLICATION_NAME.to_string());
let dry_run = bool_value(find_input(options, "dry-run")).unwrap_or(false);
let skip_install = bool_value(find_input(options, "skip-install")).unwrap_or(false);
let skip_git = bool_value(find_input(options, "skip-git")).unwrap_or(false);
let project_directory = get_project_directory(&name, find_input(options, "directory"));
let collection = string_value(find_input(options, "collection"))
.filter(|value| !value.is_empty())
.unwrap_or_else(|| DEFAULT_COLLECTION.to_string());
let mut missing_information = Vec::new();
if string_value(find_input(inputs, "name")).is_none() {
missing_information.push(NewActionMissingInformation::Name);
}
if string_value(find_input(options, "packageManager")).is_none() {
missing_information.push(NewActionMissingInformation::PackageManager);
}
let schematic_options = map_schematic_options(inputs, options)?;
let schematic_command = CollectionFactory::create(collection.clone())
.execute_command("application", &schematic_options)?;
let package_manager = string_value(find_input(options, "packageManager"))
.filter(|value| !value.is_empty())
.map(|value| {
value
.parse::<PackageManager>()
.map_err(|error| error.to_string())
})
.transpose()?;
let install_command = if skip_install || dry_run {
None
} else {
package_manager.map(|manager| {
PackageManagerClient::new(manager).install_command(project_directory.clone())
})
};
let project_path = current_dir.as_ref().join(&project_directory);
let initialize_git_command = if dry_run || skip_git {
None
} else {
Some(RunnerFactory::create(RunnerKind::Git).describe("init", true, Some(project_path)))
};
Ok(NewActionPlan {
collection,
schematic: "application".to_string(),
schematic_options,
schematic_command,
project_directory,
package_manager,
install_command,
initialize_git_command,
create_git_ignore: !dry_run && !skip_git,
git_ignore_content: (!dry_run && !skip_git).then_some(DEFAULT_GIT_IGNORE),
print_collective: !dry_run,
dry_run,
missing_information,
})
}
pub fn get_project_directory(application_name: &str, directory_option: Option<&Input>) -> PathBuf {
string_value(directory_option)
.filter(|value| !value.is_empty())
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from(normalize_to_kebab_or_snake_case(application_name)))
}
pub fn map_schematic_options(
inputs: &[Input],
options: &[Input],
) -> Result<Vec<SchematicOption>, String> {
inputs
.iter()
.chain(options.iter())
.filter(|input| input.name != "skip-install")
.filter_map(input_to_schematic_option)
.collect()
}
fn input_to_schematic_option(input: &Input) -> Option<Result<SchematicOption, String>> {
let value = input.value.as_ref()?;
Some(match value {
InputValue::Bool(value) => Ok(SchematicOption::new(&input.name, *value)),
InputValue::String(value) => Ok(SchematicOption::new(&input.name, value.clone())),
InputValue::StringList(_) => Err(format!(
"Input `{}` cannot be mapped to a schematic option because arrays are not supported",
input.name
)),
})
}
fn find_input<'a>(inputs: &'a [Input], name: &str) -> Option<&'a Input> {
inputs.iter().find(|input| input.name == name)
}
fn string_value(input: Option<&Input>) -> Option<String> {
match input?.value.as_ref()? {
InputValue::String(value) => Some(value.clone()),
_ => None,
}
}
fn bool_value(input: Option<&Input>) -> Option<bool> {
match input?.value.as_ref()? {
InputValue::Bool(value) => Some(*value),
_ => None,
}
}