mini-build 0.1.0

Builds the directory a static server serves: CSS/JS bundling and minification via external tools, plus asset mirroring.
Documentation
//! What changed, and how that gets reported to whoever is listening.
//!
//! These types were `mini_static::reload::ChangeType` and
//! `mini_static::watcher::{ChangeEvent, Broadcaster}` when the pipeline lived inside the
//! server. They are reproduced here rather than imported because the server keeps its
//! own copies: `ChangeType` is the payload of its live-reload SSE frames, and the
//! broadcaster feeds its connected browsers. Sharing them would make a build tool depend
//! on an HTTP server, which is the coupling this crate exists to remove — and the two
//! will drift apart, since this one has no reason to care what a browser does with a
//! stylesheet.

use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};

use std::sync::mpsc::{channel, Receiver, Sender};

/// The kind of file that changed, classified by extension.
///
/// Classification drives which pipeline owns a file: `Css` goes to the CSS tool,
/// `Script` to the JS tool, and everything else is copied rather than transformed.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ChangeType {
    /// A CSS stylesheet changed.
    Css,
    /// A JavaScript module changed.
    Script,
    /// An HTML page changed.
    Html,
    /// Some other file changed.
    Other,
}

impl ChangeType {
    /// Determine the change type from a file path's extension.
    pub fn from_path(path: &Path) -> Self {
        match path.extension().and_then(|e| e.to_str()) {
            Some("css") => ChangeType::Css,
            Some("js" | "mjs") => ChangeType::Script,
            Some("html" | "htm") => ChangeType::Html,
            _ => ChangeType::Other,
        }
    }

    /// The stable string name for this change type.
    pub fn as_str(&self) -> &'static str {
        match self {
            ChangeType::Css => "css",
            ChangeType::Script => "script",
            ChangeType::Html => "html",
            ChangeType::Other => "other",
        }
    }
}

/// A file that was added, modified, or removed.
#[derive(Clone, Debug)]
pub struct ChangeEvent {
    /// The path to the file that changed.
    pub path: PathBuf,
    /// What kind of file it is.
    pub change_type: ChangeType,
}

/// Fans change events out to every subscriber.
///
/// A build reports what it produced through this, so a caller — a watch loop, a dev
/// server wanting to reload a browser — can react without polling the output directory
/// itself. Subscribers whose channel has closed are dropped on the next broadcast.
#[derive(Clone, Default)]
pub struct Broadcaster {
    senders: Arc<Mutex<Vec<Sender<ChangeEvent>>>>,
}

impl Broadcaster {
    /// Create a broadcaster with no subscribers.
    pub fn new() -> Self {
        Broadcaster::default()
    }

    /// Send `event` to every active subscriber, dropping any that have gone away.
    pub fn broadcast(&self, event: ChangeEvent) {
        let mut senders = self.senders.lock().unwrap();
        senders.retain(|sender| sender.send(event.clone()).is_ok());
    }

    /// Subscribe to change events.
    pub fn subscribe(&self) -> Receiver<ChangeEvent> {
        let (tx, rx) = channel();
        self.senders.lock().unwrap().push(tx);
        rx
    }

    /// How many subscribers are currently attached.
    #[cfg(test)]
    pub fn subscriber_count(&self) -> usize {
        self.senders.lock().unwrap().len()
    }
}

#[cfg(test)]
#[path = "../tests/unit/change.rs"]
mod tests;