@chengyixu/filewatch 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +124 -147
- package/package.json +7 -21
package/index.js
CHANGED
|
@@ -3,195 +3,172 @@
|
|
|
3
3
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
-
const { spawn
|
|
6
|
+
const { spawn } = require('child_process');
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
const
|
|
8
|
+
// Usage
|
|
9
|
+
const usage = `filewatch — watch files and run a command on changes
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
Usage:
|
|
12
|
+
filewatch [options] -- <command>
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
filewatch [options] <directory> -- <command>
|
|
23
|
-
|
|
24
|
-
Options:
|
|
25
|
-
--version, -v Show version
|
|
26
|
-
--help, -h Show this help
|
|
27
|
-
--dir, -d <path> Directory or file to watch (default: .)
|
|
28
|
-
--ext, -e <exts> Comma-separated extensions to watch (e.g. js,ts,json)
|
|
29
|
-
--ignore, -i <pat> Ignore pattern (can repeat)
|
|
30
|
-
--debounce <ms> Debounce delay in ms (default: 300)
|
|
31
|
-
--clear Clear terminal before each run
|
|
32
|
-
--quiet, -q Suppress startup message
|
|
33
|
-
|
|
34
|
-
Examples:
|
|
35
|
-
filewatch -- node server.js
|
|
36
|
-
filewatch -d src -e js,ts -- npm test
|
|
37
|
-
filewatch -d . -i node_modules -i .git -- make build
|
|
38
|
-
filewatch --debounce 500 --clear -- python main.py
|
|
39
|
-
`);
|
|
40
|
-
process.exit(0);
|
|
41
|
-
}
|
|
14
|
+
Options:
|
|
15
|
+
-d, --dir <path> Directory to watch (default: .)
|
|
16
|
+
-p, --pattern <glob> File pattern to match (default: *.*)
|
|
17
|
+
-i, --ignore <dirs> Comma-separated dirs to ignore (default: node_modules,.git)
|
|
18
|
+
-q, --quiet Suppress startup message
|
|
19
|
+
-t, --throttle <ms> Debounce in ms (default: 300)
|
|
20
|
+
-r, --restart Kill and restart command on each change
|
|
21
|
+
-h, --help Show this help
|
|
42
22
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
console.log(VERSION);
|
|
48
|
-
process.exit(0);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
23
|
+
Examples:
|
|
24
|
+
filewatch -d src -- node test.js
|
|
25
|
+
filewatch -p '*.js' -i 'dist,coverage' -- npm test
|
|
26
|
+
filewatch -t 1000 -r -- make build`;
|
|
51
27
|
|
|
52
|
-
// Parse
|
|
53
|
-
|
|
54
|
-
let
|
|
55
|
-
let
|
|
56
|
-
let
|
|
57
|
-
let clearScreen = false;
|
|
28
|
+
// Parse argv
|
|
29
|
+
const args = process.argv.slice(2);
|
|
30
|
+
let dir = '.';
|
|
31
|
+
let pattern = '*.*';
|
|
32
|
+
let ignore = ['node_modules', '.git', '.DS_Store'];
|
|
58
33
|
let quiet = false;
|
|
59
|
-
let
|
|
34
|
+
let throttle = 300;
|
|
35
|
+
let restart = false;
|
|
36
|
+
let command = null;
|
|
60
37
|
|
|
61
38
|
for (let i = 0; i < args.length; i++) {
|
|
62
|
-
const
|
|
63
|
-
if (
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
67
|
-
if (
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
ignorePatterns.push(args[++i]);
|
|
73
|
-
} else if (arg === '--debounce') {
|
|
74
|
-
debounceMs = parseInt(args[++i], 10) || 300;
|
|
75
|
-
} else if (arg === '--clear') {
|
|
76
|
-
clearScreen = true;
|
|
77
|
-
} else if (arg === '--quiet' || arg === '-q') {
|
|
78
|
-
quiet = true;
|
|
79
|
-
} else if (!arg.startsWith('-')) {
|
|
80
|
-
// Positional arg: treat as directory if no --dir given
|
|
81
|
-
if (watchDir === '.' && i < args.indexOf('--')) {
|
|
82
|
-
watchDir = arg;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
39
|
+
const a = args[i];
|
|
40
|
+
if (a === '-d' || a === '--dir') { dir = args[++i]; }
|
|
41
|
+
else if (a === '-p' || a === '--pattern') { pattern = args[++i]; }
|
|
42
|
+
else if (a === '-i' || a === '--ignore') { ignore = args[++i].split(',').map(s => s.trim()); }
|
|
43
|
+
else if (a === '-q' || a === '--quiet') { quiet = true; }
|
|
44
|
+
else if (a === '-t' || a === '--throttle') { throttle = parseInt(args[++i], 10); }
|
|
45
|
+
else if (a === '-r' || a === '--restart') { restart = true; }
|
|
46
|
+
else if (a === '-h' || a === '--help') { console.log(usage); process.exit(0); }
|
|
47
|
+
else if (a === '--') { command = args.slice(i + 1).join(' '); break; }
|
|
48
|
+
else if (!command && !a.startsWith('-')) { command = args.slice(i).join(' '); break; }
|
|
85
49
|
}
|
|
86
50
|
|
|
87
|
-
if (!
|
|
51
|
+
if (!command) {
|
|
88
52
|
console.error('Error: No command specified. Use -- before the command.');
|
|
89
|
-
console.
|
|
53
|
+
console.log(usage);
|
|
90
54
|
process.exit(1);
|
|
91
55
|
}
|
|
92
56
|
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
57
|
+
// Resolve dir
|
|
58
|
+
const watchDir = path.resolve(dir);
|
|
59
|
+
if (!fs.existsSync(watchDir)) {
|
|
60
|
+
console.error(`Error: Directory not found: ${watchDir}`);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
if (!fs.statSync(watchDir).isDirectory()) {
|
|
64
|
+
console.error(`Error: Not a directory: ${watchDir}`);
|
|
100
65
|
process.exit(1);
|
|
101
66
|
}
|
|
102
67
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
68
|
+
// Glob match helper (simple — zero deps)
|
|
69
|
+
function globMatch(filename, pattern) {
|
|
70
|
+
const re = new RegExp(
|
|
71
|
+
'^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '.*').replace(/\?/g, '.') + '$'
|
|
72
|
+
);
|
|
73
|
+
return re.test(filename);
|
|
74
|
+
}
|
|
108
75
|
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (filePath.includes(ig)) return false;
|
|
114
|
-
}
|
|
115
|
-
if (extensions.length > 0) {
|
|
116
|
-
const ext = path.extname(filePath).replace(/^\./, '');
|
|
117
|
-
if (ext && !extensions.includes(ext)) return false;
|
|
118
|
-
}
|
|
119
|
-
return true;
|
|
76
|
+
function isIgnored(filePath) {
|
|
77
|
+
const rel = path.relative(watchDir, filePath);
|
|
78
|
+
const parts = rel.split(path.sep);
|
|
79
|
+
return parts.some(p => ignore.includes(p));
|
|
120
80
|
}
|
|
121
81
|
|
|
82
|
+
// Debounce
|
|
83
|
+
let timer = null;
|
|
84
|
+
let pending = false;
|
|
122
85
|
let child = null;
|
|
123
|
-
let debounceTimer = null;
|
|
124
|
-
let running = false;
|
|
125
86
|
|
|
126
87
|
function runCommand() {
|
|
127
|
-
if (
|
|
128
|
-
if (child) {
|
|
129
|
-
child.kill('SIGTERM');
|
|
130
|
-
// Give it a moment to die
|
|
131
|
-
try { child.kill('SIGKILL'); } catch (_) {}
|
|
132
|
-
}
|
|
133
|
-
// Re-schedule
|
|
134
|
-
debounceTimer = setTimeout(runCommand, debounceMs);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
running = true;
|
|
88
|
+
if (!restart && child) return; // don't re-run if not restart mode
|
|
139
89
|
|
|
140
|
-
if (
|
|
141
|
-
|
|
90
|
+
if (child) {
|
|
91
|
+
child.kill('SIGTERM');
|
|
92
|
+
child = null;
|
|
142
93
|
}
|
|
143
94
|
|
|
144
|
-
const
|
|
145
|
-
|
|
95
|
+
const [cmd, ...cmdArgs] = command.split(/\s+/);
|
|
96
|
+
if (!quiet) {
|
|
97
|
+
const now = new Date().toLocaleTimeString();
|
|
98
|
+
process.stderr.write(`\x1b[36m[${now}]\x1b[0m running: \x1b[33m${command}\x1b[0m\n`);
|
|
99
|
+
}
|
|
146
100
|
|
|
147
101
|
child = spawn(cmd, cmdArgs, {
|
|
148
102
|
stdio: 'inherit',
|
|
149
|
-
|
|
103
|
+
cwd: watchDir,
|
|
104
|
+
shell: true,
|
|
150
105
|
});
|
|
151
106
|
|
|
152
107
|
child.on('exit', (code) => {
|
|
153
|
-
|
|
154
|
-
if (code !==
|
|
155
|
-
|
|
108
|
+
child = null;
|
|
109
|
+
if (!quiet && code !== 0 && code !== null) {
|
|
110
|
+
process.stderr.write(`\x1b[31mCommand exited with code ${code}\x1b[0m\n`);
|
|
156
111
|
}
|
|
157
112
|
});
|
|
158
|
-
|
|
159
|
-
child.on('error', (err) => {
|
|
160
|
-
running = false;
|
|
161
|
-
console.error(`[filewatch] Failed to start: ${err.message}`);
|
|
162
|
-
});
|
|
163
113
|
}
|
|
164
114
|
|
|
165
115
|
function onChange(eventType, filename) {
|
|
166
116
|
if (!filename) return;
|
|
167
|
-
const fullPath = path.join(
|
|
168
|
-
if (!shouldWatch(fullPath)) return;
|
|
117
|
+
const fullPath = path.join(watchDir, filename);
|
|
169
118
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
119
|
+
// Ignore check
|
|
120
|
+
if (isIgnored(fullPath)) return;
|
|
173
121
|
|
|
174
|
-
//
|
|
175
|
-
if (!
|
|
176
|
-
console.log(`[filewatch] Watching: ${watchPath}`);
|
|
177
|
-
if (extensions.length) console.log(`[filewatch] Extensions: ${extensions.join(',')}`);
|
|
178
|
-
console.log(`[filewatch] Command: ${cmd} ${cmdArgs.join(' ')}`);
|
|
179
|
-
console.log(`[filewatch] Ready for changes...`);
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
runCommand();
|
|
122
|
+
// Pattern match
|
|
123
|
+
if (!globMatch(filename, pattern)) return;
|
|
183
124
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
125
|
+
if (throttle > 0) {
|
|
126
|
+
if (timer) clearTimeout(timer);
|
|
127
|
+
if (!pending) {
|
|
128
|
+
pending = true;
|
|
129
|
+
if (!quiet) process.stderr.write(`\x1b[2mChange detected: ${filename}\x1b[0m\n`);
|
|
130
|
+
}
|
|
131
|
+
timer = setTimeout(() => {
|
|
132
|
+
pending = false;
|
|
133
|
+
timer = null;
|
|
134
|
+
runCommand();
|
|
135
|
+
}, throttle);
|
|
188
136
|
} else {
|
|
189
|
-
|
|
137
|
+
if (!quiet) process.stderr.write(`\x1b[2mChange detected: ${filename}\x1b[0m\n`);
|
|
138
|
+
runCommand();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Watch recursively
|
|
143
|
+
function watchRecursive(dirPath) {
|
|
144
|
+
try {
|
|
145
|
+
fs.watch(dirPath, { recursive: false }, onChange);
|
|
146
|
+
} catch (e) {
|
|
147
|
+
// Some OSes don't support recursive watch natively, fall back to manual
|
|
190
148
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
152
|
+
for (const entry of entries) {
|
|
153
|
+
if (!entry.isDirectory()) continue;
|
|
154
|
+
const full = path.join(dirPath, entry.name);
|
|
155
|
+
if (isIgnored(full)) continue;
|
|
156
|
+
watchRecursive(full);
|
|
157
|
+
}
|
|
158
|
+
} catch (e) {
|
|
159
|
+
// Permission errors, skip
|
|
196
160
|
}
|
|
197
161
|
}
|
|
162
|
+
|
|
163
|
+
// Start
|
|
164
|
+
if (!quiet) {
|
|
165
|
+
console.log(`\x1b[1mfilewatch\x1b[0m — watching \x1b[2m${watchDir}\x1b[0m`);
|
|
166
|
+
console.log(` pattern: ${pattern} | ignore: ${ignore.join(',')} | throttle: ${throttle}ms`);
|
|
167
|
+
console.log(` command: \x1b[33m${command}\x1b[0m\n`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
watchRecursive(watchDir);
|
|
171
|
+
runCommand(); // Initial run
|
|
172
|
+
|
|
173
|
+
// Keep alive
|
|
174
|
+
process.stdin.resume();
|
package/package.json
CHANGED
|
@@ -1,30 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chengyixu/filewatch",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "Watch files
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Watch files and run a command on changes — zero dependencies",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "chengyixu",
|
|
7
|
+
"keywords": ["watch", "files", "monitor", "restart", "nodemon", "cli", "dev"],
|
|
6
8
|
"bin": {
|
|
7
9
|
"filewatch": "./index.js"
|
|
8
10
|
},
|
|
9
|
-
"files": [
|
|
10
|
-
"index.js"
|
|
11
|
-
],
|
|
12
|
-
"scripts": {
|
|
13
|
-
"test": "node test.js"
|
|
14
|
-
},
|
|
15
|
-
"keywords": [
|
|
16
|
-
"watch",
|
|
17
|
-
"filewatcher",
|
|
18
|
-
"nodemon",
|
|
19
|
-
"cli",
|
|
20
|
-
"file-watch",
|
|
21
|
-
"dev",
|
|
22
|
-
"zero-dependency"
|
|
23
|
-
],
|
|
24
|
-
"author": "chengyixu",
|
|
25
|
-
"license": "MIT",
|
|
11
|
+
"files": ["index.js"],
|
|
26
12
|
"engines": {
|
|
27
|
-
"node": ">=14
|
|
13
|
+
"node": ">=14"
|
|
28
14
|
},
|
|
29
15
|
"repository": {
|
|
30
16
|
"type": "git",
|