Skip to content

Commit 6f57d4a

Browse files
committed
feat(cli): added support for eatch exclusions with --custom-watch !my-path
This works fully with nesting, and exclusions override inclusions.
1 parent 5f54722 commit 6f57d4a

File tree

4 files changed

+167
-25
lines changed

4 files changed

+167
-25
lines changed

packages/perseus-cli/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ directories = "4"
5151
cargo_metadata = "0.15"
5252
cargo-lock = "8"
5353
minify-js = "0.4"
54+
walkdir = "2"
5455

5556
[dev-dependencies]
5657
assert_cmd = "2"

packages/perseus-cli/src/bin/main.rs

+141-25
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ use perseus_cli::{
1414
};
1515
use perseus_cli::{
1616
check, create_dist, delete_dist, errors::*, export_error_page, order_reload, run_reload_server,
17-
snoop_build, snoop_server, snoop_wasm_build, Tools,
17+
snoop_build, snoop_server, snoop_wasm_build, Tools, WATCH_EXCLUSIONS,
1818
};
1919
use std::env;
2020
use std::path::{Path, PathBuf};
2121
use std::process::{Command, ExitCode};
2222
use std::sync::mpsc::channel;
23+
use walkdir::WalkDir;
2324

2425
// All this does is run the program and terminate with the acquired exit code
2526
#[tokio::main]
@@ -154,6 +155,33 @@ async fn core(dir: PathBuf) -> Result<i32, Error> {
154155
custom_watch,
155156
..
156157
}) if *watch && watch_allowed => {
158+
// Parse the custom watching information into includes and excludes (these are
159+
// all canonicalized to allow easier comparison)
160+
let mut watch_includes: Vec<PathBuf> = Vec::new();
161+
let mut watch_excludes: Vec<PathBuf> = Vec::new();
162+
for custom in custom_watch {
163+
if custom.starts_with('!') {
164+
let custom = custom.strip_prefix('!').unwrap();
165+
let path = Path::new(custom);
166+
let full_path =
167+
path.canonicalize()
168+
.map_err(|err| WatchError::WatchFileNotResolved {
169+
filename: custom.to_string(),
170+
source: err,
171+
})?;
172+
watch_excludes.push(full_path);
173+
} else {
174+
let path = Path::new(custom);
175+
let full_path =
176+
path.canonicalize()
177+
.map_err(|err| WatchError::WatchFileNotResolved {
178+
filename: custom.to_string(),
179+
source: err,
180+
})?;
181+
watch_includes.push(full_path);
182+
}
183+
}
184+
157185
let (tx_term, rx) = channel();
158186
let tx_fs = tx_term.clone();
159187
// Set the handler for termination events (more than just SIGINT) on all
@@ -201,37 +229,125 @@ async fn core(dir: PathBuf) -> Result<i32, Error> {
201229
tx_fs.send(Event::Reload).unwrap();
202230
})
203231
.map_err(|err| WatchError::WatcherSetupFailed { source: err })?;
204-
// Watch the current directory
232+
233+
// Resolve all the exclusions to a list of files
234+
let mut file_watch_excludes = Vec::new();
235+
for entry in watch_excludes.iter() {
236+
// To allow exclusions to work sanely, we have to manually resolve the file tree
237+
if entry.is_dir() {
238+
// The `notify` crate internally follows symlinks, so we do here too
239+
for entry in WalkDir::new(&entry).follow_links(true) {
240+
let entry = entry
241+
.map_err(|err| WatchError::ReadCustomDirEntryFailed { source: err })?;
242+
let entry = entry.path();
243+
244+
file_watch_excludes.push(entry.to_path_buf());
245+
}
246+
} else {
247+
file_watch_excludes.push(entry.to_path_buf());
248+
}
249+
}
250+
251+
// Watch the current directory, accounting for exclusions at the top-level
252+
// simply to avoid even starting to watch `target` etc., although
253+
// user exclusions are generally handled separately (and have to be,
254+
// since otherwise we would be trying to later remove watches that were never
255+
// added)
205256
for entry in std::fs::read_dir(".")
206257
.map_err(|err| WatchError::ReadCurrentDirFailed { source: err })?
207258
{
208-
// We want to exclude `target/` and `dist`, otherwise we should watch everything
209259
let entry = entry.map_err(|err| WatchError::ReadDirEntryFailed { source: err })?;
210-
let name = entry.file_name();
211-
if name != "target"
212-
&& name != "dist"
213-
&& name != ".git"
214-
&& name != "target_engine"
215-
&& name != "target_wasm"
216-
{
217-
watcher
218-
.watch(&entry.path(), RecursiveMode::Recursive)
219-
.map_err(|err| WatchError::WatchFileFailed {
220-
filename: entry.path().to_str().unwrap().to_string(),
221-
source: err,
260+
let entry_name = entry.file_name().to_string_lossy().to_string();
261+
262+
// The base watch exclusions operate at the very top level
263+
if WATCH_EXCLUSIONS.contains(&entry_name.as_str()) {
264+
continue;
265+
}
266+
267+
let entry = entry.path().canonicalize().map_err(|err| {
268+
WatchError::WatchFileNotResolved {
269+
filename: entry.path().to_string_lossy().to_string(),
270+
source: err,
271+
}
272+
})?;
273+
// To allow exclusions to work sanely, we have to manually resolve the file tree
274+
if entry.is_dir() {
275+
// The `notify` crate internally follows symlinks, so we do here too
276+
for entry in WalkDir::new(&entry).follow_links(true) {
277+
let entry = entry
278+
.map_err(|err| WatchError::ReadCustomDirEntryFailed { source: err })?;
279+
if entry.path().is_dir() {
280+
continue;
281+
}
282+
let entry = entry.path().canonicalize().map_err(|err| {
283+
WatchError::WatchFileNotResolved {
284+
filename: entry.path().to_string_lossy().to_string(),
285+
source: err,
286+
}
222287
})?;
288+
289+
if !file_watch_excludes.contains(&entry) {
290+
watcher
291+
// The recursivity flag here will be irrelevant in all cases
292+
.watch(&entry, RecursiveMode::Recursive)
293+
.map_err(|err| WatchError::WatchFileFailed {
294+
filename: entry.to_string_lossy().to_string(),
295+
source: err,
296+
})?;
297+
}
298+
}
299+
} else {
300+
if !file_watch_excludes.contains(&entry) {
301+
watcher
302+
// The recursivity flag here will be irrelevant in all cases
303+
.watch(&entry, RecursiveMode::Recursive)
304+
.map_err(|err| WatchError::WatchFileFailed {
305+
filename: entry.to_string_lossy().to_string(),
306+
source: err,
307+
})?;
308+
}
223309
}
224310
}
225-
// Watch any other files/directories the user has nominated
226-
for entry in custom_watch.iter() {
227-
watcher
228-
// If it's a directory, we'll watch it recursively
229-
// If it's a file, the second parameter here is usefully ignored
230-
.watch(Path::new(entry), RecursiveMode::Recursive)
231-
.map_err(|err| WatchError::WatchFileFailed {
232-
filename: entry.to_string(),
233-
source: err,
234-
})?;
311+
// Watch any other files/directories the user has nominated (pre-canonicalized
312+
// at the top-level)
313+
for entry in watch_includes.iter() {
314+
// To allow exclusions to work sanely, we have to manually resolve the file tree
315+
if entry.is_dir() {
316+
// The `notify` crate internally follows symlinks, so we do here too
317+
for entry in WalkDir::new(&entry).follow_links(true) {
318+
let entry = entry
319+
.map_err(|err| WatchError::ReadCustomDirEntryFailed { source: err })?;
320+
if entry.path().is_dir() {
321+
continue;
322+
}
323+
let entry = entry.path().canonicalize().map_err(|err| {
324+
WatchError::WatchFileNotResolved {
325+
filename: entry.path().to_string_lossy().to_string(),
326+
source: err,
327+
}
328+
})?;
329+
330+
if !file_watch_excludes.contains(&entry) {
331+
watcher
332+
// The recursivity flag here will be irrelevant in all cases
333+
.watch(&entry, RecursiveMode::Recursive)
334+
.map_err(|err| WatchError::WatchFileFailed {
335+
filename: entry.to_string_lossy().to_string(),
336+
source: err,
337+
})?;
338+
}
339+
}
340+
} else {
341+
if !file_watch_excludes.contains(&entry) {
342+
watcher
343+
// The recursivity flag here will be irrelevant in all cases
344+
.watch(&entry, RecursiveMode::Recursive)
345+
.map_err(|err| WatchError::WatchFileFailed {
346+
filename: entry.to_string_lossy().to_string(),
347+
source: err,
348+
})?;
349+
}
350+
}
235351
}
236352

237353
// This will store the handle to the child process

packages/perseus-cli/src/errors.rs

+16
Original file line numberDiff line numberDiff line change
@@ -192,12 +192,23 @@ pub enum WatchError {
192192
#[source]
193193
source: std::io::Error,
194194
},
195+
#[error("the file/folder '{filename}' could not be resolved to an absolute path (does the file/folder exist?)")]
196+
WatchFileNotResolved {
197+
filename: String,
198+
source: std::io::Error,
199+
},
195200
#[error("couldn't watch file at '{filename}', try re-running the command")]
196201
WatchFileFailed {
197202
filename: String,
198203
#[source]
199204
source: notify::Error,
200205
},
206+
#[error("couldn't unwatch file at '{filename}', try re-running the command")]
207+
UnwatchFileFailed {
208+
filename: String,
209+
#[source]
210+
source: notify::Error,
211+
},
201212
#[error("an error occurred while watching files")]
202213
WatcherError {
203214
#[source]
@@ -213,6 +224,11 @@ pub enum WatchError {
213224
#[source]
214225
source: std::io::Error,
215226
},
227+
#[error("couldn't read an entry in the targeted custom directory for watching, do you have the necessary permissions?")]
228+
ReadCustomDirEntryFailed {
229+
#[source]
230+
source: walkdir::Error,
231+
},
216232
}
217233

218234
#[derive(Error, Debug)]

packages/perseus-cli/src/lib.rs

+9
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,12 @@ pub fn get_user_crate_name(dir: &Path) -> Result<String, ExecutionError> {
121121
.name;
122122
Ok(name)
123123
}
124+
125+
/// The paths of files/folders that are excluded from the watcher system by
126+
/// default. These can be overriden using the custom watcher system.
127+
///
128+
/// *Note: following v0.4.0-beta.18, these will only affect the top-level paths
129+
/// with these names, rather than any paths with these names. Further exclusions
130+
/// should be manually specified.*
131+
pub static WATCH_EXCLUSIONS: &[&str] =
132+
&["dist", "target", "target_engine", "target_browser", ".git"];

0 commit comments

Comments
 (0)