From 8da1f1c9078dadaee4099e6d41972f9987c317eb Mon Sep 17 00:00:00 2001 From: Jboucly Date: Wed, 22 Oct 2025 13:26:22 +0200 Subject: [PATCH 01/17] feat: add .nvmrc file with Node.js v22.20.0 --- .nvmrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..c004e356d6 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v22.20.0 From 74506e5edff8794873739c8b023c4ece80ce2fb7 Mon Sep 17 00:00:00 2001 From: Jboucly Date: Wed, 22 Oct 2025 13:26:22 +0200 Subject: [PATCH 02/17] feat: add nodemon for development et and created nodemon.json file for configuration --- nodemon.json | 8 ++++ package-lock.json | 96 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 105 insertions(+) create mode 100644 nodemon.json diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000000..ab40e25727 --- /dev/null +++ b/nodemon.json @@ -0,0 +1,8 @@ +{ + "watch": ["modules", "config/config.js"], + "ext": "ts,mjs,js,json", + "env": { + "SOURCE_MAP": true + }, + "exec": "node ./serveronly" +} diff --git a/package-lock.json b/package-lock.json index 9e8e76e140..3e41b7441c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7408,6 +7408,13 @@ "node": ">= 4" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -10343,6 +10350,58 @@ "dev": true, "license": "MIT" }, + "node_modules/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -11525,6 +11584,13 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", @@ -12381,6 +12447,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -13655,6 +13734,16 @@ "node": ">=0.6" } }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/tough-cookie": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", @@ -13904,6 +13993,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, "node_modules/undici": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", diff --git a/package.json b/package.json index 139baf9400..e304ee018f 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "lint:prettier": "prettier . --write", "prepare": "[ -f node_modules/.bin/husky ] && husky || echo no husky installed.", "server": "node ./serveronly", + "server:dev": "nodemon", "start": "node --run start:x11", "start:dev": "node --run start:x11 -- dev", "start:wayland": "WAYLAND_DISPLAY=\"${WAYLAND_DISPLAY:=wayland-1}\" ./node_modules/.bin/electron js/electron.js --enable-features=UseOzonePlatform --ozone-platform=wayland", From 4dbfe29f46f551a2d8d4724c56f0b30d561d9e9e Mon Sep 17 00:00:00 2001 From: Jboucly Date: Wed, 22 Oct 2025 13:26:23 +0200 Subject: [PATCH 03/17] feat: update changelog file --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4f9c9a0c4..845a575948 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ planned for 2026-01-01 ### Added - [weather] feat: add configurable forecast date format option (#3918) +- [config] Add `.nvmrc` file to specify node version for nvm users +- [core] Add new script `server:dev` to start MagicMirror² server only without electron but with live reload support ### Changed From 925d91d53bae587a32f6aee43a3f77132ebe4436 Mon Sep 17 00:00:00 2001 From: Jboucly Date: Wed, 22 Oct 2025 13:26:23 +0200 Subject: [PATCH 04/17] feat: remove .nvmrc file --- .nvmrc | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .nvmrc diff --git a/.nvmrc b/.nvmrc deleted file mode 100644 index c004e356d6..0000000000 --- a/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -v22.20.0 From 833aca968a1b1e2600c827ee0776d95bacb9b702 Mon Sep 17 00:00:00 2001 From: Jboucly Date: Wed, 22 Oct 2025 13:26:23 +0200 Subject: [PATCH 05/17] refactor(deps): remove nodemon --- package-lock.json | 96 ----------------------------------------------- 1 file changed, 96 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3e41b7441c..9e8e76e140 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7408,13 +7408,6 @@ "node": ">= 4" } }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true, - "license": "ISC" - }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -10350,58 +10343,6 @@ "dev": true, "license": "MIT" }, - "node_modules/nodemon": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", - "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nodemon/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nodemon/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -11584,13 +11525,6 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true, - "license": "MIT" - }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", @@ -12447,19 +12381,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -13734,16 +13655,6 @@ "node": ">=0.6" } }, - "node_modules/touch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", - "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", - "dev": true, - "license": "ISC", - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, "node_modules/tough-cookie": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", @@ -13993,13 +13904,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true, - "license": "MIT" - }, "node_modules/undici": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.16.0.tgz", From 95c6d7f65170e1608280c8ec9c914526f0ea3cfd Mon Sep 17 00:00:00 2001 From: Jboucly Date: Wed, 22 Oct 2025 13:26:23 +0200 Subject: [PATCH 06/17] feat: add feature to reload server with rs or restart in stdin --- js/app.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/js/app.js b/js/app.js index 5e5d3aee85..e40029a7a0 100644 --- a/js/app.js +++ b/js/app.js @@ -388,6 +388,16 @@ function App () { return httpServer.close(); }; + + this.restart = async function () { + Log.info("Restarting MagicMirror..."); + + await this.stop(); + await this.start(); + + Log.info("MagicMirror restarted!"); + }; + /** * Listen for SIGINT signal and call stop() function. * @@ -416,6 +426,17 @@ function App () { await this.stop(); process.exit(0); }); + + /** + * Listen for input 'restart' or 'rs' on stdin to restart the server. + */ + process.stdin.setEncoding("utf8").on("data", (data) => { + const input = data.trim(); + + if (input === "restart" || input === "rs") { + this.restart(); + } + }); } module.exports = new App(); From fa3dcad05e8f898b243b70270970559393227639 Mon Sep 17 00:00:00 2001 From: Jboucly Date: Wed, 22 Oct 2025 13:26:23 +0200 Subject: [PATCH 07/17] feat: add a monitoring script to restart the server on changes --- package.json | 2 +- serveronly/watcher.js | 49 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 serveronly/watcher.js diff --git a/package.json b/package.json index e304ee018f..1d894d4867 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "lint:prettier": "prettier . --write", "prepare": "[ -f node_modules/.bin/husky ] && husky || echo no husky installed.", "server": "node ./serveronly", - "server:dev": "nodemon", + "server:watch": "node serveronly/watcher.js", "start": "node --run start:x11", "start:dev": "node --run start:x11 -- dev", "start:wayland": "WAYLAND_DISPLAY=\"${WAYLAND_DISPLAY:=wayland-1}\" ./node_modules/.bin/electron js/electron.js --enable-features=UseOzonePlatform --ozone-platform=wayland", diff --git a/serveronly/watcher.js b/serveronly/watcher.js new file mode 100644 index 0000000000..07a0ddc5a9 --- /dev/null +++ b/serveronly/watcher.js @@ -0,0 +1,49 @@ +const { spawn } = require("child_process"); +const fs = require("fs"); +const path = require("path"); +const Log = require("../js/logger"); + +let child = null; +let restartTimer = null; + +/** + * Start the server process + */ +function startServer () { + child = spawn("npm", ["run", "server"], { stdio: "inherit" }); + + child.on("exit", (code) => { + Log.info(`Changes detected : ${code}`); + Log.info("Restarting server..."); + startServer(); + }); +} + +/** + * Watch a directory for changes and restart the server on change + * @param dir + */ +function watchDir (dir) { + fs.watch(dir, { recursive: true }, (_eventType, filename) => { + if (!filename) return; + + if (dir.includes("modules") && !filename.endsWith("node_helper.js")) return; + + if (restartTimer) clearTimeout(restartTimer); + + restartTimer = setTimeout(() => { + Log.info(`Changes detected in ${dir}: ${filename} — restarting...`); + if (child) child.kill("SIGTERM"); + }, 500); + }); +} + +startServer(); +watchDir(path.join(__dirname, "..", "config")); +watchDir(path.join(__dirname, "..", "modules")); + +process.on("SIGINT", () => { + if (restartTimer) clearTimeout(restartTimer); + if (child) child.kill("SIGTERM"); + process.exit(0); +}); From 8b24dbf6a0ee62418e72b886e439094121ecdd66 Mon Sep 17 00:00:00 2001 From: Jboucly Date: Wed, 22 Oct 2025 13:26:23 +0200 Subject: [PATCH 08/17] feat: update changelog to reflect new server:watch script and remove .nvmrc entry --- CHANGELOG.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 845a575948..92ac19b803 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,7 @@ planned for 2026-01-01 ### Added - [weather] feat: add configurable forecast date format option (#3918) -- [config] Add `.nvmrc` file to specify node version for nvm users -- [core] Add new script `server:dev` to start MagicMirror² server only without electron but with live reload support +- [core] Add new script `server:watch` to start MagicMirror² server only without electron but with live reload support on changes for `node_helpers.js` files in `modules` directory and `config` folder ### Changed From dfb1a5529073a1c3894ea1377b1f5293c4c93864 Mon Sep 17 00:00:00 2001 From: Jboucly Date: Wed, 22 Oct 2025 13:26:23 +0200 Subject: [PATCH 09/17] feat: remove useless config file --- nodemon.json | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 nodemon.json diff --git a/nodemon.json b/nodemon.json deleted file mode 100644 index ab40e25727..0000000000 --- a/nodemon.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "watch": ["modules", "config/config.js"], - "ext": "ts,mjs,js,json", - "env": { - "SOURCE_MAP": true - }, - "exec": "node ./serveronly" -} From c2103f5559d493e576a1d1ba75109c4c459bb77b Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:51:31 +0200 Subject: [PATCH 10/17] refactor(core): improve server:watch script with better error handling and port management - Add port availability checks before restart to prevent race conditions - Add error handler for spawn failures - Extract hardcoded values to constants (RESTART_DELAY_MS, PORT_CHECK_*) - Add getConfigFilePath() helper for cleaner config path resolution - Watch js/ and serveronly/ directories in addition to modules/ and config/ - Watch all JS files (.js, .mjs, .cjs) instead of just node_helper.js - Improve restart mechanism with isRestarting flag - Better error messages for watcher and spawn failures - Remove unused app.restart() method from app.js These changes make the watcher more robust and easier to maintain. --- CHANGELOG.md | 2 +- js/app.js | 21 ---- package.json | 2 +- serveronly/watcher.js | 222 +++++++++++++++++++++++++++++++++++++++--- 4 files changed, 208 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92ac19b803..811eeebd3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ planned for 2026-01-01 ### Added - [weather] feat: add configurable forecast date format option (#3918) -- [core] Add new script `server:watch` to start MagicMirror² server only without electron but with live reload support on changes for `node_helpers.js` files in `modules` directory and `config` folder +- [core] Add new `server:watch` script to run MagicMirror² server-only with automatic restarts when JS files in `modules`, `js`, `serveronly`, or the `config` files changes (#3920) ### Changed diff --git a/js/app.js b/js/app.js index e40029a7a0..5e5d3aee85 100644 --- a/js/app.js +++ b/js/app.js @@ -388,16 +388,6 @@ function App () { return httpServer.close(); }; - - this.restart = async function () { - Log.info("Restarting MagicMirror..."); - - await this.stop(); - await this.start(); - - Log.info("MagicMirror restarted!"); - }; - /** * Listen for SIGINT signal and call stop() function. * @@ -426,17 +416,6 @@ function App () { await this.stop(); process.exit(0); }); - - /** - * Listen for input 'restart' or 'rs' on stdin to restart the server. - */ - process.stdin.setEncoding("utf8").on("data", (data) => { - const input = data.trim(); - - if (input === "restart" || input === "rs") { - this.restart(); - } - }); } module.exports = new App(); diff --git a/package.json b/package.json index 1d894d4867..06fb3cf855 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "lint:prettier": "prettier . --write", "prepare": "[ -f node_modules/.bin/husky ] && husky || echo no husky installed.", "server": "node ./serveronly", - "server:watch": "node serveronly/watcher.js", + "server:watch": "node ./serveronly/watcher.js", "start": "node --run start:x11", "start:dev": "node --run start:x11 -- dev", "start:wayland": "WAYLAND_DISPLAY=\"${WAYLAND_DISPLAY:=wayland-1}\" ./node_modules/.bin/electron js/electron.js --enable-features=UseOzonePlatform --ozone-platform=wayland", diff --git a/serveronly/watcher.js b/serveronly/watcher.js index 07a0ddc5a9..d11438fa45 100644 --- a/serveronly/watcher.js +++ b/serveronly/watcher.js @@ -1,48 +1,238 @@ const { spawn } = require("child_process"); const fs = require("fs"); const path = require("path"); +const net = require("net"); const Log = require("../js/logger"); +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 watcherErrorLogged = false; +let serverPort = null; + +/** + * Get the server port from config + * @returns {number} The port number + */ +function getServerPort () { + if (serverPort) return serverPort; + + try { + // Try to read the config file to get the port + const configPath = path.join(__dirname, "..", "config", "config.js"); + delete require.cache[require.resolve(configPath)]; + const config = require(configPath); + serverPort = global.mmPort || config.port || 8080; + } catch (err) { + serverPort = 8080; + } + + return serverPort; +} + +/** + * Check if a port is available + * @param {number} port The port to check + * @returns {Promise} 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); + }); + + server.listen(port, "127.0.0.1"); + }); +} + +/** + * Wait until port is available + * @param {number} port The port to wait for + * @param {number} maxAttempts Maximum number of attempts + * @returns {Promise} + */ +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 () { - child = spawn("npm", ["run", "server"], { stdio: "inherit" }); + // Start node directly instead of via npm to avoid process tree issues + child = spawn("node", ["./serveronly"], { + stdio: "inherit", + cwd: path.join(__dirname, "..") + }); - child.on("exit", (code) => { - Log.info(`Changes detected : ${code}`); - Log.info("Restarting server..."); - startServer(); + 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}`); + } + }); +} + +/** + * Restart the server process + * @param {string} reason The reason for the restart + */ +async function restartServer (reason) { + if (restartTimer) clearTimeout(restartTimer); + + restartTimer = setTimeout(async () => { + Log.info(reason); + + if (child) { + isRestarting = true; + + // Get the actual port being used + const port = getServerPort(); + + // Set up one-time listener for the exit event + child.once("exit", async () => { + // Wait until port is actually available + await waitForPort(port); + // Reset port cache in case config changed + serverPort = null; + startServer(); + }); + + child.kill("SIGTERM"); + } else { + startServer(); + } + }, RESTART_DELAY_MS); } /** * Watch a directory for changes and restart the server on change - * @param dir + * @param {string} dir The directory path to watch */ function watchDir (dir) { - fs.watch(dir, { recursive: true }, (_eventType, filename) => { - if (!filename) return; + try { + const watcher = fs.watch(dir, { recursive: true }, (_eventType, filename) => { + if (!filename) return; - if (dir.includes("modules") && !filename.endsWith("node_helper.js")) return; + // Ignore node_modules - too many changes during npm install + // After installing dependencies, manually restart the watcher + if (filename.includes("node_modules")) return; - if (restartTimer) clearTimeout(restartTimer); + // Only watch .js, .mjs and .cjs files + if (!filename.endsWith(".js") && !filename.endsWith(".mjs") && !filename.endsWith(".cjs")) return; - restartTimer = setTimeout(() => { - Log.info(`Changes detected in ${dir}: ${filename} — restarting...`); - if (child) child.kill("SIGTERM"); - }, 500); - }); + if (restartTimer) clearTimeout(restartTimer); + + restartTimer = setTimeout(() => { + restartServer(`Changes detected in ${dir}: ${filename} — restarting...`); + }, RESTART_DELAY_MS); + }); + + watcher.on("error", (error) => { + if (error.code === "ENOSPC") { + if (!watcherErrorLogged) { + watcherErrorLogged = true; + Log.error("System limit for file watchers reached. Try increasing: sudo sysctl fs.inotify.max_user_watches=524288"); + } + } else { + Log.error(`Watcher error for ${dir}:`, error.message); + } + }); + } catch (error) { + Log.error(`Failed to watch directory ${dir}:`, error.message); + } +} + +/** + * Watch a specific file for changes and restart the server on change + * @param {string} file The file path to watch + */ +function watchFile (file) { + try { + const watcher = fs.watch(file, (_eventType) => { + if (restartTimer) clearTimeout(restartTimer); + + restartTimer = setTimeout(() => { + restartServer(`Config file changed: ${path.basename(file)} — restarting...`); + }, RESTART_DELAY_MS); + }); + + watcher.on("error", (error) => { + Log.error(`Watcher error for ${file}:`, error.message); + }); + + Log.log(`Watching config file: ${file}`); + } catch (error) { + Log.error(`Failed to watch file ${file}:`, error.message); + } +} + +/** + * Get the config file path from environment or default location + * @returns {string} The config file path + */ +function getConfigFilePath () { + if (process.env.MM_CONFIG_FILE) { + return process.env.MM_CONFIG_FILE; + } + + if (global.configuration_file && global.root_path) { + return path.resolve(global.root_path, global.configuration_file); + } + + return path.join(__dirname, "..", "config", "config.js"); } startServer(); -watchDir(path.join(__dirname, "..", "config")); + +// Watch the config file (might be in custom location) +// Priority: MM_CONFIG_FILE env var, then global.configuration_file, then default +const configFile = getConfigFilePath(); +watchFile(configFile); + +// Watch core directories (modules, js and serveronly) +// We watch specific directories instead of the whole project root to avoid +// watching unnecessary files like node_modules (even though we filter it), +// tests, translations, css, fonts, vendor, etc. watchDir(path.join(__dirname, "..", "modules")); +watchDir(path.join(__dirname, "..", "js")); +watchDir(path.join(__dirname)); // serveronly process.on("SIGINT", () => { + isShuttingDown = true; if (restartTimer) clearTimeout(restartTimer); if (child) child.kill("SIGTERM"); process.exit(0); From 6b7433c112fc1059a3c628b4507e70b058b96592 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sun, 26 Oct 2025 05:20:51 +0100 Subject: [PATCH 11/17] feat(core): improve server watch mode with CSS support and client reload - allow `config.watchTargets` to opt into additional files while falling back to the active config and custom CSS - expose `/reload` in server.js that emits `RELOAD` so connected clients refresh automatically The watcher runs outside server.js and previously had no socket.io access, so browser reloads were impossible; the new endpoint bridges that gap. --- js/main.js | 14 +++- js/server.js | 7 ++ serveronly/watcher.js | 177 ++++++++++++++++++++++++++++++------------ 3 files changed, 148 insertions(+), 50 deletions(-) diff --git a/js/main.js b/js/main.js index feee330419..6f7f65f8b2 100644 --- a/js/main.js +++ b/js/main.js @@ -1,4 +1,4 @@ -/* global Loader, defaults, Translator, addAnimateCSS, removeAnimateCSS, AnimateCSSIn, AnimateCSSOut, modulePositions */ +/* global Loader, defaults, Translator, addAnimateCSS, removeAnimateCSS, AnimateCSSIn, AnimateCSSOut, modulePositions, io */ const MM = (function () { let modules = []; @@ -605,6 +605,18 @@ const MM = (function () { createDomObjects(); + // Setup global socket listener for RELOAD event (watch mode) + if (typeof io !== "undefined") { + const socket = io("/", { + path: `${config.basePath || "/"}socket.io` + }); + + socket.on("RELOAD", () => { + Log.warn("Reload notification received from server"); + window.location.reload(true); + }); + } + if (config.reloadAfterServerRestart) { setInterval(async () => { // if server startup time has changed (which means server was restarted) diff --git a/js/server.js b/js/server.js index 281d417385..f6105936f1 100644 --- a/js/server.js +++ b/js/server.js @@ -111,6 +111,13 @@ function Server (config) { app.get("/", (req, res) => getHtml(req, res)); + // Reload endpoint for watch mode - triggers browser reload + app.get("/reload", (req, res) => { + Log.info("Reload request received, notifying all clients"); + io.emit("RELOAD"); + res.status(200).send("OK"); + }); + server.on("listening", () => { resolve({ app, diff --git a/serveronly/watcher.js b/serveronly/watcher.js index d11438fa45..2daced80d8 100644 --- a/serveronly/watcher.js +++ b/serveronly/watcher.js @@ -2,6 +2,7 @@ const { spawn } = require("child_process"); const fs = require("fs"); const path = require("path"); const net = require("net"); +const http = require("http"); const Log = require("../js/logger"); const RESTART_DELAY_MS = 500; @@ -12,8 +13,8 @@ let child = null; let restartTimer = null; let isShuttingDown = false; let isRestarting = false; -let watcherErrorLogged = false; let serverPort = null; +const rootDir = path.join(__dirname, ".."); /** * Get the server port from config @@ -106,6 +107,32 @@ function startServer () { }); } +/** + * Send reload notification to all connected clients + */ +function notifyClientsToReload () { + const port = getServerPort(); + const options = { + hostname: "localhost", + 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 @@ -122,6 +149,9 @@ async function restartServer (reason) { // Get the actual port being used const port = getServerPort(); + // 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 @@ -138,55 +168,26 @@ async function restartServer (reason) { }, RESTART_DELAY_MS); } -/** - * Watch a directory for changes and restart the server on change - * @param {string} dir The directory path to watch - */ -function watchDir (dir) { - try { - const watcher = fs.watch(dir, { recursive: true }, (_eventType, filename) => { - if (!filename) return; - - // Ignore node_modules - too many changes during npm install - // After installing dependencies, manually restart the watcher - if (filename.includes("node_modules")) return; - - // Only watch .js, .mjs and .cjs files - if (!filename.endsWith(".js") && !filename.endsWith(".mjs") && !filename.endsWith(".cjs")) return; - - if (restartTimer) clearTimeout(restartTimer); - - restartTimer = setTimeout(() => { - restartServer(`Changes detected in ${dir}: ${filename} — restarting...`); - }, RESTART_DELAY_MS); - }); - - watcher.on("error", (error) => { - if (error.code === "ENOSPC") { - if (!watcherErrorLogged) { - watcherErrorLogged = true; - Log.error("System limit for file watchers reached. Try increasing: sudo sysctl fs.inotify.max_user_watches=524288"); - } - } else { - Log.error(`Watcher error for ${dir}:`, error.message); - } - }); - } catch (error) { - Log.error(`Failed to watch directory ${dir}:`, error.message); - } -} - /** * 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 watcher = fs.watch(file, (_eventType) => { + 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(() => { - restartServer(`Config file changed: ${path.basename(file)} — restarting...`); + Log.info(`[watchFile] Triggering restart due to change in: ${file}`); + restartServer(`File changed: ${path.basename(file)} — restarting...`); }, RESTART_DELAY_MS); }); @@ -194,7 +195,7 @@ function watchFile (file) { Log.error(`Watcher error for ${file}:`, error.message); }); - Log.log(`Watching config file: ${file}`); + Log.log(`Watching file: ${file}`); } catch (error) { Log.error(`Failed to watch file ${file}:`, error.message); } @@ -223,13 +224,91 @@ startServer(); const configFile = getConfigFilePath(); watchFile(configFile); -// Watch core directories (modules, js and serveronly) -// We watch specific directories instead of the whole project root to avoid -// watching unnecessary files like node_modules (even though we filter it), -// tests, translations, css, fonts, vendor, etc. -watchDir(path.join(__dirname, "..", "modules")); -watchDir(path.join(__dirname, "..", "js")); -watchDir(path.join(__dirname)); // serveronly +/** + * Resolve the active custom CSS path based on config or environment overrides + * @param {object} config The loaded MagicMirror config + * @returns {string} Absolute path to the CSS file + */ +function resolveCustomCssPath (config = {}) { + const cssFromEnv = process.env.MM_CUSTOMCSS_FILE; + let cssPath = cssFromEnv || config.customCss || "css/custom.css"; + + if (!cssPath || typeof cssPath !== "string") { + cssPath = "css/custom.css"; + } + + return path.isAbsolute(cssPath) ? cssPath : path.join(rootDir, cssPath); +} + +/** + * Determine fallback watch targets when no explicit watchTargets are provided + * @param {object} config The loaded MagicMirror config (may be partial) + * @returns {string[]} Array of absolute paths to watch + */ +function getFallbackWatchTargets (config = {}) { + const targets = new Set(); + if (configFile) { + targets.add(configFile); + } + + const cssPath = resolveCustomCssPath(config); + if (cssPath) { + targets.add(cssPath); + } + + return Array.from(targets); +} + +// 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() !== ""); + } else { + watchTargets = getFallbackWatchTargets(config); + Log.log("Watch targets not specified. Using active config and custom CSS as fallback."); + } + + 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}`); + } + } +} catch (err) { + // Config file might not exist or be invalid, use fallback targets + Log.warn("Could not load watchTargets from config, watching active config/custom CSS instead:", err.message); + + for (const target of getFallbackWatchTargets()) { + if (!fs.existsSync(target)) { + Log.warn(`Fallback watch target does not exist: ${target}`); + continue; + } + + watchFile(target); + Log.log(`Watching fallback file: ${target}`); + } +} process.on("SIGINT", () => { isShuttingDown = true; From b79bfccfb0e7378162db07d23b9ba679ab645fe3 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Mon, 27 Oct 2025 20:39:01 +0100 Subject: [PATCH 12/17] refactor(watcher): remove hardcoded config path and localhost IP - Use existing getConfigFilePath() in getServerPort() - Remove hardcoded 127.0.0.1 from isPortAvailable() for container compatibility --- serveronly/watcher.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/serveronly/watcher.js b/serveronly/watcher.js index 2daced80d8..4cde3a6dd6 100644 --- a/serveronly/watcher.js +++ b/serveronly/watcher.js @@ -25,7 +25,7 @@ function getServerPort () { try { // Try to read the config file to get the port - const configPath = path.join(__dirname, "..", "config", "config.js"); + const configPath = getConfigFilePath(); delete require.cache[require.resolve(configPath)]; const config = require(configPath); serverPort = global.mmPort || config.port || 8080; @@ -54,7 +54,7 @@ function isPortAvailable (port) { resolve(true); }); - server.listen(port, "127.0.0.1"); + server.listen(port); }); } From d8fe63ba598c0229fda88488f790542b11808bef Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Mon, 27 Oct 2025 20:58:32 +0100 Subject: [PATCH 13/17] refactor: centralize config file path resolution - Move getConfigFilePath() to server_functions.js - Use in app.js and watcher.js to avoid duplication --- js/app.js | 4 ++-- js/server_functions.js | 10 +++++++++- serveronly/watcher.js | 22 +++++----------------- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/js/app.js b/js/app.js index 5e5d3aee85..1641d68bcd 100644 --- a/js/app.js +++ b/js/app.js @@ -15,7 +15,7 @@ const Utils = require(`${__dirname}/utils`); const defaultModules = require(`${global.root_path}/modules/default/defaultmodules`); // used to control fetch timeout for node_helpers const { setGlobalDispatcher, Agent } = require("undici"); -const { getEnvVarsAsObj } = require("#server_functions"); +const { getEnvVarsAsObj, getConfigFilePath } = require("#server_functions"); // common timeout value, provide environment override in case const fetch_timeout = process.env.mmFetchTimeout !== undefined ? process.env.mmFetchTimeout : 30000; @@ -72,7 +72,7 @@ function App () { // For this check proposed to TestSuite // https://forum.magicmirror.builders/topic/1456/test-suite-for-magicmirror/8 - const configFilename = path.resolve(global.configuration_file || `${global.root_path}/config/config.js`); + const configFilename = getConfigFilePath(); let templateFile = `${configFilename}.template`; // check if templateFile exists diff --git a/js/server_functions.js b/js/server_functions.js index 1f206ccd88..fac8a23b8f 100644 --- a/js/server_functions.js +++ b/js/server_functions.js @@ -176,4 +176,12 @@ function getEnvVars (req, res) { res.send(obj); } -module.exports = { cors, getConfig, getHtml, getVersion, getStartup, getEnvVars, getEnvVarsAsObj, getUserAgent }; +/** + * Get the config file path from environment or default location + * @returns {string} The absolute config file path + */ +function getConfigFilePath () { + return path.resolve(global.configuration_file || `${global.root_path}/config/config.js`); +} + +module.exports = { cors, getConfig, getHtml, getVersion, getStartup, getEnvVars, getEnvVarsAsObj, getUserAgent, getConfigFilePath }; diff --git a/serveronly/watcher.js b/serveronly/watcher.js index 4cde3a6dd6..d2dc1bcb17 100644 --- a/serveronly/watcher.js +++ b/serveronly/watcher.js @@ -1,9 +1,13 @@ +// Load lightweight internal alias resolver to enable require("logger") +require("../js/alias-resolver"); + const { spawn } = require("child_process"); const fs = require("fs"); const path = require("path"); const net = require("net"); const http = require("http"); -const Log = require("../js/logger"); +const Log = require("logger"); +const { getConfigFilePath } = require("#server_functions"); const RESTART_DELAY_MS = 500; const PORT_CHECK_MAX_ATTEMPTS = 20; @@ -201,22 +205,6 @@ function watchFile (file) { } } -/** - * Get the config file path from environment or default location - * @returns {string} The config file path - */ -function getConfigFilePath () { - if (process.env.MM_CONFIG_FILE) { - return process.env.MM_CONFIG_FILE; - } - - if (global.configuration_file && global.root_path) { - return path.resolve(global.root_path, global.configuration_file); - } - - return path.join(__dirname, "..", "config", "config.js"); -} - startServer(); // Watch the config file (might be in custom location) From 5d902793794c0b06020c1dc64703f7ff4a3ff7b9 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Mon, 27 Oct 2025 21:33:02 +0100 Subject: [PATCH 14/17] fix(watcher): bind port checks to config.address for container compatibility --- serveronly/watcher.js | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/serveronly/watcher.js b/serveronly/watcher.js index d2dc1bcb17..645cc88863 100644 --- a/serveronly/watcher.js +++ b/serveronly/watcher.js @@ -17,31 +17,33 @@ let child = null; let restartTimer = null; let isShuttingDown = false; let isRestarting = false; -let serverPort = null; +let serverConfig = null; const rootDir = path.join(__dirname, ".."); /** - * Get the server port from config - * @returns {number} The port number + * Get the server configuration (port and address) + * @returns {{port: number, address: string}} The server config */ -function getServerPort () { - if (serverPort) return serverPort; +function getServerConfig () { + if (serverConfig) return serverConfig; try { - // Try to read the config file to get the port const configPath = getConfigFilePath(); delete require.cache[require.resolve(configPath)]; const config = require(configPath); - serverPort = global.mmPort || config.port || 8080; + serverConfig = { + port: global.mmPort || config.port || 8080, + address: config.address || "localhost" + }; } catch (err) { - serverPort = 8080; + serverConfig = { port: 8080, address: "localhost" }; } - return serverPort; + return serverConfig; } /** - * Check if a port is available + * Check if a port is available on the configured address * @param {number} port The port to check * @returns {Promise} True if port is available */ @@ -58,7 +60,9 @@ function isPortAvailable (port) { resolve(true); }); - server.listen(port); + // Use the same address as the actual server will bind to + const { address } = getServerConfig(); + server.listen(port, address); }); } @@ -115,7 +119,7 @@ function startServer () { * Send reload notification to all connected clients */ function notifyClientsToReload () { - const port = getServerPort(); + const { port } = getServerConfig(); const options = { hostname: "localhost", port: port, @@ -151,7 +155,7 @@ async function restartServer (reason) { isRestarting = true; // Get the actual port being used - const port = getServerPort(); + const { port } = getServerConfig(); // Notify clients to reload before restart notifyClientsToReload(); @@ -160,8 +164,8 @@ async function restartServer (reason) { child.once("exit", async () => { // Wait until port is actually available await waitForPort(port); - // Reset port cache in case config changed - serverPort = null; + // Reset config cache in case it changed + serverConfig = null; startServer(); }); From f38be2d49830170864240b04af1d0ea5e08332a3 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Mon, 27 Oct 2025 22:20:31 +0100 Subject: [PATCH 15/17] fix(watcher): use config.address for reload notifications --- serveronly/watcher.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/serveronly/watcher.js b/serveronly/watcher.js index 645cc88863..66843d9b92 100644 --- a/serveronly/watcher.js +++ b/serveronly/watcher.js @@ -119,9 +119,9 @@ function startServer () { * Send reload notification to all connected clients */ function notifyClientsToReload () { - const { port } = getServerConfig(); + const { port, address } = getServerConfig(); const options = { - hostname: "localhost", + hostname: address, port: port, path: "/reload", method: "GET" From 6fff61bd75ae62c2bd02ccf61b9673b4da9f3e98 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Mon, 27 Oct 2025 23:00:12 +0100 Subject: [PATCH 16/17] docs: update changelog entry --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 811eeebd3e..a00059a445 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ planned for 2026-01-01 ### Added - [weather] feat: add configurable forecast date format option (#3918) -- [core] Add new `server:watch` script to run MagicMirror² server-only with automatic restarts when JS files in `modules`, `js`, `serveronly`, or the `config` files changes (#3920) +- [core] Add new `server:watch` script to run MagicMirror² server-only with automatic restarts when files (defined in `config.watchTargets`) change (#3920) ### Changed From 487d270b6e239fbf1d12f4e4e169d74ba77c62d0 Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Mon, 27 Oct 2025 23:43:37 +0100 Subject: [PATCH 17/17] refactor(watcher): simplify by removing fallback logic and making getConfigFilePath standalone-safe --- js/server_functions.js | 10 +++++++ serveronly/watcher.js | 59 ++++-------------------------------------- 2 files changed, 15 insertions(+), 54 deletions(-) diff --git a/js/server_functions.js b/js/server_functions.js index fac8a23b8f..6772c7a480 100644 --- a/js/server_functions.js +++ b/js/server_functions.js @@ -181,6 +181,16 @@ function getEnvVars (req, res) { * @returns {string} The absolute config file path */ function getConfigFilePath () { + // Ensure root_path is set (for standalone contexts like watcher) + if (!global.root_path) { + global.root_path = path.resolve(`${__dirname}/../`); + } + + // Check environment variable if global not set + if (!global.configuration_file && process.env.MM_CONFIG_FILE) { + global.configuration_file = process.env.MM_CONFIG_FILE; + } + return path.resolve(global.configuration_file || `${global.root_path}/config/config.js`); } diff --git a/serveronly/watcher.js b/serveronly/watcher.js index 66843d9b92..0f75a5fb49 100644 --- a/serveronly/watcher.js +++ b/serveronly/watcher.js @@ -211,46 +211,6 @@ function watchFile (file) { startServer(); -// Watch the config file (might be in custom location) -// Priority: MM_CONFIG_FILE env var, then global.configuration_file, then default -const configFile = getConfigFilePath(); -watchFile(configFile); - -/** - * Resolve the active custom CSS path based on config or environment overrides - * @param {object} config The loaded MagicMirror config - * @returns {string} Absolute path to the CSS file - */ -function resolveCustomCssPath (config = {}) { - const cssFromEnv = process.env.MM_CUSTOMCSS_FILE; - let cssPath = cssFromEnv || config.customCss || "css/custom.css"; - - if (!cssPath || typeof cssPath !== "string") { - cssPath = "css/custom.css"; - } - - return path.isAbsolute(cssPath) ? cssPath : path.join(rootDir, cssPath); -} - -/** - * Determine fallback watch targets when no explicit watchTargets are provided - * @param {object} config The loaded MagicMirror config (may be partial) - * @returns {string[]} Array of absolute paths to watch - */ -function getFallbackWatchTargets (config = {}) { - const targets = new Set(); - if (configFile) { - targets.add(configFile); - } - - const cssPath = resolveCustomCssPath(config); - if (cssPath) { - targets.add(cssPath); - } - - return Array.from(targets); -} - // Setup file watching based on config try { const configPath = getConfigFilePath(); @@ -260,9 +220,10 @@ try { let watchTargets = []; if (Array.isArray(config.watchTargets) && config.watchTargets.length > 0) { watchTargets = config.watchTargets.filter((target) => typeof target === "string" && target.trim() !== ""); - } else { - watchTargets = getFallbackWatchTargets(config); - Log.log("Watch targets not specified. Using active config and custom CSS as fallback."); + } + + 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)`); @@ -289,17 +250,7 @@ try { } } catch (err) { // Config file might not exist or be invalid, use fallback targets - Log.warn("Could not load watchTargets from config, watching active config/custom CSS instead:", err.message); - - for (const target of getFallbackWatchTargets()) { - if (!fs.existsSync(target)) { - Log.warn(`Fallback watch target does not exist: ${target}`); - continue; - } - - watchFile(target); - Log.log(`Watching fallback file: ${target}`); - } + Log.warn("Could not load watchTargets from config."); } process.on("SIGINT", () => {