nestrs-cli-rs 0.1.0

Rust port of the Nest CLI for the nestrs organization.
Documentation
use std::env;
use std::fs;
use std::path::{Path, PathBuf};

use cli::actions::ActionKind;
use cli::add_action::{
    AddActionRequest, AddExecutionPlan, create_add_action_plan, create_add_execution_plan,
    has_native_add_handler, unsupported_native_add_reason,
};
use cli::build_action::{BuildActionPlanRequest, create_build_action_plan};
use cli::build_executor::{
    create_build_execution_plan, execute_build_plan, execute_build_watch_plan,
};
use cli::commands::{Input, InputValue, load_command_invocation};
use cli::configuration::{Configuration, load_configuration};
use cli::generate_action::{GenerateActionPlanOptions, build_generate_action_plan};
use cli::new_action::{DEFAULT_GIT_IGNORE, build_new_action_plan};
use cli::package_managers::{PackageManager, detect_package_manager};
use cli::runners::RunnerCommand;
use cli::start_action::{StartActionPlanRequest, create_start_action_plan};
use cli::start_executor::{create_start_execution_plan, execute_start_plan};
use schematics::factories::ApplicationOptions;
use schematics::generators::{generate_application, write_application};
use schematics::nest_add::{NestAddOptions, generate_nest_add};
use schematics::{GenerateOptions, GeneratedFile, generate, write_generated_files};

fn main() {
    if let Err(error) = run() {
        eprintln!("error: {error}");
        std::process::exit(1);
    }
}

fn run() -> Result<(), String> {
    let args = env::args().skip(1).collect::<Vec<_>>();
    if args.is_empty() || args.iter().any(|arg| arg == "--help" || arg == "-h") {
        print_help();
        return Ok(());
    }

    let invocation = load_command_invocation(args).map_err(|error| error.to_string())?;
    let cwd = env::current_dir().map_err(|error| error.to_string())?;
    let configuration = load_configuration(&cwd).unwrap_or_else(|_| Configuration::default());

    match invocation.kind {
        ActionKind::New => run_new(invocation.inputs, invocation.options, &cwd)?,
        ActionKind::Generate => {
            run_generate(invocation.inputs, invocation.options, &configuration, &cwd)?
        }
        ActionKind::Add => run_add(
            invocation.inputs,
            invocation.options,
            invocation.extra_flags,
            configuration,
            &cwd,
        )?,
        ActionKind::Build => run_build(invocation.inputs, invocation.options, configuration, cwd)?,
        ActionKind::Start => run_start(
            invocation.inputs,
            invocation.options,
            invocation.extra_flags,
            configuration,
            cwd,
        )?,
        ActionKind::Info => run_info(&cwd),
    }

    Ok(())
}

fn run_new(inputs: Vec<Input>, options: Vec<Input>, cwd: &Path) -> Result<(), String> {
    let plan = build_new_action_plan(&inputs, &options, cwd).map_err(|error| error.to_string())?;
    let application_options = application_options(&inputs, &options)?;
    let files = if plan.dry_run {
        generate_application(application_options, cwd).map_err(|error| error.to_string())?
    } else {
        write_application(application_options, cwd).map_err(|error| error.to_string())?
    };

    if plan.dry_run {
        println!("dry run: {} files would be created", files.len());
        print_generated_files(&files, cwd);
        return Ok(());
    }

    if plan.create_git_ignore {
        fs::write(
            cwd.join(&plan.project_directory).join(".gitignore"),
            DEFAULT_GIT_IGNORE,
        )
        .map_err(|error| error.to_string())?;
    }

    println!("created {} files", files.len());

    if let Some(command) = plan.initialize_git_command {
        run_command(&command)?;
    }

    Ok(())
}

fn run_generate(
    inputs: Vec<Input>,
    options: Vec<Input>,
    configuration: &Configuration,
    cwd: &Path,
) -> Result<(), String> {
    let dry_run = bool_input(&options, "dry-run");
    let mut schematic_inputs = inputs;
    schematic_inputs.extend(options);
    let plan = build_generate_action_plan(
        &schematic_inputs,
        configuration,
        GenerateActionPlanOptions::default(),
    )
    .map_err(|error| error.to_string())?;
    let files =
        generate(generate_options_from_plan(&plan)?, cwd).map_err(|error| error.to_string())?;

    if dry_run {
        println!("dry run: {} files would be created", files.len());
        print_generated_files(&files, cwd);
    } else {
        write_generated_files(&files).map_err(|error| error.to_string())?;
        println!("created {} files", files.len());
    }

    Ok(())
}

fn run_add(
    inputs: Vec<Input>,
    options: Vec<Input>,
    extra_flags: Vec<String>,
    configuration: Configuration,
    cwd: &Path,
) -> Result<(), String> {
    let library = string_input(&inputs, "library")
        .ok_or_else(|| "add requires a library name".to_string())?;
    let dry_run = bool_input(&options, "dry-run");
    let add_extra_flags = extra_flags.clone();
    let plan = create_add_action_plan(AddActionRequest {
        library,
        skip_install: bool_input(&options, "skip-install"),
        dry_run,
        project: string_input(&options, "project"),
        source_root: string_input(&options, "sourceRoot"),
        extra_flags,
        package_manager: detect_package_manager(cwd).unwrap_or(PackageManager::Npm),
        configuration,
    });

    if let Some(command) = &plan.install_command {
        run_command(&command)?;
    }

    if dry_run {
        if has_native_add_handler(&plan.collection_name) {
            let files = generate_nest_add(
                NestAddOptions {
                    collection: plan.collection_name.clone(),
                    source_root: plan.source_root.clone(),
                    extra_flags: add_extra_flags,
                },
                cwd,
            )
            .map_err(|error| error.to_string())?;
            println!("dry run: {} files would be created", files.len());
            print_generated_files(&files, cwd);
        } else if let Some(reason) = unsupported_native_add_reason(&plan.collection_name) {
            return Err(reason.to_string());
        } else {
            println!("{}", plan.schematic_command);
        }
        return Ok(());
    }

    match create_add_execution_plan(&plan, &add_extra_flags, cwd)? {
        AddExecutionPlan::Native(native_plan) => {
            let files = generate_nest_add(
                NestAddOptions {
                    collection: native_plan.collection_name,
                    source_root: native_plan.source_root,
                    extra_flags: native_plan.extra_flags,
                },
                cwd,
            )
            .map_err(|error| error.to_string())?;
            write_generated_files(&files).map_err(|error| error.to_string())?;
            println!("created {} files", files.len());
            Ok(())
        }
        AddExecutionPlan::Node(execution_plan) => run_command(&execution_plan.command),
    }
}

