Skip to content

Commit 84d9495

Browse files
committed
fix: apply --watch-exclude filter to file change events
Previously, `--watch-exclude` only filtered paths when registering them with the OS file watcher. When a directory was watched recursively (e.g. `--watch=.`), the OS reported changes to all files within it, including excluded ones, because the exclude check was never applied to incoming change notifications. This fix passes the exclude set into the `new_watcher()` callback and filters changed paths before they enter the debounce channel, ensuring excluded files never trigger a restart. Closes #26217
1 parent 812d567 commit 84d9495

File tree

2 files changed

+135
-4
lines changed

2 files changed

+135
-4
lines changed

cli/util/file_watcher.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,7 @@ where
303303
let initial_cwd = std::env::current_dir()
304304
.ok()
305305
.and_then(|path| deno_path_util::url_from_directory_path(&path).ok());
306-
let exclude_set = flags.resolve_watch_exclude_set()?;
306+
let exclude_set = Arc::new(flags.resolve_watch_exclude_set()?);
307307
let (paths_to_watch_tx, mut paths_to_watch_rx) =
308308
tokio::sync::mpsc::unbounded_channel();
309309
let (restart_tx, mut restart_rx) = tokio::sync::mpsc::unbounded_channel();
@@ -354,7 +354,7 @@ where
354354
tokio::task::yield_now().await;
355355
}
356356

357-
let mut watcher = new_watcher(watcher_sender.clone())?;
357+
let mut watcher = new_watcher(watcher_sender.clone(), exclude_set.clone())?;
358358
consume_paths_to_watch(&mut watcher, &mut paths_to_watch_rx, &exclude_set);
359359

