watch.js 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. // Copyright (c) 2026 Cesanta Software Limited
  2. // All rights reserved
  3. const { spawn } = require('child_process');
  4. const fs = require('fs');
  5. const path = require('path');
  6. const args = process.argv.slice(2);
  7. const watchTargets = [];
  8. let command = [];
  9. for (let i = 0; i < args.length; i++) {
  10. if (args[i] === '-w' && i + 1 < args.length) {
  11. watchTargets.push(args[++i]);
  12. } else if (args[i] === '--') {
  13. command = args.slice(i + 1);
  14. break;
  15. } else if (!args[i].startsWith('-')) {
  16. command = args.slice(i);
  17. break;
  18. }
  19. }
  20. if (watchTargets.length === 0 || command.length === 0) {
  21. console.error('Usage: node watch.js -w DIR_OR_FILE [-w DIR_OR_FILE] -- COMMAND');
  22. process.exit(1);
  23. }
  24. let child = null;
  25. let restartTimeout = null;
  26. let ignoreEventsUntil = 0;
  27. let lastStartAt = 0;
  28. const MIN_RESTART_MS = 250;
  29. function start() {
  30. // Prevent restart loops caused by the command touching watched paths.
  31. ignoreEventsUntil = Date.now() + 200;
  32. lastStartAt = Date.now();
  33. if (child) {
  34. child.kill('SIGTERM');
  35. }
  36. console.log(`Starting: ${command.join(' ')}`);
  37. child = spawn(command[0], command.slice(1), { stdio: 'inherit', shell: true });
  38. child.on('exit', (code) => {
  39. if (code !== null && code !== 0) {
  40. console.log(`Process exited with code ${code}`);
  41. }
  42. });
  43. }
  44. function watch(target) {
  45. const stats = fs.statSync(target);
  46. if (stats.isFile()) {
  47. // fs.watchFile is polling-based; use a small interval to react quickly.
  48. fs.watchFile(target, { interval: 100 }, (curr, prev) => {
  49. if (Date.now() < ignoreEventsUntil) return;
  50. if (curr.mtimeMs !== prev.mtimeMs) {
  51. console.log(`File changed: ${target}`);
  52. clearTimeout(restartTimeout);
  53. const delay = Math.max(25, MIN_RESTART_MS - (Date.now() - lastStartAt));
  54. restartTimeout = setTimeout(start, delay);
  55. }
  56. });
  57. } else {
  58. const dir = target;
  59. fs.watch(dir, { recursive: true }, (eventType, filename) => {
  60. if (Date.now() < ignoreEventsUntil) return;
  61. if (filename) {
  62. console.log(`File changed: ${filename}`);
  63. clearTimeout(restartTimeout);
  64. const delay = Math.max(25, MIN_RESTART_MS - (Date.now() - lastStartAt));
  65. restartTimeout = setTimeout(start, delay);
  66. }
  67. });
  68. }
  69. }
  70. start();
  71. watchTargets.forEach(target => watch(target));
  72. console.log(`Watching ${watchTargets.join(', ')} for changes...`);
  73. process.on('SIGINT', () => {
  74. console.log('\nShutting down...');
  75. if (child) {
  76. child.kill('SIGTERM');
  77. }
  78. process.exit(0);
  79. });