mini_build/change.rs
1//! What changed, and how that gets reported to whoever is listening.
2//!
3//! These types were `mini_static::reload::ChangeType` and
4//! `mini_static::watcher::{ChangeEvent, Broadcaster}` when the pipeline lived inside the
5//! server. They are reproduced here rather than imported because the server keeps its
6//! own copies: `ChangeType` is the payload of its live-reload SSE frames, and the
7//! broadcaster feeds its connected browsers. Sharing them would make a build tool depend
8//! on an HTTP server, which is the coupling this crate exists to remove — and the two
9//! will drift apart, since this one has no reason to care what a browser does with a
10//! stylesheet.
11
12use std::path::{Path, PathBuf};
13use std::sync::{Arc, Mutex};
14
15use std::sync::mpsc::{channel, Receiver, Sender};
16
17/// The kind of file that changed, classified by extension.
18///
19/// Classification drives which pipeline owns a file: `Css` goes to the CSS tool,
20/// `Script` to the JS tool, and everything else is copied rather than transformed.
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub enum ChangeType {
23 /// A CSS stylesheet changed.
24 Css,
25 /// A JavaScript module changed.
26 Script,
27 /// An HTML page changed.
28 Html,
29 /// Some other file changed.
30 Other,
31}
32
33impl ChangeType {
34 /// Determine the change type from a file path's extension.
35 pub fn from_path(path: &Path) -> Self {
36 match path.extension().and_then(|e| e.to_str()) {
37 Some("css") => ChangeType::Css,
38 Some("js" | "mjs") => ChangeType::Script,
39 Some("html" | "htm") => ChangeType::Html,
40 _ => ChangeType::Other,
41 }
42 }
43
44 /// The stable string name for this change type.
45 pub fn as_str(&self) -> &'static str {
46 match self {
47 ChangeType::Css => "css",
48 ChangeType::Script => "script",
49 ChangeType::Html => "html",
50 ChangeType::Other => "other",
51 }
52 }
53}
54
55/// A file that was added, modified, or removed.
56#[derive(Clone, Debug)]
57pub struct ChangeEvent {
58 /// The path to the file that changed.
59 pub path: PathBuf,
60 /// What kind of file it is.
61 pub change_type: ChangeType,
62}
63
64/// Fans change events out to every subscriber.
65///
66/// A build reports what it produced through this, so a caller — a watch loop, a dev
67/// server wanting to reload a browser — can react without polling the output directory
68/// itself. Subscribers whose channel has closed are dropped on the next broadcast.
69#[derive(Clone, Default)]
70pub struct Broadcaster {
71 senders: Arc<Mutex<Vec<Sender<ChangeEvent>>>>,
72}
73
74impl Broadcaster {
75 /// Create a broadcaster with no subscribers.
76 pub fn new() -> Self {
77 Broadcaster::default()
78 }
79
80 /// Send `event` to every active subscriber, dropping any that have gone away.
81 pub fn broadcast(&self, event: ChangeEvent) {
82 let mut senders = self.senders.lock().unwrap();
83 senders.retain(|sender| sender.send(event.clone()).is_ok());
84 }
85
86 /// Subscribe to change events.
87 pub fn subscribe(&self) -> Receiver<ChangeEvent> {
88 let (tx, rx) = channel();
89 self.senders.lock().unwrap().push(tx);
90 rx
91 }
92
93 /// How many subscribers are currently attached.
94 #[cfg(test)]
95 pub fn subscriber_count(&self) -> usize {
96 self.senders.lock().unwrap().len()
97 }
98}
99
100#[cfg(test)]
101#[path = "../tests/unit/change.rs"]
102mod tests;