fix(watcher.node): Improve handling of temporary files from atomic editor writes in Node watcher
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-12-11 - 6.2.3 - fix(watcher.node)
|
||||
Improve handling of temporary files from atomic editor writes in Node watcher
|
||||
|
||||
- Detect temporary files produced by atomic editor saves and attempt to map them to the real target file instead of silently skipping the event
|
||||
- Add getTempFileTarget() to extract the real file path from temp filenames (supports patterns like file.ts.tmp.PID.TIMESTAMP and generic .tmp.*)
|
||||
- When a temp-file event is seen, queue a corresponding event for the resolved real file after a short delay (50ms) to allow rename/replace to complete
|
||||
- Add logging around temp file detection and real-file checks to aid debugging
|
||||
|
||||
## 2025-12-11 - 6.2.2 - fix(watcher.node)
|
||||
Defer events during initial scan, track full event sequences, and harden watcher shutdown
|
||||
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartwatch',
|
||||
version: '6.2.2',
|
||||
version: '6.2.3',
|
||||
description: 'A cross-runtime file watcher with glob pattern support for Node.js, Deno, and Bun.'
|
||||
}
|
||||
|
||||
@@ -227,6 +227,35 @@ export class NodeWatcher implements IWatcher {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the real file path from a temporary file path
|
||||
* Used to detect atomic writes where only the temp file event is emitted
|
||||
*
|
||||
* Patterns:
|
||||
* - Claude Code: file.ts.tmp.PID.TIMESTAMP -> file.ts
|
||||
* - Vim swap: .file.ts.swp -> file.ts (but we don't handle this case)
|
||||
*/
|
||||
private getTempFileTarget(tempFilePath: string): string | null {
|
||||
const basename = path.basename(tempFilePath);
|
||||
|
||||
// Claude Code pattern: file.ts.tmp.PID.TIMESTAMP
|
||||
// Match: anything.tmp.digits.digits
|
||||
const claudeMatch = basename.match(/^(.+)\.tmp\.\d+\.\d+$/);
|
||||
if (claudeMatch) {
|
||||
const realBasename = claudeMatch[1];
|
||||
return path.join(path.dirname(tempFilePath), realBasename);
|
||||
}
|
||||
|
||||
// Generic .tmp. pattern: file.ts.tmp.something -> file.ts
|
||||
const tmpMatch = basename.match(/^(.+)\.tmp\.[^.]+$/);
|
||||
if (tmpMatch) {
|
||||
const realBasename = tmpMatch[1];
|
||||
return path.join(path.dirname(tempFilePath), realBasename);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
get isWatching(): boolean {
|
||||
return this._isWatching;
|
||||
}
|
||||
@@ -410,14 +439,26 @@ export class NodeWatcher implements IWatcher {
|
||||
|
||||
const fullPath = path.join(basePath, filename);
|
||||
|
||||
// Skip temporary files - but ONLY pure temp files, not the target of atomic writes
|
||||
// Atomic writes: editor writes to file.tmp.xxx then renames to file
|
||||
// We need to detect the final file, so only skip files that ARE temp files
|
||||
// and haven't been renamed to the real file yet
|
||||
// Handle temporary files from atomic writes (Claude Code, editors, etc.)
|
||||
// Pattern: editor writes to file.tmp.xxx then renames to file
|
||||
// Problem: fs.watch on Linux may ONLY emit event for the temp file, not the target!
|
||||
// Solution: When we see a temp file event, also check the corresponding real file
|
||||
if (this.isTemporaryFile(fullPath)) {
|
||||
// For temp files, we still want to track if they get renamed TO a real file
|
||||
// The 'rename' event fires for both source and target, so we'll catch the real file
|
||||
console.log(`[smartwatch] Skipping temp file event: ${filename}`);
|
||||
console.log(`[smartwatch] Detected temp file event: ${filename}`);
|
||||
|
||||
// Extract the real file path from the temp file path
|
||||
// Pattern: file.ts.tmp.PID.TIMESTAMP -> file.ts
|
||||
const realFilePath = this.getTempFileTarget(fullPath);
|
||||
if (realFilePath) {
|
||||
console.log(`[smartwatch] Checking corresponding real file: ${realFilePath}`);
|
||||
// Queue an event for the REAL file - this is the actual file that changed
|
||||
// Use a short delay to let the rename complete
|
||||
setTimeout(() => {
|
||||
if (this._isWatching) {
|
||||
this.handleFsEvent(basePath, path.relative(basePath, realFilePath), 'change');
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user