feat(core): add `server:watch` script with automatic restart on file changes (#3920)
## Description
This PR adds a new `server:watch` script that runs MagicMirror² in
server-only mode with automatic restart and browser reload capabilities.
Particularly helpful for:
- **Developers** who need to see changes immediately without manual
restarts.
- **Users setting up their mirror** who make many changes to `config.js`
or `custom.css` and need quick feedback.
### What it does
When you run `npm run server:watch`, the watcher monitors files you
specify in `config.watchTargets`. Whenever a monitored file changes:
1. The server automatically restarts
2. Waits for the port to become available
3. Sends a reload notification to all connected browsers via Socket.io
4. Browsers automatically refresh to show the changes
This creates a seamless development experience where you can edit code,
save, and see the results within seconds.
### Implementation highlights
**Zero dependencies:** Uses only Node.js built-ins (`fs.watch`,
`child_process.spawn`, `net`, `http`) - no nodemon or external watchers
needed.
**Smart file watching:** Monitors parent directories instead of files
directly to handle atomic writes from modern editors (VSCode, etc.) that
create temporary files during save operations.
**Port management:** Waits for the old server instance to fully release
the port before starting a new one, preventing "port already in use"
errors.
### Configuration
Users explicitly define which files to monitor in their `config.js`:
```js
let config = {
watchTargets: [
"config/config.js",
"css/custom.css",
"modules/MMM-MyModule/MMM-MyModule.js",
"modules/MMM-MyModule/node_helper.js"
],
// ... rest of config
};
```
This explicit approach keeps the implementation simple (~260 lines)
while giving users full control over what triggers restarts. If
`watchTargets` is empty or undefined, the watcher starts but monitors
nothing, logging a clear warning message.
---
**Note:** This PR description has been updated to reflect the final
implementation. During the review process, we refined the approach
multiple times based on feedback.
---------
Co-authored-by: Jboucly <contact@jboucly.fr>
Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com>
2025-10-28 19:14:51 +01:00
// Load lightweight internal alias resolver to enable require("logger")
require ( "../js/alias-resolver" ) ;
2026-03-07 16:34:28 +01:00
const { spawn } = require ( "node:child_process" ) ;
const fs = require ( "node:fs" ) ;
const path = require ( "node:path" ) ;
const net = require ( "node:net" ) ;
const http = require ( "node:http" ) ;
feat(core): add `server:watch` script with automatic restart on file changes (#3920)
## Description
This PR adds a new `server:watch` script that runs MagicMirror² in
server-only mode with automatic restart and browser reload capabilities.
Particularly helpful for:
- **Developers** who need to see changes immediately without manual
restarts.
- **Users setting up their mirror** who make many changes to `config.js`
or `custom.css` and need quick feedback.
### What it does
When you run `npm run server:watch`, the watcher monitors files you
specify in `config.watchTargets`. Whenever a monitored file changes:
1. The server automatically restarts
2. Waits for the port to become available
3. Sends a reload notification to all connected browsers via Socket.io
4. Browsers automatically refresh to show the changes
This creates a seamless development experience where you can edit code,
save, and see the results within seconds.
### Implementation highlights
**Zero dependencies:** Uses only Node.js built-ins (`fs.watch`,
`child_process.spawn`, `net`, `http`) - no nodemon or external watchers
needed.
**Smart file watching:** Monitors parent directories instead of files
directly to handle atomic writes from modern editors (VSCode, etc.) that
create temporary files during save operations.
**Port management:** Waits for the old server instance to fully release
the port before starting a new one, preventing "port already in use"
errors.
### Configuration
Users explicitly define which files to monitor in their `config.js`:
```js
let config = {
watchTargets: [
"config/config.js",
"css/custom.css",
"modules/MMM-MyModule/MMM-MyModule.js",
"modules/MMM-MyModule/node_helper.js"
],
// ... rest of config
};
```
This explicit approach keeps the implementation simple (~260 lines)
while giving users full control over what triggers restarts. If
`watchTargets` is empty or undefined, the watcher starts but monitors
nothing, logging a clear warning message.
---
**Note:** This PR description has been updated to reflect the final
implementation. During the review process, we refined the approach
multiple times based on feedback.
---------
Co-authored-by: Jboucly <contact@jboucly.fr>
Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com>
2025-10-28 19:14:51 +01:00
const Log = require ( "logger" ) ;
const { getConfigFilePath } = require ( "#server_functions" ) ;
const RESTART _DELAY _MS = 500 ;
const PORT _CHECK _MAX _ATTEMPTS = 20 ;
const PORT _CHECK _INTERVAL _MS = 500 ;
let child = null ;
let restartTimer = null ;
let isShuttingDown = false ;
let isRestarting = false ;
let serverConfig = null ;
const rootDir = path . join ( _ _dirname , ".." ) ;
/ * *
* Get the server configuration ( port and address )
* @ returns { { port : number , address : string } } The server config
* /
function getServerConfig ( ) {
if ( serverConfig ) return serverConfig ;
try {
const configPath = getConfigFilePath ( ) ;
delete require . cache [ require . resolve ( configPath ) ] ;
const config = require ( configPath ) ;
serverConfig = {
port : global . mmPort || config . port || 8080 ,
address : config . address || "localhost"
} ;
2026-04-02 08:56:27 +02:00
} catch {
feat(core): add `server:watch` script with automatic restart on file changes (#3920)
## Description
This PR adds a new `server:watch` script that runs MagicMirror² in
server-only mode with automatic restart and browser reload capabilities.
Particularly helpful for:
- **Developers** who need to see changes immediately without manual
restarts.
- **Users setting up their mirror** who make many changes to `config.js`
or `custom.css` and need quick feedback.
### What it does
When you run `npm run server:watch`, the watcher monitors files you
specify in `config.watchTargets`. Whenever a monitored file changes:
1. The server automatically restarts
2. Waits for the port to become available
3. Sends a reload notification to all connected browsers via Socket.io
4. Browsers automatically refresh to show the changes
This creates a seamless development experience where you can edit code,
save, and see the results within seconds.
### Implementation highlights
**Zero dependencies:** Uses only Node.js built-ins (`fs.watch`,
`child_process.spawn`, `net`, `http`) - no nodemon or external watchers
needed.
**Smart file watching:** Monitors parent directories instead of files
directly to handle atomic writes from modern editors (VSCode, etc.) that
create temporary files during save operations.
**Port management:** Waits for the old server instance to fully release
the port before starting a new one, preventing "port already in use"
errors.
### Configuration
Users explicitly define which files to monitor in their `config.js`:
```js
let config = {
watchTargets: [
"config/config.js",
"css/custom.css",
"modules/MMM-MyModule/MMM-MyModule.js",
"modules/MMM-MyModule/node_helper.js"
],
// ... rest of config
};
```
This explicit approach keeps the implementation simple (~260 lines)
while giving users full control over what triggers restarts. If
`watchTargets` is empty or undefined, the watcher starts but monitors
nothing, logging a clear warning message.
---
**Note:** This PR description has been updated to reflect the final
implementation. During the review process, we refined the approach
multiple times based on feedback.
---------
Co-authored-by: Jboucly <contact@jboucly.fr>
Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com>
2025-10-28 19:14:51 +01:00
serverConfig = { port : 8080 , address : "localhost" } ;
}
return serverConfig ;
}
/ * *
* Check if a port is available on the configured address
* @ param { number } port The port to check
* @ returns { Promise < boolean > } True if port is available
* /
function isPortAvailable ( port ) {
return new Promise ( ( resolve ) => {
const server = net . createServer ( ) ;
server . once ( "error" , ( ) => {
resolve ( false ) ;
} ) ;
server . once ( "listening" , ( ) => {
server . close ( ) ;
resolve ( true ) ;
} ) ;
// Use the same address as the actual server will bind to
const { address } = getServerConfig ( ) ;
server . listen ( port , address ) ;
} ) ;
}
/ * *
* Wait until port is available
* @ param { number } port The port to wait for
* @ param { number } maxAttempts Maximum number of attempts
* @ returns { Promise < void > }
* /
async function waitForPort ( port , maxAttempts = PORT _CHECK _MAX _ATTEMPTS ) {
for ( let i = 0 ; i < maxAttempts ; i ++ ) {
if ( await isPortAvailable ( port ) ) {
Log . info ( ` Port ${ port } is now available ` ) ;
return ;
}
await new Promise ( ( resolve ) => setTimeout ( resolve , PORT _CHECK _INTERVAL _MS ) ) ;
}
Log . warn ( ` Port ${ port } still not available after ${ maxAttempts } attempts ` ) ;
}
/ * *
* Start the server process
* /
function startServer ( ) {
// Start node directly instead of via npm to avoid process tree issues
child = spawn ( "node" , [ "./serveronly" ] , {
stdio : "inherit" ,
cwd : path . join ( _ _dirname , ".." )
} ) ;
child . on ( "error" , ( error ) => {
Log . error ( "Failed to start server process:" , error . message ) ;
child = null ;
} ) ;
child . on ( "exit" , ( code , signal ) => {
child = null ;
if ( isShuttingDown ) {
return ;
}
if ( isRestarting ) {
// Expected restart - don't log as error
isRestarting = false ;
} else {
// Unexpected exit
Log . error ( ` Server exited unexpectedly with code ${ code } and signal ${ signal } ` ) ;
}
} ) ;
}
/ * *
* Send reload notification to all connected clients
* /
function notifyClientsToReload ( ) {
const { port , address } = getServerConfig ( ) ;
const options = {
hostname : address ,
port : port ,
path : "/reload" ,
method : "GET"
} ;
const req = http . request ( options , ( res ) => {
if ( res . statusCode === 200 ) {
Log . info ( "Reload notification sent to clients" ) ;
}
} ) ;
req . on ( "error" , ( err ) => {
// Server might not be running yet, ignore
Log . debug ( ` Could not send reload notification: ${ err . message } ` ) ;
} ) ;
req . end ( ) ;
}
/ * *
* Restart the server process
* @ param { string } reason The reason for the restart
* /
2026-02-25 10:55:56 +01:00
function restartServer ( reason ) {
feat(core): add `server:watch` script with automatic restart on file changes (#3920)
## Description
This PR adds a new `server:watch` script that runs MagicMirror² in
server-only mode with automatic restart and browser reload capabilities.
Particularly helpful for:
- **Developers** who need to see changes immediately without manual
restarts.
- **Users setting up their mirror** who make many changes to `config.js`
or `custom.css` and need quick feedback.
### What it does
When you run `npm run server:watch`, the watcher monitors files you
specify in `config.watchTargets`. Whenever a monitored file changes:
1. The server automatically restarts
2. Waits for the port to become available
3. Sends a reload notification to all connected browsers via Socket.io
4. Browsers automatically refresh to show the changes
This creates a seamless development experience where you can edit code,
save, and see the results within seconds.
### Implementation highlights
**Zero dependencies:** Uses only Node.js built-ins (`fs.watch`,
`child_process.spawn`, `net`, `http`) - no nodemon or external watchers
needed.
**Smart file watching:** Monitors parent directories instead of files
directly to handle atomic writes from modern editors (VSCode, etc.) that
create temporary files during save operations.
**Port management:** Waits for the old server instance to fully release
the port before starting a new one, preventing "port already in use"
errors.
### Configuration
Users explicitly define which files to monitor in their `config.js`:
```js
let config = {
watchTargets: [
"config/config.js",
"css/custom.css",
"modules/MMM-MyModule/MMM-MyModule.js",
"modules/MMM-MyModule/node_helper.js"
],
// ... rest of config
};
```
This explicit approach keeps the implementation simple (~260 lines)
while giving users full control over what triggers restarts. If
`watchTargets` is empty or undefined, the watcher starts but monitors
nothing, logging a clear warning message.
---
**Note:** This PR description has been updated to reflect the final
implementation. During the review process, we refined the approach
multiple times based on feedback.
---------
Co-authored-by: Jboucly <contact@jboucly.fr>
Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com>
2025-10-28 19:14:51 +01:00
if ( restartTimer ) clearTimeout ( restartTimer ) ;
2026-02-25 10:55:56 +01:00
restartTimer = setTimeout ( ( ) => {
feat(core): add `server:watch` script with automatic restart on file changes (#3920)
## Description
This PR adds a new `server:watch` script that runs MagicMirror² in
server-only mode with automatic restart and browser reload capabilities.
Particularly helpful for:
- **Developers** who need to see changes immediately without manual
restarts.
- **Users setting up their mirror** who make many changes to `config.js`
or `custom.css` and need quick feedback.
### What it does
When you run `npm run server:watch`, the watcher monitors files you
specify in `config.watchTargets`. Whenever a monitored file changes:
1. The server automatically restarts
2. Waits for the port to become available
3. Sends a reload notification to all connected browsers via Socket.io
4. Browsers automatically refresh to show the changes
This creates a seamless development experience where you can edit code,
save, and see the results within seconds.
### Implementation highlights
**Zero dependencies:** Uses only Node.js built-ins (`fs.watch`,
`child_process.spawn`, `net`, `http`) - no nodemon or external watchers
needed.
**Smart file watching:** Monitors parent directories instead of files
directly to handle atomic writes from modern editors (VSCode, etc.) that
create temporary files during save operations.
**Port management:** Waits for the old server instance to fully release
the port before starting a new one, preventing "port already in use"
errors.
### Configuration
Users explicitly define which files to monitor in their `config.js`:
```js
let config = {
watchTargets: [
"config/config.js",
"css/custom.css",
"modules/MMM-MyModule/MMM-MyModule.js",
"modules/MMM-MyModule/node_helper.js"
],
// ... rest of config
};
```
This explicit approach keeps the implementation simple (~260 lines)
while giving users full control over what triggers restarts. If
`watchTargets` is empty or undefined, the watcher starts but monitors
nothing, logging a clear warning message.
---
**Note:** This PR description has been updated to reflect the final
implementation. During the review process, we refined the approach
multiple times based on feedback.
---------
Co-authored-by: Jboucly <contact@jboucly.fr>
Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com>
2025-10-28 19:14:51 +01:00
Log . info ( reason ) ;
if ( child ) {
isRestarting = true ;
// Get the actual port being used
const { port } = getServerConfig ( ) ;
// Notify clients to reload before restart
notifyClientsToReload ( ) ;
// Set up one-time listener for the exit event
child . once ( "exit" , async ( ) => {
// Wait until port is actually available
await waitForPort ( port ) ;
// Reset config cache in case it changed
serverConfig = null ;
startServer ( ) ;
} ) ;
child . kill ( "SIGTERM" ) ;
} else {
startServer ( ) ;
}
} , RESTART _DELAY _MS ) ;
}
/ * *
* Watch a specific file for changes and restart the server on change
* Watches the parent directory to handle editors that use atomic writes
* @ param { string } file The file path to watch
* /
function watchFile ( file ) {
try {
const fileName = path . basename ( file ) ;
const dirName = path . dirname ( file ) ;
const watcher = fs . watch ( dirName , ( _eventType , changedFile ) => {
// Only trigger for the specific file we're interested in
if ( changedFile !== fileName ) return ;
Log . info ( ` [watchFile] Change detected in: ${ file } ` ) ;
if ( restartTimer ) clearTimeout ( restartTimer ) ;
restartTimer = setTimeout ( ( ) => {
Log . info ( ` [watchFile] Triggering restart due to change in: ${ file } ` ) ;
restartServer ( ` File changed: ${ path . basename ( file ) } — restarting... ` ) ;
} , RESTART _DELAY _MS ) ;
} ) ;
watcher . on ( "error" , ( error ) => {
Log . error ( ` Watcher error for ${ file } : ` , error . message ) ;
} ) ;
Log . log ( ` Watching file: ${ file } ` ) ;
} catch ( error ) {
Log . error ( ` Failed to watch file ${ file } : ` , error . message ) ;
}
}
startServer ( ) ;
// Setup file watching based on config
try {
const configPath = getConfigFilePath ( ) ;
delete require . cache [ require . resolve ( configPath ) ] ;
const config = require ( configPath ) ;
let watchTargets = [ ] ;
if ( Array . isArray ( config . watchTargets ) && config . watchTargets . length > 0 ) {
watchTargets = config . watchTargets . filter ( ( target ) => typeof target === "string" && target . trim ( ) !== "" ) ;
}
if ( watchTargets . length === 0 ) {
Log . warn ( "Watch mode is enabled but no watchTargets are configured. No files will be monitored. Set the watchTargets array in your config.js to enable file watching." ) ;
}
Log . log ( ` Watch mode enabled. Watching ${ watchTargets . length } file(s) ` ) ;
// Watch each target file
for ( const target of watchTargets ) {
const targetPath = path . isAbsolute ( target )
? target
: path . join ( rootDir , target ) ;
// Check if file exists
if ( ! fs . existsSync ( targetPath ) ) {
Log . warn ( ` Watch target does not exist: ${ targetPath } ` ) ;
continue ;
}
// Check if it's a file (directories are not supported)
const stats = fs . statSync ( targetPath ) ;
if ( stats . isFile ( ) ) {
watchFile ( targetPath ) ;
} else {
Log . warn ( ` Watch target is not a file (directories not supported): ${ targetPath } ` ) ;
}
}
2026-04-02 08:56:27 +02:00
} catch {
feat(core): add `server:watch` script with automatic restart on file changes (#3920)
## Description
This PR adds a new `server:watch` script that runs MagicMirror² in
server-only mode with automatic restart and browser reload capabilities.
Particularly helpful for:
- **Developers** who need to see changes immediately without manual
restarts.
- **Users setting up their mirror** who make many changes to `config.js`
or `custom.css` and need quick feedback.
### What it does
When you run `npm run server:watch`, the watcher monitors files you
specify in `config.watchTargets`. Whenever a monitored file changes:
1. The server automatically restarts
2. Waits for the port to become available
3. Sends a reload notification to all connected browsers via Socket.io
4. Browsers automatically refresh to show the changes
This creates a seamless development experience where you can edit code,
save, and see the results within seconds.
### Implementation highlights
**Zero dependencies:** Uses only Node.js built-ins (`fs.watch`,
`child_process.spawn`, `net`, `http`) - no nodemon or external watchers
needed.
**Smart file watching:** Monitors parent directories instead of files
directly to handle atomic writes from modern editors (VSCode, etc.) that
create temporary files during save operations.
**Port management:** Waits for the old server instance to fully release
the port before starting a new one, preventing "port already in use"
errors.
### Configuration
Users explicitly define which files to monitor in their `config.js`:
```js
let config = {
watchTargets: [
"config/config.js",
"css/custom.css",
"modules/MMM-MyModule/MMM-MyModule.js",
"modules/MMM-MyModule/node_helper.js"
],
// ... rest of config
};
```
This explicit approach keeps the implementation simple (~260 lines)
while giving users full control over what triggers restarts. If
`watchTargets` is empty or undefined, the watcher starts but monitors
nothing, logging a clear warning message.
---
**Note:** This PR description has been updated to reflect the final
implementation. During the review process, we refined the approach
multiple times based on feedback.
---------
Co-authored-by: Jboucly <contact@jboucly.fr>
Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com>
2025-10-28 19:14:51 +01:00
// Config file might not exist or be invalid, use fallback targets
Log . warn ( "Could not load watchTargets from config." ) ;
}
process . on ( "SIGINT" , ( ) => {
isShuttingDown = true ;
if ( restartTimer ) clearTimeout ( restartTimer ) ;
if ( child ) child . kill ( "SIGTERM" ) ;
process . exit ( 0 ) ;
} ) ;