nestrs-cli-rs 0.1.0

Rust port of the Nest CLI for the nestrs organization.
Documentation
//! Legacy SWC option planning kept only for projects that explicitly opt into SWC.

use std::collections::BTreeMap;
use std::path::PathBuf;

use crate::configuration::{Builder, Configuration, DEFAULT_OUT_DIR, DEFAULT_SOURCE_ROOT};

use super::super::SwcCliOptions;

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct SourceCompilerOptions {
    pub source_map: Option<bool>,
    pub inline_source_map: Option<bool>,
    pub out_dir: Option<String>,
    pub root_dir: Option<String>,
    pub base_url: Option<String>,
    pub paths: BTreeMap<String, Vec<String>>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SwcDefaults {
    pub swc_options: SwcOptions,
    pub cli_options: SwcCliOptions,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SwcOptions {
    pub source_maps: Option<SwcSourceMaps>,
    pub module_type: String,
    pub target: String,
    pub parser_syntax: String,
    pub decorators: bool,
    pub dynamic_import: bool,
    pub legacy_decorator: bool,
    pub decorator_metadata: bool,
    pub use_define_for_class_fields: bool,
    pub keep_class_names: bool,
    pub base_url: Option<String>,
    pub paths: BTreeMap<String, Vec<String>>,
    pub minify: bool,
    pub swcrc: bool,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SwcSourceMaps {
    Bool(bool),
    Inline,
}

pub fn swc_defaults_factory(
    ts_options: Option<&SourceCompilerOptions>,
    configuration: Option<&Configuration>,
) -> SwcDefaults {
    let ts_options = ts_options.cloned().unwrap_or_default();
    let configuration = configuration.cloned().unwrap_or_default();
    let builder_options = match &configuration.compiler_options.builder {
        Builder::Swc(options) => Some(options),
        _ => None,
    };
    let default_out_dir = ts_options
        .out_dir
        .as_deref()
        .map(convert_path)
        .unwrap_or_else(|| DEFAULT_OUT_DIR.to_string());

    SwcDefaults {
        swc_options: SwcOptions {
            source_maps: match (ts_options.source_map, ts_options.inline_source_map) {
                (Some(true), _) => Some(SwcSourceMaps::Bool(true)),
                (_, Some(true)) => Some(SwcSourceMaps::Inline),
                _ => None,
            },
            module_type: "commonjs".to_string(),
            target: "es2021".to_string(),
            parser_syntax: "rust".to_string(),
            decorators: true,
            dynamic_import: true,
            legacy_decorator: true,
            decorator_metadata: true,
            use_define_for_class_fields: false,
            keep_class_names: true,
            base_url: ts_options.base_url,
            paths: ts_options.paths,
            minify: false,
            swcrc: true,
        },
        cli_options: SwcCliOptions {
            out_dir: builder_options
                .and_then(|options| options.out_dir.as_ref())
                .map(PathBuf::from)
                .unwrap_or_else(|| PathBuf::from(default_out_dir)),
            filenames: non_empty_paths(
                builder_options.map(|options| options.filenames.as_slice()),
                vec![PathBuf::from(if configuration.source_root.is_empty() {
                    DEFAULT_SOURCE_ROOT
                } else {
                    &configuration.source_root
                })],
            ),
            sync: builder_options
                .and_then(|options| options.sync)
                .unwrap_or(false),
            extensions: non_empty_strings(
                builder_options.map(|options| options.extensions.as_slice()),
                vec![".js".to_string(), ".ts".to_string()],
            ),
            copy_files: builder_options
                .and_then(|options| options.copy_files)
                .unwrap_or(false),
            include_dotfiles: builder_options
                .and_then(|options| options.include_dotfiles)
                .unwrap_or(false),
            quiet: builder_options
                .and_then(|options| options.quiet)
                .unwrap_or(false),
            watch: false,
            strip_leading_paths: ts_options.root_dir.is_none(),
        },
    }
}

pub fn convert_path(windows_path: &str) -> String {
    let without_unc = windows_path.strip_prefix(r"\\?\").unwrap_or(windows_path);
    let normalized = without_unc.replace('\\', "/");
    let mut compact = String::new();
    let mut previous_slash = false;
    for ch in normalized.chars() {
        if ch == '/' {
            if !previous_slash {
                compact.push(ch);
            }
            previous_slash = true;
        } else {
            compact.push(ch);
            previous_slash = false;
        }
    }
    compact
}

fn non_empty_paths(values: Option<&[String]>, default: Vec<PathBuf>) -> Vec<PathBuf> {
    match values {
        Some(values) if !values.is_empty() => values.iter().map(PathBuf::from).collect(),
        _ => default,
    }
}

fn non_empty_strings(values: Option<&[String]>, default: Vec<String>) -> Vec<String> {
    match values {
        Some(values) if !values.is_empty() => values.to_vec(),
        _ => default,
    }
}