fix(core): correctly output created and update events for the watcher on macos (#18186)

Co-authored-by: FrozenPandaz <jasonjean1993@gmail.com>
This commit is contained in:
Jonathan Cammisuli 2023-07-19 18:05:52 -04:00 committed by GitHub
parent 28df2057fd
commit fb0f1f0ec8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 63 additions and 33 deletions

View File

@ -27,7 +27,7 @@ describe('watcher', () => {
}); });
it('should trigger the callback for files that are not ignored', (done) => { it('should trigger the callback for files that are not ignored', (done) => {
watcher = new Watcher(realpathSync(temp.tempDir)); watcher = new Watcher(temp.tempDir);
watcher.watch((error, paths) => { watcher.watch((error, paths) => {
expect(paths).toMatchInlineSnapshot(` expect(paths).toMatchInlineSnapshot(`
[ [
@ -48,7 +48,7 @@ describe('watcher', () => {
}); });
it('should trigger the callback when files are updated', (done) => { it('should trigger the callback when files are updated', (done) => {
watcher = new Watcher(realpathSync(temp.tempDir)); watcher = new Watcher(temp.tempDir);
watcher.watch((err, paths) => { watcher.watch((err, paths) => {
expect(paths).toMatchInlineSnapshot(` expect(paths).toMatchInlineSnapshot(`
@ -62,7 +62,7 @@ describe('watcher', () => {
done(); done();
}); });
wait().then(() => { wait(1000).then(() => {
// nxignored file should not trigger a callback // nxignored file should not trigger a callback
temp.appendFile('app2/main.js', 'update'); temp.appendFile('app2/main.js', 'update');
temp.appendFile('app1/main.js', 'update'); temp.appendFile('app1/main.js', 'update');
@ -70,18 +70,22 @@ describe('watcher', () => {
}); });
it('should watch file renames', (done) => { it('should watch file renames', (done) => {
watcher = new Watcher(realpathSync(temp.tempDir)); watcher = new Watcher(temp.tempDir);
watcher.watch((err, paths) => { watcher.watch((err, paths) => {
expect(paths.length).toBe(2); expect(paths.length).toBe(2);
expect(paths.find((p) => p.type === 'update')).toMatchObject({ expect(paths.find((p) => p.type === 'create')).toMatchInlineSnapshot(`
path: 'app1/rename.js', {
type: 'update', "path": "app1/rename.js",
}); "type": "create",
expect(paths.find((p) => p.type === 'delete')).toMatchObject({ }
path: 'app1/main.js', `);
type: 'delete', expect(paths.find((p) => p.type === 'delete')).toMatchInlineSnapshot(`
}); {
"path": "app1/main.js",
"type": "delete",
}
`);
done(); done();
}); });
@ -91,7 +95,7 @@ describe('watcher', () => {
}); });
it('should trigger on deletes', (done) => { it('should trigger on deletes', (done) => {
watcher = new Watcher(realpathSync(temp.tempDir)); watcher = new Watcher(temp.tempDir);
watcher.watch((err, paths) => { watcher.watch((err, paths) => {
expect(paths).toMatchInlineSnapshot(` expect(paths).toMatchInlineSnapshot(`
@ -111,7 +115,7 @@ describe('watcher', () => {
}); });
it('should ignore nested gitignores', (done) => { it('should ignore nested gitignores', (done) => {
watcher = new Watcher(realpathSync(temp.tempDir)); watcher = new Watcher(temp.tempDir);
watcher.watch((err, paths) => { watcher.watch((err, paths) => {
expect(paths).toMatchInlineSnapshot(` expect(paths).toMatchInlineSnapshot(`
@ -133,10 +137,10 @@ describe('watcher', () => {
}); });
}); });
function wait() { function wait(timeout = 500) {
return new Promise<void>((res) => { return new Promise<void>((res) => {
setTimeout(() => { setTimeout(() => {
res(); res();
}, 500); }, timeout);
}); });
} }

View File

@ -1,7 +1,9 @@
use napi::bindgen_prelude::*; use napi::bindgen_prelude::*;
use std::path::PathBuf; use std::path::PathBuf;
use tracing::trace; use tracing::trace;
use watchexec_events::filekind::FileEventKind; use watchexec_events::filekind::ModifyKind::Name;
use watchexec_events::filekind::RenameMode;
use watchexec_events::{Event, Tag}; use watchexec_events::{Event, Tag};
#[napi(string_enum)] #[napi(string_enum)]
@ -67,11 +69,37 @@ impl From<&Event> for WatchEventInternal {
let event_type = if matches!(path.1, None) && !path_ref.exists() { let event_type = if matches!(path.1, None) && !path_ref.exists() {
EventType::delete EventType::delete
} else { } else {
match event_kind { #[cfg(target_os = "macos")]
FileEventKind::Create(_) => EventType::create, {
FileEventKind::Modify(_) => EventType::update, use std::fs;
FileEventKind::Remove(_) => EventType::delete, use std::os::macos::fs::MetadataExt;
_ => EventType::update,
let t = fs::metadata(path_ref).expect("metadata should be available");
let modified_time = t.st_mtime();
let birth_time = t.st_birthtime();
// if a file is created and updated near the same time, we always get a create event
// so we need to check the timestamps to see if it was created or updated
// if the modified time is the same as birth_time then it was created
if modified_time == birth_time {
EventType::create
} else {
EventType::update
}
}
#[cfg(not(target_os = "macos"))]
{
use watchexec_events::filekind::FileEventKind;
match event_kind {
FileEventKind::Create(_) => EventType::create,
FileEventKind::Modify(Name(RenameMode::To)) => EventType::create,
FileEventKind::Modify(Name(RenameMode::From)) => EventType::delete,
FileEventKind::Modify(_) => EventType::update,
_ => EventType::update,
}
} }
}; };

View File

@ -2,7 +2,6 @@ use crate::native::watch::utils::get_ignore_files;
use crate::native::watch::watch_filterer::WatchFilterer; use crate::native::watch::watch_filterer::WatchFilterer;
use ignore_files::IgnoreFilter; use ignore_files::IgnoreFilter;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration;
use tracing::trace; use tracing::trace;
use watchexec::config::RuntimeConfig; use watchexec::config::RuntimeConfig;
use watchexec_filterer_ignore::IgnoreFilterer; use watchexec_filterer_ignore::IgnoreFilterer;

View File

@ -5,7 +5,6 @@ use std::path::MAIN_SEPARATOR;
use std::sync::Arc; use std::sync::Arc;
use crate::native::watch::types::{EventType, WatchEvent, WatchEventInternal}; use crate::native::watch::types::{EventType, WatchEvent, WatchEventInternal};
use itertools::Itertools;
use napi::bindgen_prelude::*; use napi::bindgen_prelude::*;
use napi::threadsafe_function::{ use napi::threadsafe_function::{
ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode, ThreadSafeCallContext, ThreadsafeFunction, ThreadsafeFunctionCallMode,

View File

@ -1,17 +1,17 @@
import { basename, dirname, join } from 'path'; import { dirname, join } from 'path';
import { tmpdir } from 'os'; import { tmpdir } from 'os';
import { import {
mkdtempSync,
readFile,
outputFile,
rmSync,
emptyDirSync, emptyDirSync,
outputFileSync,
unlinkSync,
mkdirpSync, mkdirpSync,
mkdtempSync,
outputFile,
readFile,
realpathSync,
rmSync,
unlinkSync,
} from 'fs-extra'; } from 'fs-extra';
import { joinPathFragments } from '../path'; import { joinPathFragments } from '../path';
import { appendFileSync, writeFileSync, renameSync, existsSync } from 'fs'; import { appendFileSync, existsSync, renameSync, writeFileSync } from 'fs';
type NestedFiles = { type NestedFiles = {
[fileName: string]: string; [fileName: string]: string;
@ -20,7 +20,7 @@ type NestedFiles = {
export class TempFs { export class TempFs {
readonly tempDir: string; readonly tempDir: string;
constructor(private dirname: string, overrideWorkspaceRoot = true) { constructor(private dirname: string, overrideWorkspaceRoot = true) {
this.tempDir = mkdtempSync(join(tmpdir(), this.dirname)); this.tempDir = realpathSync(mkdtempSync(join(tmpdir(), this.dirname)));
if (overrideWorkspaceRoot) { if (overrideWorkspaceRoot) {
process.env.NX_WORKSPACE_ROOT_PATH = this.tempDir; process.env.NX_WORKSPACE_ROOT_PATH = this.tempDir;
} }