fn run_build(
    inputs: Vec<Input>,
    options: Vec<Input>,
    configuration: Configuration,
    cwd: PathBuf,
) -> Result<(), String> {
    let plan = create_build_action_plan(BuildActionPlanRequest {
        cwd,
        configuration,
        command_inputs: inputs,
        command_options: options,
        ts_build_info_file: None,
    })
    .map_err(|error| error.to_string())?;
    let execution_plan = create_build_execution_plan(&plan).map_err(|error| error.to_string())?;
    for warning in &execution_plan.warnings {
        eprintln!("warning: {warning}");
    }
    if execution_plan.watch.is_some() {
        return execute_build_watch_plan(&execution_plan).map_err(|error| error.to_string());
    }
    execute_build_plan(&execution_plan).map_err(|error| error.to_string())
}

fn run_start(
    inputs: Vec<Input>,
    options: Vec<Input>,
    extra_flags: Vec<String>,
    configuration: Configuration,
    cwd: PathBuf,
) -> Result<(), String> {
    let plan = create_start_action_plan(StartActionPlanRequest {
        cwd,
        configuration,
        command_inputs: inputs,
        command_options: options,
        extra_flags,
        ts_build_info_file: None,
    })
    .map_err(|error| error.to_string())?;
    let execution_plan = create_start_execution_plan(&plan).map_err(|error| error.to_string())?;
    for warning in &execution_plan.warnings {
        eprintln!("warning: {warning}");
    }
    execute_start_plan(&execution_plan).map_err(|error| error.to_string())
}

fn run_info(cwd: &Path) {
    println!("cli {}", env!("CARGO_PKG_VERSION"));
    println!(
        "package manager {}",
        detect_package_manager(cwd)
            .unwrap_or(PackageManager::Npm)
            .as_str()
    );
}

fn print_help() {
    println!("Nestrs CLI");
    println!("Commands: new, generate, add, build, start, info");
}

fn run_command(command: &RunnerCommand) -> Result<(), String> {
    command
        .execute()
        .map(|_| ())
        .map_err(|error| error.to_string())
}

fn print_generated_files(files: &[GeneratedFile], root: &Path) {
    for file in files {
        let display_path = file.path.strip_prefix(root).unwrap_or(&file.path).display();
        println!("create {display_path}");
    }
}

fn application_options(inputs: &[Input], options: &[Input]) -> Result<ApplicationOptions, String> {
    let name = string_input(inputs, "name").ok_or_else(|| "new requires a name".to_string())?;
    Ok(ApplicationOptions {
        name,
        author: None,
        description: None,
        directory: string_input(options, "directory").map(PathBuf::from),
        strict: bool_input_value(options, "strict"),
        version: None,
        language: Some("rs".to_string()),
        package_manager: string_input(options, "packageManager"),
        dependencies: None,
        dev_dependencies: None,
        spec: None,
        spec_file_suffix: None,
    })
}

fn generate_options_from_plan(
    plan: &cli::generate_action::GenerateActionPlan,
) -> Result<GenerateOptions, String> {
    let path = match schematic_option_string(&plan.schematic_options, "path") {
        Some(path) => plan.source_root.join(path),
        None => plan.source_root.clone(),
    };

    Ok(GenerateOptions {
        schematic: plan.schematic.clone(),
        name: schematic_option_string(&plan.schematic_options, "name")
            .ok_or_else(|| "generate requires a name".to_string())?,
        path: Some(path),
        language: "rs".to_string(),
        spec: plan.spec,
        flat: plan.flat,
        spec_file_suffix: plan.spec_file_suffix.clone(),
        extra: Default::default(),
    })
}

fn string_input(inputs: &[Input], name: &str) -> Option<String> {
    inputs
        .iter()
        .find(|input| input.name == name)
        .and_then(|input| match input.value.as_ref()? {
            InputValue::String(value) => Some(value.clone()),
            _ => None,
        })
}

fn bool_input(inputs: &[Input], name: &str) -> bool {
    bool_input_value(inputs, name).unwrap_or(false)
}

fn bool_input_value(inputs: &[Input], name: &str) -> Option<bool> {
    inputs
        .iter()
        .find(|input| input.name == name)
        .and_then(|input| match input.value.as_ref()? {
            InputValue::Bool(value) => Some(*value),
            _ => None,
        })
}

fn schematic_option_string(
    options: &[cli::schematics::SchematicOption],
    name: &str,
) -> Option<String> {
    options
        .iter()
        .find(|option| option.name == name)
        .and_then(|option| match &option.value {
            cli::schematics::SchematicOptionValue::String(value) => Some(value.clone()),
            _ => None,
        })
}