nestrs-cli-rs 0.1.0

Rust port of the Nest CLI for the nestrs organization.
Documentation
//! Upstream source: `../nest-cli/lib/compiler/hooks/tsconfig-paths.hook.ts`.

use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TsconfigPathsHook {
    pub base_url: PathBuf,
    pub paths: BTreeMap<String, Vec<String>>,
}

impl TsconfigPathsHook {
    pub fn new(base_url: impl Into<PathBuf>, paths: BTreeMap<String, Vec<String>>) -> Self {
        Self {
            base_url: base_url.into(),
            paths,
        }
    }

    pub fn rewrite_module_specifier(
        &self,
        source_file: impl AsRef<Path>,
        module_specifier: &str,
    ) -> Option<String> {
        if module_specifier.starts_with('.') || module_specifier.starts_with('/') {
            return None;
        }
        let target = self.resolve_alias(module_specifier)?;
        let source_dir = source_file
            .as_ref()
            .parent()
            .unwrap_or_else(|| Path::new(""));
        let relative = make_relative_posix(source_dir, &target);
        Some(if relative.starts_with('.') {
            relative
        } else {
            format!("./{relative}")
        })
    }

    pub fn resolve_alias(&self, module_specifier: &str) -> Option<PathBuf> {
        self.paths.iter().find_map(|(pattern, replacements)| {
            match_pattern(pattern, module_specifier).and_then(|matched| {
                replacements.first().map(|replacement| {
                    let replaced = replacement.replace('*', &matched);
                    self.base_url.join(replaced)
                })
            })
        })
    }
}

pub fn tsconfig_paths_before_hook_factory(
    base_url: impl Into<PathBuf>,
    paths: BTreeMap<String, Vec<String>>,
) -> TsconfigPathsHook {
    TsconfigPathsHook::new(base_url, paths)
}

fn match_pattern(pattern: &str, text: &str) -> Option<String> {
    if pattern == text {
        return Some(String::new());
    }
    let (prefix, suffix) = pattern.split_once('*')?;
    text.strip_prefix(prefix)
        .and_then(|value| value.strip_suffix(suffix))
        .map(ToString::to_string)
}

fn make_relative_posix(from: &Path, to: &Path) -> String {
    let from_components: Vec<_> = from.components().collect();
    let to_components: Vec<_> = to.components().collect();
    let common = from_components
        .iter()
        .zip(&to_components)
        .take_while(|(a, b)| a == b)
        .count();
    let mut parts = Vec::new();
    for _ in common..from_components.len() {
        parts.push("..".to_string());
    }
    parts.extend(
        to_components[common..]
            .iter()
            .map(|component| component.as_os_str().to_string_lossy().replace('\\', "/")),
    );
    if parts.is_empty() {
        ".".to_string()
    } else {
        parts.join("/")
    }
}