Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion defaultmodules/calendar/calendar.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand Down
4 changes: 2 additions & 2 deletions defaultmodules/clock/clock.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 () {
Expand Down
4 changes: 2 additions & 2 deletions defaultmodules/newsfeed/newsfeed.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 "";
}
Expand Down Expand Up @@ -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;
Expand Down
10 changes: 5 additions & 5 deletions defaultmodules/weather/weather.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 0 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ export default defineConfig([
Log: "readonly",
MM: "readonly",
Module: "readonly",
config: "readonly",
moment: "readonly"
}
},
Expand Down
10 changes: 5 additions & 5 deletions js/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
};
}

Expand All @@ -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
Expand Down Expand Up @@ -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;
}

Expand Down
55 changes: 30 additions & 25 deletions js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -448,27 +448,30 @@ function updateWrapperStates () {

/**
* Loads the core config from the server (already combined with the system defaults).
* @returns {Promise<object>} 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(`Config request failed with status ${res.status}.`);
}

// The server tags functions as { __mmFunction: "<source>" } 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: "<source>" } 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;
}

/**
Expand Down Expand Up @@ -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();
},
Expand All @@ -595,20 +596,20 @@ 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", () => {
Log.warn("Reload notification received from server");
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) {
Expand All @@ -619,7 +620,7 @@ export const MM = {
} catch (err) {
Log.error(`MagicMirror not reachable: ${err}`);
}
}, config.checkServerInterval);
}, globalThis.config.checkServerInterval);
}
},

Expand Down Expand Up @@ -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);
}
2 changes: 1 addition & 1 deletion js/module.js
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
2 changes: 1 addition & 1 deletion js/node_helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion js/server_functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
2 changes: 1 addition & 1 deletion js/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
8 changes: 4 additions & 4 deletions tests/e2e/config_functions_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
16 changes: 8 additions & 8 deletions tests/e2e/config_variables_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
4 changes: 2 additions & 2 deletions tests/e2e/helpers/global-setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down