From 1643d1eef32440a7ff634be13bf82b43bfa5f81a Mon Sep 17 00:00:00 2001 From: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com> Date: Sat, 22 Aug 2026 00:28:36 +0200 Subject: [PATCH] refactor: use explicit global config --- defaultmodules/calendar/calendar.js | 2 +- defaultmodules/clock/clock.js | 4 +-- defaultmodules/newsfeed/newsfeed.js | 4 +-- defaultmodules/weather/weather.js | 10 +++--- eslint.config.mjs | 1 - js/loader.js | 10 +++--- js/main.js | 55 ++++++++++++++++------------- js/module.js | 2 +- js/node_helper.js | 2 +- js/server_functions.js | 2 +- js/utils.js | 2 +- tests/e2e/config_functions_spec.js | 8 ++--- tests/e2e/config_variables_spec.js | 16 ++++----- tests/e2e/helpers/global-setup.js | 4 +-- 14 files changed, 63 insertions(+), 59 deletions(-) diff --git a/defaultmodules/calendar/calendar.js b/defaultmodules/calendar/calendar.js index 62eba7553b..23fff4a7bd 100644 --- a/defaultmodules/calendar/calendar.js +++ b/defaultmodules/calendar/calendar.js @@ -105,7 +105,7 @@ Module.register("calendar", { } // Set locale. - moment.updateLocale(config.language, CalendarUtils.getLocaleSpecification(config.timeFormat)); + moment.updateLocale(globalThis.config.language, CalendarUtils.getLocaleSpecification(globalThis.config.timeFormat)); // clear data holder before start this.calendarData = {}; diff --git a/defaultmodules/clock/clock.js b/defaultmodules/clock/clock.js index c3f4e8541a..9731d3673a 100644 --- a/defaultmodules/clock/clock.js +++ b/defaultmodules/clock/clock.js @@ -5,7 +5,7 @@ Module.register("clock", { defaults: { displayType: "digital", // options: digital, analog, both - timeFormat: config.timeFormat, + timeFormat: globalThis.config.timeFormat, timezone: null, displaySeconds: true, @@ -85,7 +85,7 @@ Module.register("clock", { setTimeout(notificationTimer, delayCalculator(this.second)); // Set locale. - moment.locale(config.language); + moment.locale(globalThis.config.language); }, // Override dom generator. getDom () { diff --git a/defaultmodules/newsfeed/newsfeed.js b/defaultmodules/newsfeed/newsfeed.js index 2dc8616168..6228b286cc 100644 --- a/defaultmodules/newsfeed/newsfeed.js +++ b/defaultmodules/newsfeed/newsfeed.js @@ -39,7 +39,7 @@ Module.register("newsfeed", { getUrlPrefix (item) { if (item.useCorsProxy) { - return `${location.protocol}//${location.host}${config.basePath}cors?url=`; + return `${location.protocol}//${location.host}${globalThis.config.basePath}cors?url=`; } else { return ""; } @@ -68,7 +68,7 @@ Module.register("newsfeed", { Log.info(`Starting module: ${this.name}`); // Set locale. - moment.locale(config.language); + moment.locale(globalThis.config.language); this.newsItems = []; this.loaded = false; diff --git a/defaultmodules/weather/weather.js b/defaultmodules/weather/weather.js index 936e8037f8..febd1a5ea9 100644 --- a/defaultmodules/weather/weather.js +++ b/defaultmodules/weather/weather.js @@ -6,11 +6,11 @@ Module.register("weather", { weatherProvider: "openweathermap", roundTemp: false, type: "current", // current, forecast, daily (equivalent to forecast), hourly - lang: config.language, - units: config.units, - tempUnits: config.units, - windUnits: config.units, - timeFormat: config.timeFormat, + lang: globalThis.config.language, + units: globalThis.config.units, + tempUnits: globalThis.config.units, + windUnits: globalThis.config.units, + timeFormat: globalThis.config.timeFormat, updateInterval: 10 * 60 * 1000, // every 10 minutes animationSpeed: 1000, showFeelsLike: true, diff --git a/eslint.config.mjs b/eslint.config.mjs index cac0dffe3d..fd6a9e672f 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -41,7 +41,6 @@ export default defineConfig([ Log: "readonly", MM: "readonly", Module: "readonly", - config: "readonly", moment: "readonly" } }, diff --git a/js/loader.js b/js/loader.js index 02b92d27fb..69722888ce 100644 --- a/js/loader.js +++ b/js/loader.js @@ -12,9 +12,9 @@ const moduleObjects = []; */ function getEnvVarsFromConfig () { return { - modulesDir: config.foreignModulesDir || "modules", - defaultModulesDir: config.defaultModulesDir || "defaultmodules", - customCss: config.customCss || "config/custom.css" + modulesDir: globalThis.config.foreignModulesDir || "modules", + defaultModulesDir: globalThis.config.defaultModulesDir || "defaultmodules", + customCss: globalThis.config.customCss || "config/custom.css" }; } @@ -30,7 +30,7 @@ async function getEnvVars () { // In production, fetch env vars from server try { - const res = await fetch(new URL("env", `${location.origin}${config.basePath}`)); + const res = await fetch(new URL("env", `${location.origin}${globalThis.config.basePath}`)); return JSON.parse(await res.text()); } catch (error) { // Fallback to config values if server fetch fails @@ -79,7 +79,7 @@ async function startModules () { * @returns {object[]} module data as configured in config */ function getAllModules () { - const AllModules = config.modules.filter((module) => (module.module !== undefined) && (MM.getAvailableModulePositions.indexOf(module.position) > -1 || typeof (module.position) === "undefined")); + const AllModules = globalThis.config.modules.filter((module) => (module.module !== undefined) && (MM.getAvailableModulePositions.indexOf(module.position) > -1 || typeof (module.position) === "undefined")); return AllModules; } diff --git a/js/main.js b/js/main.js index 01419a2596..12b9298b06 100644 --- a/js/main.js +++ b/js/main.js @@ -448,27 +448,30 @@ function updateWrapperStates () { /** * Loads the core config from the server (already combined with the system defaults). + * @returns {Promise} The loaded config. */ async function loadConfig () { - try { - const res = await fetch(new URL("config/", `${location.origin}${config.basePath}`)); + const basePath = globalThis.config?.basePath ?? "/"; + const res = await fetch(new URL("config/", `${location.origin}${basePath}`)); + if (!res.ok) { + throw new Error(`Unable to retrieve config: server responded with HTTP ${res.status} ${res.statusText}.`); + } - // The server tags functions as { __mmFunction: "" } because - // JSON.stringify can't serialise live functions. This reviver turns - // those tagged objects back into callable functions. - config = JSON.parse(await res.text(), (key, value) => { - if (value && typeof value === "object" && typeof value.__mmFunction === "string") { - try { - return new Function(`return (${value.__mmFunction})`)(); - } catch { - Log.warn(`Failed to revive function for config key "${key}".`); - } + // The server tags functions as { __mmFunction: "" } because + // JSON.stringify can't serialise live functions. This reviver turns + // those tagged objects back into callable functions. + const config = JSON.parse(await res.text(), (key, value) => { + if (value && typeof value === "object" && typeof value.__mmFunction === "string") { + try { + return new Function(`return (${value.__mmFunction})`)(); + } catch { + Log.warn(`Failed to revive function for config key "${key}".`); } - return value; - }); - } catch (error) { - Log.error("Unable to retrieve config", error); - } + } + return value; + }); + globalThis.config = config; + return config; } /** @@ -570,10 +573,8 @@ export const MM = { */ async init () { Log.info("Initializing MagicMirror²."); - await loadConfig(); - + const config = await loadConfig(); Log.setLogLevel(config.logLevel); - await Translator.loadCoreTranslations(config.language); await loadModules(); }, @@ -595,7 +596,7 @@ export const MM = { // Setup global socket listener for RELOAD event (watch mode) const socket = io("/", { - path: `${config.basePath || "/"}socket.io` + path: `${globalThis.config.basePath || "/"}socket.io` }); socket.on("RELOAD", () => { @@ -603,12 +604,12 @@ export const MM = { window.location.reload(true); }); - if (config.reloadAfterServerRestart) { + if (globalThis.config.reloadAfterServerRestart) { setInterval(async () => { // if server startup time has changed (which means server was restarted) // the client reloads the mm page try { - const res = await fetch(`${location.protocol}//${location.host}${config.basePath}startup`); + const res = await fetch(`${location.protocol}//${location.host}${globalThis.config.basePath}startup`); const curr = await res.text(); if (startUp === "") startUp = curr; if (startUp !== curr) { @@ -619,7 +620,7 @@ export const MM = { } catch (err) { Log.error(`MagicMirror not reachable: ${err}`); } - }, config.checkServerInterval); + }, globalThis.config.checkServerInterval); } }, @@ -711,4 +712,8 @@ export const MM = { // Legacy global bridge for third-party modules that reference window.MM directly. if (!globalThis.MM) globalThis.MM = MM; -MM.init(); +try { + await MM.init(); +} catch (error) { + Log.error(error); +} diff --git a/js/module.js b/js/module.js index 8ce48b2b84..f82a716584 100644 --- a/js/module.js +++ b/js/module.js @@ -298,7 +298,7 @@ export class Module { */ async loadTranslations () { const translations = this.getTranslations() || {}; - const language = config.language.toLowerCase(); + const language = globalThis.config.language.toLowerCase(); const languages = Object.keys(translations); const fallbackLanguage = languages[0]; diff --git a/js/node_helper.js b/js/node_helper.js index 1883bd6b66..6ff435bf07 100644 --- a/js/node_helper.js +++ b/js/node_helper.js @@ -108,7 +108,7 @@ class NodeHelper { io.of(this.name).on("connection", (socket) => { // register catch all. socket.onAny((notification, payload) => { - if (config?.hideConfigSecrets && payload && typeof payload === "object") { + if (global.config?.hideConfigSecrets && payload && typeof payload === "object") { try { // Calculate exactly which secrets this module is allowed to receive const allowedSecrets = getAllowedSecrets(this.name); diff --git a/js/server_functions.js b/js/server_functions.js index 2648e3317e..1d04a2f384 100644 --- a/js/server_functions.js +++ b/js/server_functions.js @@ -71,7 +71,7 @@ async function cors (req, res) { } else { url = match[1]; if (typeof global.config !== "undefined") { - if (config.hideConfigSecrets) { + if (global.config.hideConfigSecrets) { url = replaceSecretPlaceholder(url); } } diff --git a/js/utils.js b/js/utils.js index e5363feccb..be0aeb7ef8 100644 --- a/js/utils.js +++ b/js/utils.js @@ -163,7 +163,7 @@ const loadConfig = () => { checkDeprecatedOptions(configObj.fullConf); try { - const cfg = `let config = { basePath: "${configObj.fullConf.basePath}"};`; + const cfg = `globalThis.config = { basePath: "${configObj.fullConf.basePath}"};`; fs.writeFileSync(`${global.root_path}/config/basepath.js`, cfg, "utf-8"); } catch (error) { Log.error(`Could not write config/basepath.js file: ${error.message}`); diff --git a/tests/e2e/config_functions_spec.js b/tests/e2e/config_functions_spec.js index b40361cdcb..6156fb0971 100644 --- a/tests/e2e/config_functions_spec.js +++ b/tests/e2e/config_functions_spec.js @@ -10,12 +10,12 @@ describe("config with module function", () => { }); it("config should resolve module functions", () => { - expect(config.modules[0].config.moduleFunctions.roundToInt1(13.3)).toBe(13); - expect(config.modules[0].config.moduleFunctions.roundToInt2(13.3)).toBe(13); + expect(global.config.modules[0].config.moduleFunctions.roundToInt1(13.3)).toBe(13); + expect(global.config.modules[0].config.moduleFunctions.roundToInt2(13.3)).toBe(13); }); it("config should not revive plain strings containing arrow or function keywords", () => { - expect(config.modules[0].config.stringWithArrow).toBe("a => b is not a function"); - expect(config.modules[0].config.stringWithFunction).toBe("this function keyword is just text"); + expect(global.config.modules[0].config.stringWithArrow).toBe("a => b is not a function"); + expect(global.config.modules[0].config.stringWithFunction).toBe("this function keyword is just text"); }); }); diff --git a/tests/e2e/config_variables_spec.js b/tests/e2e/config_variables_spec.js index ebf3ba1e59..00daeaf832 100644 --- a/tests/e2e/config_variables_spec.js +++ b/tests/e2e/config_variables_spec.js @@ -10,40 +10,40 @@ describe("config with variables and secrets", () => { }); it("config.language should be \"de\"", () => { - expect(config.language).toBe("de"); + expect(global.config.language).toBe("de"); }); it("config.loglevel should be [\"ERROR\", \"LOG\", \"WARN\", \"INFO\"]", () => { - expect(config.logLevel).toStrictEqual(["ERROR", "LOG", "WARN", "INFO"]); + expect(global.config.logLevel).toStrictEqual(["ERROR", "LOG", "WARN", "INFO"]); }); it("config.ipWhitelist should be [\"::ffff:127.0.0.1\", \"::1\", \"127.0.0.1\"]", () => { - expect(config.ipWhitelist).toStrictEqual(["::ffff:127.0.0.1", "::1", "127.0.0.1"]); + expect(global.config.ipWhitelist).toStrictEqual(["::ffff:127.0.0.1", "::1", "127.0.0.1"]); }); it("config.timeFormat should be 12", () => { - expect(config.timeFormat).toBe(12); // default is 24 + expect(global.config.timeFormat).toBe(12); // default is 24 }); it("/config endpoint should show redacted secrets", async () => { - const res = await fetch(`http://localhost:${config.port}/config`); + const res = await fetch(`http://localhost:${global.config.port}/config`); expect(res.status).toBe(200); const cfg = await res.json(); expect(cfg.ipWhitelist).toStrictEqual(["**SECRET_IP2**", "::**SECRET_IP3**", "**SECRET_IP1**"]); }); it("/config/config.env should deliver 404", async () => { - const res = await fetch(`http://localhost:${config.port}/config/config.env`); + const res = await fetch(`http://localhost:${global.config.port}/config/config.env`); expect(res.status).toBe(404); }); it("/config/config.js should deliver 404", async () => { - const res = await fetch(`http://localhost:${config.port}/config/config.js`); + const res = await fetch(`http://localhost:${global.config.port}/config/config.js`); expect(res.status).toBe(404); }); it("/config/basepath.js should deliver 200", async () => { - const res = await fetch(`http://localhost:${config.port}/config/basepath.js`); + const res = await fetch(`http://localhost:${global.config.port}/config/basepath.js`); expect(res.status).toBe(200); }); }); diff --git a/tests/e2e/helpers/global-setup.js b/tests/e2e/helpers/global-setup.js index 58a7c25f3e..3f99721bc5 100644 --- a/tests/e2e/helpers/global-setup.js +++ b/tests/e2e/helpers/global-setup.js @@ -149,8 +149,8 @@ exports.stopApplication = async (waitTime = 100) => { }; exports.getDocument = async () => { - const port = global.testPort || config.port || 8080; - const address = config.address === "0.0.0.0" ? "localhost" : config.address || "localhost"; + const port = global.testPort || global.config.port || 8080; + const address = global.config.address === "0.0.0.0" ? "localhost" : global.config.address || "localhost"; const url = `http://${address}:${port}`; await openPage(url);