nestrs-cli-rs 0.1.0

Rust port of the Nest CLI for the nestrs organization.
Documentation
//! Upstream source: `../nest-cli/lib/compiler/assets-manager.ts`.

use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};

use super::helpers::copy_path_resolve::copy_path_resolve;
pub use super::{AssetDeleteOnUnlink, AssetPlan};

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct AssetsManager {
    copied_once: BTreeSet<(PathBuf, PathBuf)>,
    action_in_progress: bool,
    watchers: Vec<AssetWatchPlan>,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AssetAction {
    pub action: AssetFileAction,
    pub file_path: PathBuf,
    pub source_root: PathBuf,
    pub out_dir: PathBuf,
    pub watch_assets_mode: bool,
    pub item_watch_assets: bool,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum AssetFileAction {
    Change,
    Unlink,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AssetWatchPlan {
    pub glob: String,
    pub exclude: Option<String>,
    pub out_dir: PathBuf,
}

impl AssetsManager {
    pub fn copy_assets(&mut self, assets: &[AssetPlan], source_root: impl AsRef<Path>) {
        self.watchers.extend(
            assets
                .iter()
                .filter(|asset| asset.watch_assets)
                .map(|asset| AssetWatchPlan {
                    glob: asset.glob.clone(),
                    exclude: asset.exclude.clone(),
                    out_dir: asset.out_dir.clone(),
                }),
        );
        let source_root = source_root.as_ref();
        for asset in assets.iter().filter(|asset| !asset.watch_assets) {
            let include = asset.include.as_deref().unwrap_or_else(|| Path::new(""));
            let path = source_root.join(include).join(&asset.glob);
            let action = AssetAction {
                action: AssetFileAction::Change,
                file_path: path,
                source_root: source_root.to_path_buf(),
                out_dir: asset.out_dir.clone(),
                watch_assets_mode: false,
                item_watch_assets: false,
            };
            let _ = self.action_on_file(&action);
        }
    }

    pub fn action_on_file(&mut self, option: &AssetAction) -> Result<Option<PathBuf>, String> {
        let key = (option.file_path.clone(), option.out_dir.clone());
        let is_watch_enabled = option.watch_assets_mode || option.item_watch_assets;
        if !is_watch_enabled && self.copied_once.contains(&key) {
            return Ok(None);
        }
        self.copied_once.insert(key);
        self.action_in_progress = true;

        let up = option.source_root.components().count();
        let dest = copy_path_resolve(
            &option.file_path.to_string_lossy(),
            &option.out_dir.to_string_lossy(),
            up,
        )?;

        match option.action {
            AssetFileAction::Change => {
                if let Some(parent) = dest.parent() {
                    fs::create_dir_all(parent).map_err(|error| {
                        format!("Failed to create \"{}\": {error}", parent.display())
                    })?;
                }
                fs::copy(&option.file_path, &dest).map_err(|error| {
                    format!(
                        "Failed to copy \"{}\" to \"{}\": {error}",
                        option.file_path.display(),
                        dest.display()
                    )
                })?;
            }
            AssetFileAction::Unlink => match fs::remove_file(&dest) {
                Ok(()) => {}
                Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
                Err(error) => {
                    return Err(format!("Failed to remove \"{}\": {error}", dest.display()));
                }
            },
        }
        Ok(Some(dest))
    }

    pub fn close_watchers(&mut self) -> usize {
        let count = self.watchers.len();
        self.watchers.clear();
        self.action_in_progress = false;
        count
    }

    pub fn watchers(&self) -> &[AssetWatchPlan] {
        &self.watchers
    }

    pub fn action_in_progress(&self) -> bool {
        self.action_in_progress
    }
}