360360
let receiver_future = async {
@@ -433,6 +433,7 @@ where
433433

434434
fn new_watcher(
435435
sender: Arc<mpsc::UnboundedSender<Vec<PathBuf>>>,
436+
exclude_set: Arc<PathOrPatternSet>,
436437
) -> Result<RecommendedWatcher, AnyError> {
437438
Ok(Watcher::new(
438439
move |res: Result<NotifyEvent, NotifyError>| {
@@ -447,13 +448,16 @@ fn new_watcher(
447448
return;
448449
}
449450

450-
let paths = event
451+
let paths: Vec<PathBuf> = event
451452
.paths
452453
.iter()
453454
.filter_map(|path| canonicalize_path(path).ok())
455+
.filter(|path| !exclude_set.matches_path(path))
454456
.collect();
455457

456-
sender.send(paths).unwrap();
458+
if !paths.is_empty() {
459+
sender.send(paths).unwrap();
460+
}
457461
},
458462
Default::default(),
459463
)?)

tests/integration/watcher_tests.rs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1743,6 +1743,133 @@ async fn run_watch_with_excluded_paths() {
17431743
check_alive_then_kill(child);
17441744
}
17451745

1746+
/// Regression test for https://github.com/denoland/deno/issues/26217
1747+
/// When using `--watch=.` (directory), excluded files within that directory
1748+
/// should not trigger restarts.
1749+
#[test(flaky)]
1750+
async fn run_watch_directory_with_excluded_file() {
1751+
let t = TempDir::new();
1752+
1753+
let file_to_watch = t.path().join("file_to_watch.js");
1754+
file_to_watch.write("console.log('hello');");
1755+
1756+
let file_to_exclude = t.path().join("file_to_exclude.txt");
1757+
file_to_exclude.write("original content");
1758+
1759+
let mut child = util::deno_cmd()
1760+
.current_dir(t.path())
1761+
.arg("run")
1762+
.arg("--watch=.")
1763+
.arg("--watch-exclude=file_to_exclude.txt")
1764+
.arg("-L")
1765+
.arg("debug")
1766+
.arg(&file_to_watch)
1767+
.env("NO_COLOR", "1")
1768+
.piped_output()
1769+
.spawn()
1770+
.unwrap();
1771+
let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
1772+
1773+
wait_contains("hello", &mut stdout_lines).await;
1774+
wait_contains("finished", &mut stderr_lines).await;
1775+
1776+
// Modify excluded file - should NOT trigger restart
1777+
file_to_exclude.write("modified content");
1778+
1779+
// Modify watched file - should trigger restart
1780+
file_to_watch.write("console.log('restarted');");
1781+
wait_contains("Restarting", &mut stderr_lines).await;
1782+
wait_contains("restarted", &mut stdout_lines).await;
1783+
1784+
check_alive_then_kill(child);
1785+
}
1786+
1787+
/// When using `--watch=.`, excluded glob patterns should filter out matching
1788+
/// files from triggering restarts.
1789+
#[test(flaky)]
1790+
async fn run_watch_directory_with_excluded_glob_pattern() {
1791+
let t = TempDir::new();
1792+
1793+
let file_to_watch = t.path().join("app.js");
1794+
file_to_watch.write("console.log('hello');");
1795+
1796+
let log_file = t.path().join("debug.log");
1797+
log_file.write("log line 1");
1798+
1799+
let tmp_file = t.path().join("temp.log");
1800+
tmp_file.write("tmp data");
1801+
1802+
let mut child = util::deno_cmd()
1803+
.current_dir(t.path())
1804+
.arg("run")
1805+
.arg("--watch=.")
1806+
.arg("--watch-exclude=*.log")
1807+
.arg("-L")
1808+
.arg("debug")
1809+
.arg(&file_to_watch)
1810+
.env("NO_COLOR", "1")
1811+
.piped_output()
1812+
.spawn()
1813+
.unwrap();
1814+
let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
1815+
1816+
wait_contains("hello", &mut stdout_lines).await;
1817+
wait_contains("finished", &mut stderr_lines).await;
1818+
1819+
// Modify .log files - should NOT trigger restart
1820+
log_file.write("log line 2");
1821+
tmp_file.write("tmp data updated");
1822+
1823+
// Modify watched file - should trigger restart
1824+
file_to_watch.write("console.log('updated');");
1825+
wait_contains("Restarting", &mut stderr_lines).await;
1826+
wait_contains("updated", &mut stdout_lines).await;
1827+
1828+
check_alive_then_kill(child);
1829+
}
1830+
1831+
/// When using `--watch=.`, excluded subdirectories should not trigger restarts
1832+
/// for any file changes within them.
1833+
#[test(flaky)]
1834+
async fn run_watch_directory_with_excluded_subdirectory() {
1835+
let t = TempDir::new();
1836+
1837+
let file_to_watch = t.path().join("main.js");
1838+
file_to_watch.write("console.log('start');");
1839+
1840+
let sub_dir = t.path().join("ignored_dir");
1841+
sub_dir.create_dir_all();
1842+
let sub_file = sub_dir.join("data.txt");
1843+
sub_file.write("data");
1844+
1845+
let mut child = util::deno_cmd()
1846+
.current_dir(t.path())
1847+
.arg("run")
1848+
.arg("--watch=.")
1849+
.arg("--watch-exclude=ignored_dir")
1850+
.arg("-L")
1851+
.arg("debug")
1852+
.arg(&file_to_watch)
1853+
.env("NO_COLOR", "1")
1854+
.piped_output()
1855+
.spawn()
1856+
.unwrap();
1857+
let (mut stdout_lines, mut stderr_lines) = child_lines(&mut child);
1858+
1859+
wait_contains("start", &mut stdout_lines).await;
1860+
wait_contains("finished", &mut stderr_lines).await;
1861+
1862+
// Modify file inside excluded subdirectory - should NOT trigger restart
1863+
sub_file.write("data updated");
1864+
1865+
// Modify watched file - should trigger restart
1866+
file_to_watch.write("console.log('restarted');");
1867+
wait_contains("Restarting", &mut stderr_lines).await;
1868+
wait_contains("restarted", &mut stdout_lines).await;
1869+
1870+
check_alive_then_kill(child);
1871+
}
1872+
17461873
#[test(flaky)]
17471874
async fn run_hmr_server() {
17481875
let t = TempDir::new();

0 commit comments

Comments
 (0)