@chengyixu/filewatch 1.0.0 → 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.
Files changed (2) hide show
  1. package/index.js +124 -147
  2. 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, execSync } = require('child_process');
6
+ const { spawn } = require('child_process');
7
7
 
8
- const VERSION = '1.0.0';
9
- const NAME = '@chengyixu/filewatch';
8
+ // Usage
9
+ const usage = `filewatch — watch files and run a command on changes
10
10
 
11
- // Parse CLI args
12
- const args = process.argv.slice(2);
11
+ Usage:
12
+ filewatch [options] -- <command>
13
13
 
14
- if (args.includes('--help') || args.includes('-h')) {
15
- console.log(`
16
- ${NAME} v${VERSION}
17
-
18
- Watch files/directories and run a command on change. (mini nodemon)
19
-
20
- Usage:
21
- filewatch [options] -- <command>
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
- if (args.includes('--version') || args.includes('-v')) {
44
- // Filter out --version from args in case it's combined with other flags
45
- const isVersionOnly = args.length === 1 || (args.length === 2 && (args.includes('--version') || args.includes('-v')));
46
- if (isVersionOnly || args.indexOf('--version') === 0 || args.indexOf('-v') === 0) {
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 structured args
53
- let watchDir = '.';
54
- let extensions = [];
55
- let ignorePatterns = [];
56
- let debounceMs = 300;
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 cmdParts = null;
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 arg = args[i];
63
- if (arg === '--') {
64
- cmdParts = args.slice(i + 1);
65
- break;
66
- }
67
- if (arg === '--dir' || arg === '-d') {
68
- watchDir = args[++i];
69
- } else if (arg === '--ext' || arg === '-e') {
70
- extensions = args[++i].split(',').map(e => e.trim().replace(/^\./, ''));
71
- } else if (arg === '--ignore' || arg === '-i') {
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 (!cmdParts || cmdParts.length === 0) {
51
+ if (!command) {
88
52
  console.error('Error: No command specified. Use -- before the command.');
89
- console.error('Example: filewatch -- node server.js');
53
+ console.log(usage);
90
54
  process.exit(1);
91
55
  }
92
56
 
93
- const cmd = cmdParts[0];
94
- const cmdArgs = cmdParts.slice(1);
95
-
96
- // Resolve watch directory
97
- const watchPath = path.resolve(watchDir);
98
- if (!fs.existsSync(watchPath)) {
99
- console.error(`Error: Path not found: ${watchPath}`);
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
- const stat = fs.statSync(watchPath);
104
- const watchIsFile = stat.isFile();
105
-
106
- // Build ignore set
107
- const ignoreSet = new Set(ignorePatterns.map(p => path.resolve(p)));
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 shouldWatch(filePath) {
110
- if (ignoreSet.has(path.resolve(filePath))) return false;
111
- // Check if path contains any ignore pattern as substring
112
- for (const ig of ignorePatterns) {
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 (running) {
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 (clearScreen) {
141
- process.stdout.write('\x1b[2J\x1b[H');
90
+ if (child) {
91
+ child.kill('SIGTERM');
92
+ child = null;
142
93
  }
143
94
 
144
- const now = new Date().toLocaleTimeString();
145
- console.log(`\n[${now}] $ ${cmd} ${cmdArgs.join(' ')}`);
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
- shell: true
103
+ cwd: watchDir,
104
+ shell: true,
150
105
  });
151
106
 
152
107
  child.on('exit', (code) => {
153
- running = false;
154
- if (code !== null && code !== 0) {
155
- console.log(`[filewatch] Command exited with code ${code}`);
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(watchPath, watchIsFile ? '' : filename);
168
- if (!shouldWatch(fullPath)) return;
117
+ const fullPath = path.join(watchDir, filename);
169
118
 
170
- clearTimeout(debounceTimer);
171
- debounceTimer = setTimeout(runCommand, debounceMs);
172
- }
119
+ // Ignore check
120
+ if (isIgnored(fullPath)) return;
173
121
 
174
- // Initial run
175
- if (!quiet) {
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
- // Start watching
185
- try {
186
- if (watchIsFile) {
187
- fs.watch(watchPath, onChange);
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
- fs.watch(watchPath, { recursive: true }, onChange);
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
- } catch (err) {
192
- // Fallback for platforms without recursive support
193
- console.error(`[filewatch] Warning: ${err.message}`);
194
- if (!watchIsFile) {
195
- fs.watch(watchPath, onChange);
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.0",
4
- "description": "Watch files/directories and run a command on changea zero-dependency mini-nodemon",
5
- "main": "index.js",
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.0.0"
13
+ "node": ">=14"
28
14
  },
29
15
  "repository": {
30
16
  "type": "git",