diff --git a/Cargo.lock b/Cargo.lock index 1aada495..2fe49d98 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -403,6 +403,16 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "bstr" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "786a307d683a5bf92e6fd5fd69a7eb613751668d1d8d67d802846dfe367c62c8" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -475,6 +485,7 @@ dependencies = [ "dunce", "flate2", "flexi_logger", + "ignore", "insta", "itertools 0.13.0", "lazy_static", @@ -1179,6 +1190,19 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "globset" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -1535,6 +1559,22 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" +[[package]] +name = "ignore" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + [[package]] name = "indexmap" version = "1.9.3" diff --git a/Cargo.toml b/Cargo.toml index f2ac7b17..23b88ae5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ categories = ["development-tools", "wasm", "web-programming"] keywords = ["leptos"] version = "0.2.24" edition = "2021" -rust-version = "1.71" +rust-version = "1.82" authors = ["Henrik Akesson", "Greg Johnston", "Ben Wishovich"] [package.metadata.wix] @@ -94,6 +94,7 @@ base64ct = { version = "1.6.0", features = ["alloc"] } swc = "9.0" swc_common = "5.0" shlex = "1.3.0" +ignore = "0.4.23" [dev-dependencies] insta = { version = "1.40.0", features = ["yaml"] } diff --git a/src/command/watch.rs b/src/command/watch.rs index a074057b..728feef6 100644 --- a/src/command/watch.rs +++ b/src/command/watch.rs @@ -29,11 +29,7 @@ pub async fn watch(proj: &Arc) -> Result<()> { None }; - let _watch = service::notify::spawn(proj).await?; - if let Some(view_macros) = view_macros { - let _patch = service::patch::spawn(proj, &view_macros).await?; - } - + service::notify::spawn(proj, view_macros).await?; service::serve::spawn(proj).await; service::reload::spawn(proj).await; diff --git a/src/compile/tailwind.rs b/src/compile/tailwind.rs index d3d18493..75cc3c2e 100644 --- a/src/compile/tailwind.rs +++ b/src/compile/tailwind.rs @@ -18,7 +18,7 @@ pub async fn compile_tailwind(proj: &Project, tw_conf: &TailwindConfig) -> Resul create_default_tailwind_config(tw_conf).await?; } - let (line, process) = tailwind_process(&proj, "tailwindcss", tw_conf).await?; + let (line, process) = tailwind_process(proj, "tailwindcss", tw_conf).await?; match wait_piped_interruptible("Tailwind", process, Interrupt::subscribe_any()).await? { CommandResult::Success(output) => { diff --git a/src/service/mod.rs b/src/service/mod.rs index 0ba3806f..17b4539b 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -1,5 +1,4 @@ pub mod notify; -pub mod patch; pub mod reload; pub mod serve; pub mod site; diff --git a/src/service/notify.rs b/src/service/notify.rs index fa3f6c18..a86af7dd 100644 --- a/src/service/notify.rs +++ b/src/service/notify.rs @@ -1,70 +1,47 @@ use crate::compile::Change; use crate::config::Project; use crate::ext::anyhow::{anyhow, Result}; -use crate::signal::Interrupt; +use crate::signal::{Interrupt, ReloadSignal}; use crate::{ - ext::{remove_nested, PathBufExt, PathExt}, + ext::{PathBufExt, PathExt}, logger::GRAY, }; use camino::Utf8PathBuf; -use itertools::Itertools; +use ignore::gitignore::Gitignore; +use leptos_hot_reload::ViewMacros; use notify::event::ModifyKind; use notify::{Event, EventKind, RecursiveMode, Watcher}; -use std::collections::HashSet; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; use tokio::task::JoinHandle; pub(crate) const FALLBACK_POLLING_TIMEOUT: Duration = Duration::from_millis(200); -pub async fn spawn(proj: &Arc) -> Result> { - let mut set: HashSet = HashSet::from_iter(vec![]); - - set.extend(proj.lib.src_paths.clone()); - set.extend(proj.bin.src_paths.clone()); - set.extend(proj.watch_additional_files.clone()); - set.insert(proj.js_dir.clone()); - - if let Some(file) = &proj.style.file { - set.insert(file.source.clone().without_last()); - } - - if let Some(tailwind) = &proj.style.tailwind { - set.insert(tailwind.config_file.clone()); - set.insert(tailwind.input_file.clone()); - } - - if let Some(assets) = &proj.assets { - set.insert(assets.dir.clone()); - } - - let paths = remove_nested(set.into_iter().filter(|path| Path::new(path).exists())); - - log::info!( - "Notify watching paths {}", - GRAY.paint(paths.iter().join(", ")) - ); +pub async fn spawn(proj: &Arc, view_macros: Option) -> Result> { let proj = proj.clone(); - Ok(tokio::spawn(async move { run(&paths, proj).await })) + Ok(tokio::spawn(async move { run(proj, view_macros).await })) } -async fn run(paths: &[Utf8PathBuf], proj: Arc) { +async fn run(proj: Arc, view_macros: Option) { let (sync_tx, sync_rx) = std::sync::mpsc::channel(); - let proj = proj.clone(); - tokio::task::spawn_blocking(move || { - while let Ok(event) = sync_rx.recv() { - match event { - Ok(event) => handle(event, proj.clone()), - Err(err) => { - log::trace!("Notify error: {err:?}"); - return; + tokio::task::spawn_blocking({ + let proj = proj.clone(); + move || { + let mut gitignore = create_gitignore_instance(&proj); + while let Ok(event) = sync_rx.recv() { + match event { + Ok(event) => handle(event, proj.clone(), &view_macros, &mut gitignore), + Err(err) => { + log::trace!("Notify error: {err:?}"); + return; + } } } + log::debug!("Notify stopped"); } - log::debug!("Notify stopped"); }); let mut watcher = notify::RecommendedWatcher::new( @@ -73,9 +50,12 @@ async fn run(paths: &[Utf8PathBuf], proj: Arc) { ) .expect("failed to build file system watcher"); + let mut paths = proj.watch_additional_files.clone(); + paths.push(proj.working_dir.clone()); + for path in paths { - if let Err(e) = watcher.watch(Path::new(path), RecursiveMode::Recursive) { - log::error!("Notify could not watch {path:?} due to {e:?}"); + if let Err(e) = watcher.watch(path.as_std_path(), RecursiveMode::Recursive) { + log::error!("Notify could not watch {:?} due to {e:?}", proj.working_dir); } } @@ -84,7 +64,12 @@ async fn run(paths: &[Utf8PathBuf], proj: Arc) { } } -fn handle(event: Event, proj: Arc) { +fn handle( + event: Event, + proj: Arc, + view_macros: &Option, + gitignore: &mut Gitignore, +) { if event.paths.is_empty() { return; } @@ -98,23 +83,23 @@ fn handle(event: Event, proj: Arc) { return; }; - log::trace!("Notify handle {}", GRAY.paint(format!("{:?}", event.paths))); + let paths = ignore_paths(&proj, &event.paths, gitignore); - let paths: Vec<_> = event - .paths - .into_iter() - .filter_map(|p| match convert(&p, &proj) { - Ok(p) => Some(p), - Err(e) => { - log::info!("{e}"); - None - } - }) - .collect(); + if paths.is_empty() { + return; + } let mut changes = Vec::new(); + log::trace!("Notify handle {}", GRAY.paint(format!("{paths:?}"))); + for path in paths { + if path.starts_with(".gitignore") { + *gitignore = create_gitignore_instance(&proj); + log::debug!("Notify .gitignore change {}", GRAY.paint(path.to_string())); + continue; + } + if let Some(assets) = &proj.assets { if path.starts_with(&assets.dir) { log::debug!("Notify asset change {}", GRAY.paint(path.to_string())); @@ -131,6 +116,14 @@ fn handle(event: Event, proj: Arc) { } if path.starts_with_any(&proj.bin.src_paths) && path.is_ext_any(&["rs"]) { + if let Some(view_macros) = view_macros { + // Check if it's possible to patch + let patches = view_macros.patch(&path); + if let Ok(Some(patch)) = patches { + log::debug!("Patching view."); + ReloadSignal::send_view_patches(&patch); + } + } log::debug!("Notify bin source change {}", GRAY.paint(path.to_string())); changes.push(Change::BinSource); } @@ -159,16 +152,58 @@ fn handle(event: Event, proj: Arc) { ); changes.push(Change::Additional); } + } - if !changes.is_empty() { - Interrupt::send(&changes); - } else { - log::trace!( - "Notify changed but not watched: {}", - GRAY.paint(path.to_string()) - ); - } + if !changes.is_empty() { + Interrupt::send(&changes); + } +} + +pub(crate) fn create_gitignore_instance(proj: &Project) -> Gitignore { + log::info!("Creating ignore list from '.gitignore' file"); + + let (gi, err) = Gitignore::new(proj.working_dir.join(".gitignore")); + + if let Some(err) = err { + log::error!("Failed reading '.gitignore' file in the working directory: {err}\nThis causes the watcher to work expensively on file changes like changes in the 'target' path.\nCreate a '.gitignore' file and exclude common build and cache paths like 'target'"); } + + gi +} + +pub(crate) fn ignore_paths( + proj: &Project, + event_paths: &[PathBuf], + gitignore: &Gitignore, +) -> Vec { + event_paths + .iter() + .filter_map(|p| { + let p = match convert(p, proj) { + Ok(p) => p, + Err(e) => { + log::info!("{e}"); + return None; + } + }; + + // Check if the file excluded + let matched = gitignore.matched(p.as_std_path(), p.is_dir()); + if matches!(matched, ignore::Match::Ignore(_)) { + return None; + } + + // Check if the parent directories excluded + let mut parent = p.as_std_path(); + while let Some(par) = parent.parent() { + if matches!(gitignore.matched(par, true), ignore::Match::Ignore(_)) { + return None; + } + parent = par; + } + Some(p) + }) + .collect() } pub(crate) fn convert(p: &Path, proj: &Project) -> Result { diff --git a/src/service/patch.rs b/src/service/patch.rs deleted file mode 100644 index 2c757c63..00000000 --- a/src/service/patch.rs +++ /dev/null @@ -1,107 +0,0 @@ -use crate::config::Project; -use crate::ext::anyhow::Result; -use crate::ext::PathBufExt; -use crate::signal::{Interrupt, ReloadSignal}; -use crate::{ext::remove_nested, logger::GRAY}; -use camino::Utf8PathBuf; -use itertools::Itertools; -use leptos_hot_reload::ViewMacros; -use notify::event::ModifyKind; -use notify::{Event, EventKind, RecursiveMode, Watcher}; -use std::collections::HashSet; -use std::path::Path; -use std::sync::Arc; -use tokio::task::JoinHandle; - -pub async fn spawn(proj: &Arc, view_macros: &ViewMacros) -> Result> { - let view_macros = view_macros.to_owned(); - let mut set: HashSet = HashSet::from_iter(vec![]); - - set.extend(proj.lib.src_paths.clone()); - - let paths = remove_nested(set.into_iter()); - - log::info!( - "Patch watching folders {}", - GRAY.paint(paths.iter().join(", ")) - ); - let proj = proj.clone(); - - Ok(tokio::spawn( - async move { run(&paths, proj, view_macros).await }, - )) -} - -async fn run(paths: &[Utf8PathBuf], proj: Arc, view_macros: ViewMacros) { - let (sync_tx, sync_rx) = std::sync::mpsc::channel(); - - let proj = proj.clone(); - tokio::task::spawn_blocking(move || { - while let Ok(event) = sync_rx.recv() { - match event { - Ok(event) => handle(event, proj.clone(), view_macros.clone()), - Err(err) => { - log::trace!("Notify error: {err:?}"); - return; - } - } - } - log::debug!("Notify stopped"); - }); - - let mut watcher = notify::RecommendedWatcher::new( - sync_tx, - notify::Config::default().with_poll_interval(super::notify::FALLBACK_POLLING_TIMEOUT), - ) - .expect("failed to build file system watcher"); - - for path in paths { - if let Err(e) = watcher.watch(Path::new(path), RecursiveMode::Recursive) { - log::error!("Notify could not watch {path:?} due to {e:?}"); - } - } - - if let Err(e) = Interrupt::subscribe_shutdown().recv().await { - log::trace!("Notify stopped due to: {e:?}"); - } -} - -fn handle(event: Event, proj: Arc, view_macros: ViewMacros) { - if event.paths.is_empty() { - return; - } - - if let EventKind::Any - | EventKind::Other - | EventKind::Access(_) - | EventKind::Modify(ModifyKind::Any | ModifyKind::Other | ModifyKind::Metadata(_)) = - event.kind - { - return; - }; - - log::trace!("Notify handle {}", GRAY.paint(format!("{:?}", event.paths))); - - let paths: Vec<_> = event - .paths - .into_iter() - .filter_map(|p| match super::notify::convert(&p, &proj) { - Ok(p) => Some(p), - Err(e) => { - log::info!("{e}"); - None - } - }) - .collect(); - - for path in paths { - if path.starts_with_any(&proj.lib.src_paths) && path.is_ext_any(&["rs"]) { - // Check if it's possible to patch - let patches = view_macros.patch(&path); - if let Ok(Some(patch)) = patches { - log::debug!("Patching view."); - ReloadSignal::send_view_patches(&patch); - } - } - } -}