@@ -2,6 +2,7 @@ const { spawn } = require("child_process");
22const fs = require ( "fs" ) ;
33const path = require ( "path" ) ;
44const net = require ( "net" ) ;
5+ const http = require ( "http" ) ;
56const Log = require ( "../js/logger" ) ;
67
78const RESTART_DELAY_MS = 500 ;
@@ -12,8 +13,8 @@ let child = null;
1213let restartTimer = null ;
1314let isShuttingDown = false ;
1415let isRestarting = false ;
15- let watcherErrorLogged = false ;
1616let serverPort = null ;
17+ const rootDir = path . join ( __dirname , ".." ) ;
1718
1819/**
1920 * Get the server port from config
@@ -106,6 +107,32 @@ function startServer () {
106107 } ) ;
107108}
108109
110+ /**
111+ * Send reload notification to all connected clients
112+ */
113+ function notifyClientsToReload ( ) {
114+ const port = getServerPort ( ) ;
115+ const options = {
116+ hostname : "localhost" ,
117+ port : port ,
118+ path : "/reload" ,
119+ method : "GET"
120+ } ;
121+
122+ const req = http . request ( options , ( res ) => {
123+ if ( res . statusCode === 200 ) {
124+ Log . info ( "Reload notification sent to clients" ) ;
125+ }
126+ } ) ;
127+
128+ req . on ( "error" , ( err ) => {
129+ // Server might not be running yet, ignore
130+ Log . debug ( `Could not send reload notification: ${ err . message } ` ) ;
131+ } ) ;
132+
133+ req . end ( ) ;
134+ }
135+
109136/**
110137 * Restart the server process
111138 * @param {string } reason The reason for the restart
@@ -122,6 +149,9 @@ async function restartServer (reason) {
122149 // Get the actual port being used
123150 const port = getServerPort ( ) ;
124151
152+ // Notify clients to reload before restart
153+ notifyClientsToReload ( ) ;
154+
125155 // Set up one-time listener for the exit event
126156 child . once ( "exit" , async ( ) => {
127157 // Wait until port is actually available
@@ -138,63 +168,34 @@ async function restartServer (reason) {
138168 } , RESTART_DELAY_MS ) ;
139169}
140170
141- /**
142- * Watch a directory for changes and restart the server on change
143- * @param {string } dir The directory path to watch
144- */
145- function watchDir ( dir ) {
146- try {
147- const watcher = fs . watch ( dir , { recursive : true } , ( _eventType , filename ) => {
148- if ( ! filename ) return ;
149-
150- // Ignore node_modules - too many changes during npm install
151- // After installing dependencies, manually restart the watcher
152- if ( filename . includes ( "node_modules" ) ) return ;
153-
154- // Only watch .js, .mjs and .cjs files
155- if ( ! filename . endsWith ( ".js" ) && ! filename . endsWith ( ".mjs" ) && ! filename . endsWith ( ".cjs" ) ) return ;
156-
157- if ( restartTimer ) clearTimeout ( restartTimer ) ;
158-
159- restartTimer = setTimeout ( ( ) => {
160- restartServer ( `Changes detected in ${ dir } : ${ filename } — restarting...` ) ;
161- } , RESTART_DELAY_MS ) ;
162- } ) ;
163-
164- watcher . on ( "error" , ( error ) => {
165- if ( error . code === "ENOSPC" ) {
166- if ( ! watcherErrorLogged ) {
167- watcherErrorLogged = true ;
168- Log . error ( "System limit for file watchers reached. Try increasing: sudo sysctl fs.inotify.max_user_watches=524288" ) ;
169- }
170- } else {
171- Log . error ( `Watcher error for ${ dir } :` , error . message ) ;
172- }
173- } ) ;
174- } catch ( error ) {
175- Log . error ( `Failed to watch directory ${ dir } :` , error . message ) ;
176- }
177- }
178-
179171/**
180172 * Watch a specific file for changes and restart the server on change
173+ * Watches the parent directory to handle editors that use atomic writes
181174 * @param {string } file The file path to watch
182175 */
183176function watchFile ( file ) {
184177 try {
185- const watcher = fs . watch ( file , ( _eventType ) => {
178+ const fileName = path . basename ( file ) ;
179+ const dirName = path . dirname ( file ) ;
180+
181+ const watcher = fs . watch ( dirName , ( _eventType , changedFile ) => {
182+ // Only trigger for the specific file we're interested in
183+ if ( changedFile !== fileName ) return ;
184+
185+ Log . info ( `[watchFile] Change detected in: ${ file } ` ) ;
186186 if ( restartTimer ) clearTimeout ( restartTimer ) ;
187187
188188 restartTimer = setTimeout ( ( ) => {
189- restartServer ( `Config file changed: ${ path . basename ( file ) } — restarting...` ) ;
189+ Log . info ( `[watchFile] Triggering restart due to change in: ${ file } ` ) ;
190+ restartServer ( `File changed: ${ path . basename ( file ) } — restarting...` ) ;
190191 } , RESTART_DELAY_MS ) ;
191192 } ) ;
192193
193194 watcher . on ( "error" , ( error ) => {
194195 Log . error ( `Watcher error for ${ file } :` , error . message ) ;
195196 } ) ;
196197
197- Log . log ( `Watching config file: ${ file } ` ) ;
198+ Log . log ( `Watching file: ${ file } ` ) ;
198199 } catch ( error ) {
199200 Log . error ( `Failed to watch file ${ file } :` , error . message ) ;
200201 }
@@ -223,13 +224,91 @@ startServer();
223224const configFile = getConfigFilePath ( ) ;
224225watchFile ( configFile ) ;
225226
226- // Watch core directories (modules, js and serveronly)
227- // We watch specific directories instead of the whole project root to avoid
228- // watching unnecessary files like node_modules (even though we filter it),
229- // tests, translations, css, fonts, vendor, etc.
230- watchDir ( path . join ( __dirname , ".." , "modules" ) ) ;
231- watchDir ( path . join ( __dirname , ".." , "js" ) ) ;
232- watchDir ( path . join ( __dirname ) ) ; // serveronly
227+ /**
228+ * Resolve the active custom CSS path based on config or environment overrides
229+ * @param {object } config The loaded MagicMirror config
230+ * @returns {string } Absolute path to the CSS file
231+ */
232+ function resolveCustomCssPath ( config = { } ) {
233+ const cssFromEnv = process . env . MM_CUSTOMCSS_FILE ;
234+ let cssPath = cssFromEnv || config . customCss || "css/custom.css" ;
235+
236+ if ( ! cssPath || typeof cssPath !== "string" ) {
237+ cssPath = "css/custom.css" ;
238+ }
239+
240+ return path . isAbsolute ( cssPath ) ? cssPath : path . join ( rootDir , cssPath ) ;
241+ }
242+
243+ /**
244+ * Determine fallback watch targets when no explicit watchTargets are provided
245+ * @param {object } config The loaded MagicMirror config (may be partial)
246+ * @returns {string[] } Array of absolute paths to watch
247+ */
248+ function getFallbackWatchTargets ( config = { } ) {
249+ const targets = new Set ( ) ;
250+ if ( configFile ) {
251+ targets . add ( configFile ) ;
252+ }
253+
254+ const cssPath = resolveCustomCssPath ( config ) ;
255+ if ( cssPath ) {
256+ targets . add ( cssPath ) ;
257+ }
258+
259+ return Array . from ( targets ) ;
260+ }
261+
262+ // Setup file watching based on config
263+ try {
264+ const configPath = getConfigFilePath ( ) ;
265+ delete require . cache [ require . resolve ( configPath ) ] ;
266+ const config = require ( configPath ) ;
267+
268+ let watchTargets = [ ] ;
269+ if ( Array . isArray ( config . watchTargets ) && config . watchTargets . length > 0 ) {
270+ watchTargets = config . watchTargets . filter ( ( target ) => typeof target === "string" && target . trim ( ) !== "" ) ;
271+ } else {
272+ watchTargets = getFallbackWatchTargets ( config ) ;
273+ Log . log ( "Watch targets not specified. Using active config and custom CSS as fallback." ) ;
274+ }
275+
276+ Log . log ( `Watch mode enabled. Watching ${ watchTargets . length } file(s)` ) ;
277+
278+ // Watch each target file
279+ for ( const target of watchTargets ) {
280+ const targetPath = path . isAbsolute ( target )
281+ ? target
282+ : path . join ( rootDir , target ) ;
283+
284+ // Check if file exists
285+ if ( ! fs . existsSync ( targetPath ) ) {
286+ Log . warn ( `Watch target does not exist: ${ targetPath } ` ) ;
287+ continue ;
288+ }
289+
290+ // Check if it's a file (directories are not supported)
291+ const stats = fs . statSync ( targetPath ) ;
292+ if ( stats . isFile ( ) ) {
293+ watchFile ( targetPath ) ;
294+ } else {
295+ Log . warn ( `Watch target is not a file (directories not supported): ${ targetPath } ` ) ;
296+ }
297+ }
298+ } catch ( err ) {
299+ // Config file might not exist or be invalid, use fallback targets
300+ Log . warn ( "Could not load watchTargets from config, watching active config/custom CSS instead:" , err . message ) ;
301+
302+ for ( const target of getFallbackWatchTargets ( ) ) {
303+ if ( ! fs . existsSync ( target ) ) {
304+ Log . warn ( `Fallback watch target does not exist: ${ target } ` ) ;
305+ continue ;
306+ }
307+
308+ watchFile ( target ) ;
309+ Log . log ( `Watching fallback file: ${ target } ` ) ;
310+ }
311+ }
233312
234313process . on ( "SIGINT" , ( ) => {
235314 isShuttingDown = true ;
0 commit comments