Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,659 changes: 1,542 additions & 1,117 deletions Cargo.lock

Large diffs are not rendered by default.

30 changes: 29 additions & 1 deletion e2e/nx/src/watch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
} from '@nx/e2e-utils';
import { spawn } from 'child_process';
import { join } from 'path';
import { writeFileSync, mkdtempSync } from 'fs';
import { writeFileSync, mkdtempSync, mkdirSync } from 'fs';
import { tmpdir } from 'os';

let cacheDirectory = mkdtempSync(join(tmpdir(), 'daemon'));
Expand All @@ -25,6 +25,14 @@ async function writeFileForWatcher(path: string, content: string) {
await wait(10);
}

async function mkdirForWatcher(path: string) {
const e2ePath = join(tmpProjPath(), path);

console.log(`creating directory: ${e2ePath}`);
mkdirSync(e2ePath, { recursive: true });
await wait(10);
}

describe('Nx Watch', () => {
let proj1 = uniq('proj1');
let proj2 = uniq('proj2');
Expand Down Expand Up @@ -155,6 +163,26 @@ describe('Nx Watch', () => {
expect(results).toEqual([proj1, proj3]);
}, 50000);

it('should detect files created in newly created directories', async () => {
const getOutput = await runWatch(`--all -- echo \\$NX_FILE_CHANGES`);

// Create a new subdirectory inside an existing project
await mkdirForWatcher(`libs/${proj1}/src/newsubdir`);
// Wait for the watcher to register the new directory
await wait(2000);

// Create a file in the newly created directory
await writeFileForWatcher(
`libs/${proj1}/src/newsubdir/newfile.ts`,
'export const x = 1;'
);

let output = (await getOutput())[0];
let results = output.split(' ').sort();

expect(results).toContain(`libs/${proj1}/src/newsubdir/newfile.ts`);
}, 50000);

it('should reconnect after daemon restart', async () => {
const getOutput = await runWatchWithReconnect(
`--projects=${proj1} -- echo \\$NX_PROJECT_NAME`
Expand Down
14 changes: 7 additions & 7 deletions packages/nx/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ tui-term = { git = "https://github.com/JamesHenry/tui-term", rev = "88e3b61425c9
walkdir = '2.3.3'
xxhash-rust = { version = '0.8.5', features = ['xxh3', 'xxh64'] }
vt100-ctt = { git = "https://github.com/JamesHenry/vt100-rust", rev = "b15dc3b0f7db94167a9c584f1d403899c0cc871d" }
serde = "1.0.219"
serde = ">=1.0.219, <1.0.220"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks weird?

serde_json = "1.0.140"
static_assertions = "1.1"
wrap-ansi = "0.1"
Expand All @@ -81,19 +81,19 @@ nix = { version = "0.30.0", features = ["process", "signal"] }
arboard = { version = "3.4.1", features = ["wayland-data-control"] }
crossterm = { version = "0.29.0", features = ["event-stream", "use-dev-tty"] }
portable-pty = { git = "https://github.com/cammisuli/wezterm", rev = "b538ee29e1e89eeb4832fb35ae095564dce34c29" }
ignore-files = "2.1.0"
ignore-files = "3.0.5"
fs4 = "0.12.0"
ratatui = { version = "0.29" }
reqwest = { version = "0.12.22", default-features = false, features = [
"rustls-tls-native-roots",
] }
rusqlite = { version = "0.32.1", features = ["bundled", "array", "vtab"] }
watchexec = "3.0.1"
watchexec-events = "2.0.1"
watchexec-filterer-ignore = "3.0.0"
watchexec-signals = "2.1.0"
watchexec = "8.0.1"
watchexec-events = "6.0.0"
watchexec-filterer-ignore = "7.0.0"
watchexec-signals = "5.0.1"
machine-uid = "0.5.2"
interprocess = { version = "2.2.3", features = ["tokio"] }
interprocess = { version = "=2.2.3", features = ["tokio"] }
jsonrpsee = { version = "0.25.1", features = [
"client-core",
"async-client",
Expand Down
5 changes: 5 additions & 0 deletions packages/nx/src/internal-testing-utils/temp-fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ export class TempFs {
writeFileSync(joinPathFragments(this.tempDir, filePath), content);
}

createDirSync(dirPath: string) {
const dir = joinPathFragments(this.tempDir, dirPath);
mkdirSync(dir, { recursive: true });
}

createSymlinkSync(
fileOrDirPath: string,
symlinkPath: string,
Expand Down
6 changes: 2 additions & 4 deletions packages/nx/src/native/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,10 +166,8 @@ export declare class Watcher {
origin: string
/**
* Creates a new Watcher instance.
* Will always ignore the following directories:
* * .git/
* * node_modules/
* * .nx/
* Will always ignore directories from HARDCODED_IGNORE_PATTERNS plus
* watcher-specific patterns like vite/vitest timestamp files.
*/
constructor(origin: string, additionalGlobs?: Array<string> | undefined | null, useIgnore?: boolean | undefined | null)
watch(callback: (err: string | null, events: WatchEvent[]) => void): void
Expand Down
5 changes: 3 additions & 2 deletions packages/nx/src/native/pseudo_terminal/pseudo_terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,13 +315,14 @@ mod tests {
use super::*;

#[test]
#[ignore] // hangs on Windows
fn can_run_commands() {
let mut i = 0;
let mut pseudo_terminal = PseudoTerminal::default().unwrap();
let mut pseudo_terminal = PseudoTerminal::new(PseudoTerminalOptions::default()).unwrap();
while i < 10 {
println!("Running {}", i);
let cp1 = pseudo_terminal
.run_command(String::from("whoami"), None, None, None, None, None)
.run_command(String::from("whoami"), None, None, None, None, None, None)
.unwrap();
cp1.wait_receiver.recv().unwrap();
i += 1;
Expand Down
4 changes: 2 additions & 2 deletions packages/nx/src/native/tasks/hashers/hash_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ mod tests {

#[test]
fn test_hash_runtime() {
let workspace_root = "/tmp";
let command = "echo 'runtime'";
let workspace_root = if cfg!(windows) { "C:\\" } else { "/tmp" };
let command = "echo runtime";
let env: HashMap<String, String> = HashMap::new();
let cache = Arc::new(DashMap::new());

Expand Down
74 changes: 74 additions & 0 deletions packages/nx/src/native/tests/watcher.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,80 @@ describe('watcher', () => {
temp.appendFile('inner/boo.txt', 'hello');
});
}, 15000);

it('should detect files created in newly created directories', async () => {
return new Promise<void>(async (done) => {
await wait();
watcher = new Watcher(temp.tempDir);

const allPaths: any[] = [];
watcher.watch((err, paths) => {
allPaths.push(...paths);
});

await wait();
// Create a new subdirectory (no file)
temp.createDirSync('app1/newsubdir');

// Wait for the watcher to register the new directory
await wait(2000);

// Create a file inside the new subdirectory
temp.createFileSync('app1/newsubdir/newfile.ts', 'export const x = 1;');

// Wait for the event to be processed
await wait(2000);

// Should detect the file created in the new subdirectory
expect(
allPaths.some(({ path }) => path === 'app1/newsubdir/newfile.ts')
).toBeTruthy();
done();
});
}, 15000);

it('should detect files deleted in newly created directories', async () => {
return new Promise<void>(async (done) => {
await wait();
watcher = new Watcher(temp.tempDir);

const allPaths: any[] = [];
watcher.watch((err, paths) => {
allPaths.push(...paths);
});

await wait();
// Create a new subdirectory
temp.createDirSync('app1/newsubdir2');

await wait(2000);

// Create a file
temp.createFileSync('app1/newsubdir2/todelete.ts', 'export const x = 1;');

await wait(2000);

// Delete the file
temp.removeFileSync('app1/newsubdir2/todelete.ts');

await wait(2000);

// Should detect both the create and delete
expect(
allPaths.some(
({ path, type }) =>
path === 'app1/newsubdir2/todelete.ts' && type === 'create'
)
).toBeTruthy();
expect(
allPaths.some(
({ path, type }) =>
path === 'app1/newsubdir2/todelete.ts' && type === 'delete'
)
).toBeTruthy();
done();
});
}, 20000);
});

function wait(timeout = 1000) {
Expand Down
8 changes: 7 additions & 1 deletion packages/nx/src/native/utils/socket_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,13 @@ mod tests {
let temp_dir = std::env::temp_dir().join("nx_test_socket_dir");
unsafe { env::set_var("NX_SOCKET_DIR", &temp_dir) };
let dir = get_socket_dir(root, None);
assert_eq!(dir.to_string_lossy(), temp_dir.to_string_lossy());
// On Windows, get_socket_dir wraps the path with a named pipe prefix
let expected = if cfg!(target_os = "windows") {
PathBuf::from(format!(r"\\.\pipe\nx\{}", temp_dir.to_string_lossy()))
} else {
temp_dir
};
assert_eq!(dir.to_string_lossy(), expected.to_string_lossy());
unsafe { env::remove_var("NX_SOCKET_DIR") };
}

Expand Down
33 changes: 17 additions & 16 deletions packages/nx/src/native/walker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,10 @@ where
{
let base_dir: PathBuf = directory.as_ref().into();

let mut base_ignores: Vec<String> = vec![
"**/node_modules".into(),
"**/.git".into(),
"**/.nx/cache".into(),
"**/.nx/workspace-data".into(),
"**/.yarn/cache".into(),
];
let mut base_ignores: Vec<String> = HARDCODED_IGNORE_PATTERNS
.iter()
.map(|s| (*s).to_string())
.collect();

if let Some(additional_ignores) = ignores {
base_ignores.extend(additional_ignores.iter().map(|s| format!("**/{}", s)));
Expand Down Expand Up @@ -158,20 +155,24 @@ where
receiver_thread.join().unwrap()
}

fn create_walker<P>(directory: P, use_ignores: bool) -> WalkBuilder
/// Hardcoded ignore patterns used by both the walker and the watcher.
/// These are directories that should never be walked or watched.
pub(crate) const HARDCODED_IGNORE_PATTERNS: &[&str] = &[
"**/node_modules",
"**/.git",
"**/.nx/cache",
"**/.nx/workspace-data",
"**/.yarn/cache",
];

pub(crate) fn create_walker<P>(directory: P, use_ignores: bool) -> WalkBuilder
where
P: AsRef<Path>,
{
let directory: PathBuf = directory.as_ref().into();

let ignore_glob_set = build_glob_set(&[
"**/node_modules",
"**/.git",
"**/.nx/cache",
"**/.nx/workspace-data",
"**/.yarn/cache",
])
.expect("These static ignores always build");
let ignore_glob_set =
build_glob_set(HARDCODED_IGNORE_PATTERNS).expect("These static ignores always build");

let mut walker = WalkBuilder::new(&directory);
walker.require_git(false);
Expand Down
12 changes: 12 additions & 0 deletions packages/nx/src/native/watch/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ pub fn transform_event_to_watch_events(
{
use std::fs;
use std::os::macos::fs::MetadataExt;
use watchexec_events::FileType;

// Skip directory events - they're handled by register_new_directory_watches
if path.1.map_or(false, |ft| matches!(ft, FileType::Dir)) || path_ref.is_dir() {
return Ok(vec![]);
}

let origin = origin.to_owned();
let t = fs::metadata(path_ref);
Expand Down Expand Up @@ -118,6 +124,10 @@ pub fn transform_event_to_watch_events(

#[cfg(target_os = "windows")]
{
// Skip directory events - they're handled by register_new_directory_watches
if path.1.map_or(false, |ft| matches!(ft, FileType::Dir)) {
return Ok(vec![]);
}
Ok(create_watch_event_internal(origin, event_kind, path_ref))
}

Expand Down Expand Up @@ -171,6 +181,8 @@ fn create_watch_event_internal(
) -> Vec<WatchEventInternal> {
let event_kind = match event_kind {
FileEventKind::Create(CreateKind::File) => EventType::create,
// Windows reports CreateKind::Any for file creation via ReadDirectoryChangesW
FileEventKind::Create(CreateKind::Any) => EventType::create,
FileEventKind::Modify(Name(RenameMode::To)) => EventType::create,
FileEventKind::Modify(Name(RenameMode::From)) => EventType::delete,
FileEventKind::Modify(_) => EventType::update,
Expand Down
20 changes: 18 additions & 2 deletions packages/nx/src/native/watch/watch_filterer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,17 @@ impl Filterer for WatchFilterer {

#[cfg(target_os = "linux")]
FileEventKind::Create(CreateKind::Folder) => continue,
#[cfg(target_os = "linux")]
FileEventKind::Create(CreateKind::Any) => continue,
#[cfg(target_os = "linux")]
FileEventKind::Remove(RemoveKind::Any) => continue,
#[cfg(target_os = "linux")]
FileEventKind::Modify(ModifyKind::Any) => continue,

#[cfg(target_os = "macos")]
FileEventKind::Create(CreateKind::Folder) => continue,
#[cfg(target_os = "macos")]
FileEventKind::Modify(ModifyKind::Metadata(_)) => continue,

#[cfg(windows)]
FileEventKind::Modify(ModifyKind::Any) => continue,
Expand All @@ -96,14 +107,19 @@ impl Filterer for WatchFilterer {
file_type: Some(FileType::File) | None,
} if !path.display().to_string().ends_with('~') => continue,

#[cfg(target_os = "linux")]
// Allow directory events through on Linux, Windows, and macOS so that the
// action handler can dynamically register watches for new directories.
#[cfg(any(target_os = "linux", target_os = "macos", windows))]
Tag::Path {
path: _,
file_type: Some(FileType::Dir),
} => continue,

Tag::Source(Source::Filesystem) => continue,
_ => return Ok(false),
_ => {
trace!(?tag, "tag rejected event");
return Ok(false);
}
}
}

Expand Down
Loading
Loading