diff --git a/clientonly/index.js b/clientonly/index.js index b5b9fcbd17..7175d73661 100644 --- a/clientonly/index.js +++ b/clientonly/index.js @@ -12,17 +12,17 @@ const https = require("node:https"); * @param {string} defaultValue value if no key is given at the command line * @returns {string} the value of the parameter */ -function getCommandLineParameter (key, defaultValue = undefined) { +const getCommandLineParameter = (key, defaultValue = undefined) => { const index = process.argv.indexOf(`--${key}`); const value = index > -1 ? process.argv[index + 1] : undefined; return value !== undefined ? String(value) : defaultValue; -} +}; /** * Helper function to get server address/hostname from either the commandline or env * @returns {object} config object containing address, port, and tls properties */ -function getServerParameters () { +const getServerParameters = () => { const config = {}; // Prefer command line arguments over environment variables @@ -34,14 +34,14 @@ function getServerParameters () { config.tls = process.argv.includes("--use-tls"); return config; -} +}; /** * Gets the config from the specified server url * @param {string} url location where the server is running. * @returns {Promise} the config */ -function getServerConfig (url) { +const getServerConfig = (url) => { // Return new pending promise return new Promise((resolve, reject) => { // Select http or https module, depending on requested url @@ -67,21 +67,21 @@ function getServerConfig (url) { reject(new Error(`Unable to read config from server (${url}) (${error.message})`)); }); }); -} +}; /** * Print a message to the console in case of errors * @param {string} message error message to print * @param {number} code error code for the exit call */ -function fail (message, code = 1) { +const fail = (message, code = 1) => { if (message !== undefined && typeof message === "string") { console.error(message); } else { console.error("Usage: 'node clientonly --address 192.168.1.10 --port 8080 [--use-tls]'"); } process.exit(code); -} +}; /** * Starts the client by connecting to the server and launching the Electron application @@ -89,7 +89,7 @@ function fail (message, code = 1) { * @param {string} prefix http or https prefix * @async */ -async function startClient (config, prefix) { +const startClient = async (config, prefix) => { try { const serverUrl = `${prefix}${config.address}:${config.port}/config/`; console.log(`Client: Connecting to server at ${serverUrl}`); @@ -143,7 +143,7 @@ async function startClient (config, prefix) { } catch (reason) { fail(`Unable to connect to server: (${reason})`); } -} +}; // Main execution const config = getServerParameters(); diff --git a/defaultmodules/compliments/compliments.js b/defaultmodules/compliments/compliments.js index 7bee06eed3..9ce033c8b6 100644 --- a/defaultmodules/compliments/compliments.js +++ b/defaultmodules/compliments/compliments.js @@ -112,7 +112,7 @@ Module.register("compliments", { return 0; } - const generate = function () { + const generate = () => { return Math.floor(Math.random() * compliments.length); }; diff --git a/defaultmodules/newsfeed/newsfeed.js b/defaultmodules/newsfeed/newsfeed.js index 9a09a07406..2a8e6c89b2 100644 --- a/defaultmodules/newsfeed/newsfeed.js +++ b/defaultmodules/newsfeed/newsfeed.js @@ -261,7 +261,7 @@ Module.register("newsfeed", { } if (this.config.prohibitedWords.length > 0) { - newsItems = newsItems.filter(function (item) { + newsItems = newsItems.filter((item) => { for (const word of this.config.prohibitedWords) { if (item.title.toLowerCase().indexOf(word.toLowerCase()) > -1) { return false; diff --git a/defaultmodules/weather/provider-utils.js b/defaultmodules/weather/provider-utils.js index 5984926468..a7e4eccb5b 100644 --- a/defaultmodules/weather/provider-utils.js +++ b/defaultmodules/weather/provider-utils.js @@ -9,7 +9,7 @@ const SunCalc = require("suncalc"); * @param {string} weatherType - OpenWeatherMap icon code (e.g., "01d", "02n") * @returns {string|null} Internal weather type */ -function convertWeatherType (weatherType) { +const convertWeatherType = (weatherType) => { const weatherTypes = { "01d": "day-sunny", "02d": "day-cloudy", @@ -32,7 +32,7 @@ function convertWeatherType (weatherType) { }; return weatherTypes.hasOwnProperty(weatherType) ? weatherTypes[weatherType] : null; -} +}; /** * Apply timezone offset to a date @@ -40,10 +40,10 @@ function convertWeatherType (weatherType) { * @param {number} offsetMinutes - Timezone offset in minutes * @returns {Date} Date with applied offset */ -function applyTimezoneOffset (date, offsetMinutes) { +const applyTimezoneOffset = (date, offsetMinutes) => { const utcTime = date.getTime() + (date.getTimezoneOffset() * 60000); return new Date(utcTime + (offsetMinutes * 60000)); -} +}; /** * Limit decimal places for coordinates (truncate, not round) @@ -51,7 +51,7 @@ function applyTimezoneOffset (date, offsetMinutes) { * @param {number} decimals - Maximum number of decimal places * @returns {number} Value with limited decimal places */ -function limitDecimals (value, decimals) { +const limitDecimals = (value, decimals) => { const str = value.toString(); if (str.includes(".")) { const parts = str.split("."); @@ -60,7 +60,7 @@ function limitDecimals (value, decimals) { } } return value; -} +}; /** * Get sunrise and sunset times for a given date and location @@ -69,13 +69,13 @@ function limitDecimals (value, decimals) { * @param {number} lon - Longitude * @returns {object} Object with sunrise and sunset Date objects */ -function getSunTimes (date, lat, lon) { +const getSunTimes = (date, lat, lon) => { const sunTimes = SunCalc.getTimes(date, lat, lon); return { sunrise: sunTimes.sunrise, sunset: sunTimes.sunset }; -} +}; /** * Check if a given time is during daylight hours @@ -84,52 +84,52 @@ function getSunTimes (date, lat, lon) { * @param {Date} sunset - Sunset time * @returns {boolean} True if during daylight hours */ -function isDayTime (date, sunrise, sunset) { +const isDayTime = (date, sunrise, sunset) => { if (!sunrise || !sunset) { return true; // Default to day if times unavailable } return date >= sunrise && date < sunset; -} +}; /** * Format timezone offset as string (e.g., "+01:00", "-05:30") * @param {number} offsetMinutes - Timezone offset in minutes (use -new Date().getTimezoneOffset() for local) * @returns {string} Formatted offset string */ -function formatTimezoneOffset (offsetMinutes) { +const formatTimezoneOffset = (offsetMinutes) => { const hours = Math.floor(Math.abs(offsetMinutes) / 60); const minutes = Math.abs(offsetMinutes) % 60; const sign = offsetMinutes >= 0 ? "+" : "-"; return `${sign}${String(hours).padStart(2, "0")}:${String(minutes).padStart(2, "0")}`; -} +}; /** * Get date string in YYYY-MM-DD format (local time) * @param {Date} date - The date to format * @returns {string} Date string in YYYY-MM-DD format */ -function getDateString (date) { +const getDateString = (date) => { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`; -} +}; /** * Convert wind speed from km/h to m/s * @param {number} kmh - Wind speed in km/h * @returns {number} Wind speed in m/s */ -function convertKmhToMs (kmh) { +const convertKmhToMs = (kmh) => { return kmh / 3.6; -} +}; /** * Convert cardinal wind direction string to degrees * @param {string} direction - Cardinal direction (e.g., "N", "NNE", "SW") * @returns {number|null} Direction in degrees (0-360) or null if unknown */ -function cardinalToDegrees (direction) { +const cardinalToDegrees = (direction) => { const directions = { N: 0, NNE: 22.5, @@ -149,7 +149,7 @@ function cardinalToDegrees (direction) { NNW: 337.5 }; return directions[direction] ?? null; -} +}; /** * Validate and limit coordinate precision @@ -157,7 +157,7 @@ function cardinalToDegrees (direction) { * @param {number} maxDecimals - Maximum decimal places to preserve * @throws {Error} If coordinates are missing or invalid */ -function validateCoordinates (config, maxDecimals = 4) { +const validateCoordinates = (config, maxDecimals = 4) => { if (config.lat == null || config.lon == null || !Number.isFinite(config.lat) || !Number.isFinite(config.lon)) { throw new Error("Latitude and longitude are required"); @@ -165,7 +165,7 @@ function validateCoordinates (config, maxDecimals = 4) { config.lat = limitDecimals(config.lat, maxDecimals); config.lon = limitDecimals(config.lon, maxDecimals); -} +}; module.exports = { convertWeatherType, diff --git a/js/alias-resolver.js b/js/alias-resolver.js index 602e06677e..76213dc3b4 100644 --- a/js/alias-resolver.js +++ b/js/alias-resolver.js @@ -21,7 +21,7 @@ const resolved = Object.fromEntries( // Prevent multiple patching if this file is required more than once. if (!Module._mmAliasPatched) { const origResolveFilename = Module._resolveFilename; - Module._resolveFilename = function (request, parent, isMain, options) { + Module._resolveFilename = (request, parent, isMain, options) => { if (Object.prototype.hasOwnProperty.call(resolved, request)) { return resolved[request]; } diff --git a/js/animateCSS.js b/js/animateCSS.js index eacb51d3a8..2df8bb4fca 100644 --- a/js/animateCSS.js +++ b/js/animateCSS.js @@ -128,7 +128,7 @@ const AnimateCSSOut = [ * @param {string} [animation] animation name. * @param {number} [animationTime] animation duration. */ -function addAnimateCSS (element, animation, animationTime) { +const addAnimateCSS = (element, animation, animationTime) => { const animationName = `animate__${animation}`; const node = document.getElementById(element); if (!node) { @@ -138,14 +138,14 @@ function addAnimateCSS (element, animation, animationTime) { } node.style.setProperty("--animate-duration", `${animationTime}s`); node.classList.add("animate__animated", animationName); -} +}; /** * Remove an animation with Animate CSS * @param {string} [element] div element to animate. * @param {string} [animation] animation name. */ -function removeAnimateCSS (element, animation) { +const removeAnimateCSS = (element, animation) => { const animationName = `animate__${animation}`; const node = document.getElementById(element); if (!node) { @@ -155,5 +155,5 @@ function removeAnimateCSS (element, animation) { } node.classList.remove("animate__animated", animationName); node.style.removeProperty("--animate-duration"); -} +}; if (typeof window === "undefined") module.exports = { AnimateCSSIn, AnimateCSSOut, addAnimateCSS, removeAnimateCSS }; diff --git a/js/app.js b/js/app.js index 6fee30cc99..159201b3e9 100644 --- a/js/app.js +++ b/js/app.js @@ -68,7 +68,7 @@ function App () { * Loads a specific module. * @param {string} module The name of the module (including subpath). */ - function loadModule (module) { + const loadModule = (module) => { const elements = module.split("/"); const moduleName = elements[elements.length - 1]; let moduleFolder = path.resolve(`${global.root_path}/${env.modulesDir}`, module); @@ -130,14 +130,14 @@ function App () { m.loaded(); } - } + }; /** * Loads all modules. * @param {Module[]} modules All modules to be loaded * @returns {Promise} A promise that is resolved when all modules been loaded */ - async function loadModules (modules) { + const loadModules = async (modules) => { Log.log("Loading module helpers ..."); for (const module of modules) { @@ -145,7 +145,7 @@ function App () { } Log.log("All module helpers loaded."); - } + }; /** * Compare two semantic version numbers and return the difference. @@ -154,7 +154,7 @@ function App () { * @returns {number} A positive number if a is larger than b, a negative * number if a is smaller and 0 if they are the same */ - function cmpVersions (a, b) { + const cmpVersions = (a, b) => { let i, diff; const regExStrip0 = /(\.0+)+$/; const segmentsA = a.replace(regExStrip0, "").split("."); @@ -168,7 +168,7 @@ function App () { } } return segmentsA.length - segmentsB.length; - } + }; /** * Start the core app. @@ -177,7 +177,7 @@ function App () { * @async * @returns {Promise} the config used */ - this.start = async function () { + this.start = async () => { try { const configObj = Utils.loadConfig(); global.config = configObj.fullConf; @@ -277,7 +277,7 @@ function App () { * @returns {Promise} A promise that is resolved when all node_helpers and * the http server has been closed */ - this.stop = async function () { + this.stop = async () => { const nodePromises = []; for (const nodeHelper of nodeHelpers) { try { diff --git a/js/electron.js b/js/electron.js index 22e32d7495..bb6a61abe2 100644 --- a/js/electron.js +++ b/js/electron.js @@ -32,7 +32,7 @@ let mainWindow; /** * Create and show the main browser window. */ -function createWindow () { +const createWindow = () => { /* * see https://www.electronjs.org/docs/latest/api/screen @@ -143,7 +143,7 @@ function createWindow () { mainWindow.once("ready-to-show", () => { mainWindow.show(); }); -} +}; // Quit when all windows are closed. app.on("window-all-closed", () => { @@ -196,7 +196,7 @@ app.on("certificate-error", (event, webContents, url, error, certificate, callba /** * Bootstrap Electron: launch the client-only viewer and/or the full core application. */ -async function bootstrapElectron () { +const bootstrapElectron = async () => { if (process.env.clientonly) { await app.whenReady(); Log.log("Launching client viewer application."); @@ -213,6 +213,6 @@ async function bootstrapElectron () { Log.log("Launching application."); createWindow(); } -} +}; bootstrapElectron(); diff --git a/js/electron_helper.js b/js/electron_helper.js index 6eca6e9178..eeecd7ef9b 100644 --- a/js/electron_helper.js +++ b/js/electron_helper.js @@ -5,7 +5,7 @@ const Log = require("./logger"); * @param {object} commandLine Electron commandLine API * @param {Array} [electronSwitches] User-configured switches */ -function applyElectronSwitches (commandLine, electronSwitches) { +const applyElectronSwitches = (commandLine, electronSwitches) => { if (electronSwitches === undefined) return; if (!Array.isArray(electronSwitches)) { Log.error(`electronSwitches must be an array of strings or objects, got: ${JSON.stringify(electronSwitches)}`); @@ -25,6 +25,6 @@ function applyElectronSwitches (commandLine, electronSwitches) { Log.error(`Invalid electronSwitches entry: ${JSON.stringify(sw)}`); } } -} +}; module.exports = { applyElectronSwitches }; diff --git a/js/ip_access_control.js b/js/ip_access_control.js index e53b186e49..31f2d2a437 100644 --- a/js/ip_access_control.js +++ b/js/ip_access_control.js @@ -7,7 +7,7 @@ const Log = require("logger"); * @param {string[]} whitelist - Array of IP addresses or CIDR ranges * @returns {boolean} True if IP is allowed */ -function isAllowed (clientIp, whitelist) { +const isAllowed = (clientIp, whitelist) => { try { const addr = ipaddr.process(clientIp); @@ -31,7 +31,7 @@ function isAllowed (clientIp, whitelist) { Log.warn(`Failed to parse client IP: ${clientIp}`); return false; } -} +}; /** * Resolves a client IP for both Express and Socket.IO requests. @@ -40,7 +40,7 @@ function isAllowed (clientIp, whitelist) { * @param {object} req - Incoming request object (Express request or Socket.IO handshake request) * @returns {string} The resolved client IP address */ -function resolveClientIp (req) { +const resolveClientIp = (req) => { const directIp = req.socket?.remoteAddress || req.connection?.remoteAddress || req.ip; const LOOPBACK_WHITELIST = ["127.0.0.1", "::ffff:127.0.0.1", "::1"]; @@ -52,7 +52,7 @@ function resolveClientIp (req) { } return directIp; -} +}; /** * Checks whether a browser Origin matches the host serving the mirror. @@ -60,7 +60,7 @@ function resolveClientIp (req) { * @param {object} req - Incoming request object * @returns {boolean} True if the origin is same-host or absent */ -function isSameOrigin (req) { +const isSameOrigin = (req) => { const origin = req.headers?.origin; if (!origin) return true; @@ -72,7 +72,7 @@ function isSameOrigin (req) { } catch { return false; } -} +}; /** * Determines why a request is denied, or null if it is allowed. @@ -81,7 +81,7 @@ function isSameOrigin (req) { * @param {string[]} whitelist - Array of allowed IP addresses or CIDR ranges (empty = any IP) * @returns {string|null} A human-readable denial reason, or null when allowed */ -function accessDenialReason (req, whitelist) { +const accessDenialReason = (req, whitelist) => { // Strip control characters from the attacker-controlled Origin header before logging it if (!isSameOrigin(req)) return `Origin ${String(req.headers?.origin).replace(/[\r\n]/g, "")} is not allowed`; @@ -91,36 +91,36 @@ function accessDenialReason (req, whitelist) { } return null; -} +}; /** * Creates an Express middleware enforcing same-origin and the IP whitelist. * @param {string[]} whitelist - Array of allowed IP addresses or CIDR ranges * @returns {import("express").RequestHandler} Express middleware function */ -function ipAccessControl (whitelist) { - return function (req, res, next) { +const ipAccessControl = (whitelist) => { + return (req, res, next) => { const reason = accessDenialReason(req, whitelist); if (!reason) return next(); Log.warn(`${reason} to access the mirror`); res.status(403).send("This device is not allowed to access your mirror.
Please check your config.js or config.js.sample to change this."); }; -} +}; /** * Creates a Socket.IO `allowRequest` handler enforcing the same rules as the HTTP middleware. * @param {string[]} whitelist - Array of allowed IP addresses or CIDR ranges * @returns {(req: object, callback: (err: string | null, success: boolean) => void) => void} Socket.IO allowRequest handler */ -function socketIpAccessControl (whitelist) { - return function (req, callback) { +const socketIpAccessControl = (whitelist) => { + return (req, callback) => { const reason = accessDenialReason(req, whitelist); if (!reason) return callback(null, true); Log.warn(`${reason} to connect to the mirror socket`); callback("This device is not allowed to access your mirror.", false); }; -} +}; module.exports = { ipAccessControl, socketIpAccessControl }; diff --git a/js/loader.js b/js/loader.js index 9becc9ced9..de667c945e 100644 --- a/js/loader.js +++ b/js/loader.js @@ -10,19 +10,19 @@ const moduleObjects = []; * Get environment variables from config. * @returns {object} Env vars with modulesDir and customCss paths from config. */ -function getEnvVarsFromConfig () { +const getEnvVarsFromConfig = () => { return { modulesDir: globalThis.config.foreignModulesDir || "modules", defaultModulesDir: globalThis.config.defaultModulesDir || "defaultmodules", customCss: globalThis.config.customCss || "config/custom.css" }; -} +}; /** * Retrieve object of env variables. * @returns {object} with key: values as assembled in js/server_functions.js */ -async function getEnvVars () { +const getEnvVars = async () => { // In test mode, skip server fetch and use config values directly if (typeof process !== "undefined" && process.env && process.env.mmTestMode === "true") { return getEnvVarsFromConfig(); @@ -37,12 +37,12 @@ async function getEnvVars () { Log.error("Unable to retrieve env configuration", error); return getEnvVarsFromConfig(); } -} +}; /** * Loops through all modules and requests start for every module. */ -async function startModules () { +const startModules = async () => { const modulePromises = []; for (const module of moduleObjects) { try { @@ -72,22 +72,22 @@ async function startModules () { thisModule.hide(); } } -} +}; /** * Retrieve list of all modules. * @returns {object[]} module data as configured in config */ -function getAllModules () { +const getAllModules = () => { const AllModules = globalThis.config.modules.filter((module) => (module.module !== undefined) && (MM.getAvailableModulePositions.indexOf(module.position) > -1 || typeof (module.position) === "undefined")); return AllModules; -} +}; /** * Generate array with module information including module paths. * @returns {object[]} Module information. */ -async function getModuleData () { +const getModuleData = async () => { const modules = getAllModules(); const moduleFiles = []; const envVars = await getEnvVars(); @@ -134,25 +134,25 @@ async function getModuleData () { }); return moduleFiles; -} +}; /** * Load modules via ajax request and create module objects. * @param {object} module Information about the module we want to load. * @returns {Promise} resolved when module is loaded */ -async function loadModule (module) { +const loadModule = async (module) => { const url = module.path + module.file; /** * @returns {Promise} */ - async function afterLoad () { + const afterLoad = async () => { const moduleObject = Module.create(module.name); if (moduleObject) { await bootstrapModule(module, moduleObject); } - } + }; if (loadedModuleFiles.indexOf(url) !== -1) { await afterLoad(); @@ -161,14 +161,14 @@ async function loadModule (module) { loadedModuleFiles.push(url); await afterLoad(); } -} +}; /** * Bootstrap modules by setting the module data and loading the scripts & styles. * @param {object} module Information about the module we want to load. * @param {Module} mObj Modules instance. */ -async function bootstrapModule (module, mObj) { +const bootstrapModule = async (module, mObj) => { Log.info(`Bootstrapping module: ${module.name}`); mObj.setData(module); @@ -182,14 +182,14 @@ async function bootstrapModule (module, mObj) { Log.log(`Translations loaded for: ${module.name}`); moduleObjects.push(mObj); -} +}; /** * Load a script or stylesheet by adding it to the dom. * @param {string} fileName Path of the file we want to load. * @returns {Promise} resolved when the file is loaded */ -function loadFile (fileName) { +const loadFile = (fileName) => { const extension = fileName.slice((Math.max(0, fileName.lastIndexOf(".")) || Infinity) + 1); let script, stylesheet; @@ -200,10 +200,10 @@ function loadFile (fileName) { script = document.createElement("script"); script.type = "text/javascript"; script.src = fileName; - script.onload = function () { + script.onload = () => { resolve(); }; - script.onerror = function () { + script.onerror = () => { Log.error("Error on loading script:", fileName); script.remove(); resolve(); @@ -216,10 +216,10 @@ function loadFile (fileName) { script = document.createElement("script"); script.type = "module"; script.src = fileName; - script.onload = function () { + script.onload = () => { resolve(); }; - script.onerror = function () { + script.onerror = () => { Log.error("Error on loading module script:", fileName); script.remove(); resolve(); @@ -234,10 +234,10 @@ function loadFile (fileName) { stylesheet.rel = "stylesheet"; stylesheet.type = "text/css"; stylesheet.href = fileName; - stylesheet.onload = function () { + stylesheet.onload = () => { resolve(); }; - stylesheet.onerror = function () { + stylesheet.onerror = () => { Log.error("Error on loading stylesheet:", fileName); stylesheet.remove(); resolve(); @@ -245,14 +245,14 @@ function loadFile (fileName) { document.getElementsByTagName("head")[0].appendChild(stylesheet); }); } -} +}; /* Public Methods */ /** * Load all modules as defined in the config. */ -export async function loadModules () { +export const loadModules = async () => { const moduleData = await getModuleData(); const envVars = await getEnvVars(); const customCss = envVars.customCss; @@ -269,7 +269,7 @@ export async function loadModules () { // Start all modules. await startModules(); -} +}; /** * Load a file (script or stylesheet). @@ -278,7 +278,7 @@ export async function loadModules () { * @param {Module} module The module that calls the loadFile function. * @returns {Promise} resolved when the file is loaded */ -export function loadFileForModule (fileName, module) { +export const loadFileForModule = (fileName, module) => { if (loadedFiles.indexOf(fileName.toLowerCase()) !== -1) { Log.log(`File already loaded: ${fileName}`); return Promise.resolve(); @@ -302,4 +302,4 @@ export function loadFileForModule (fileName, module) { // Load it based on the module path. loadedFiles.push(fileName.toLowerCase()); return loadFile(module.file(fileName)); -} +}; diff --git a/js/logger.js b/js/logger.js index 28d6db5227..4038258326 100644 --- a/js/logger.js +++ b/js/logger.js @@ -1,5 +1,62 @@ // Logger for MagicMirror² — works both in Node.js (CommonJS) and the browser (global). -(function () { +(() => { + + /** + * Creates the logger object. Logging is disabled when running in test mode + * (Node.js) or inside jsdom (browser). + * @returns {object} The logger object with log level methods. + */ + const makeLogger = () => { + const enableLog = typeof module !== "undefined" + ? process.env.mmTestMode !== "true" + : typeof window === "object" && window.name !== "jsdom"; + + let logLevel; + + if (enableLog) { + logLevel = { + debug: console.debug.bind(console), + log: console.log.bind(console), + info: console.info.bind(console), + warn: console.warn.bind(console), + error: console.error.bind(console), + group: console.group.bind(console), + groupCollapsed: console.groupCollapsed.bind(console), + groupEnd: console.groupEnd.bind(console), + time: console.time.bind(console), + timeEnd: console.timeEnd.bind(console), + timeStamp: console.timeStamp.bind(console) + }; + + // Only these methods are affected by setLogLevel. + // Utility methods (group, time, etc.) are always active. + logLevel.setLogLevel = (newLevel) => { + for (const key of ["debug", "log", "info", "warn", "error"]) { + const disabled = newLevel && !newLevel.includes(key.toUpperCase()); + logLevel[key] = disabled ? () => {} : console[key].bind(console); + } + }; + } else { + logLevel = { + debug () {}, + log () {}, + info () {}, + warn () {}, + error () {}, + group () {}, + groupCollapsed () {}, + groupEnd () {}, + time () {}, + timeEnd () {}, + timeStamp () {} + }; + + logLevel.setLogLevel = () => {}; + } + + return logLevel; + }; + if (typeof module !== "undefined") { if (process.env.mmTestMode !== "true") { const { styleText } = require("node:util"); @@ -55,60 +112,4 @@ // Browser globals window.Log = makeLogger(); } - - /** - * Creates the logger object. Logging is disabled when running in test mode - * (Node.js) or inside jsdom (browser). - * @returns {object} The logger object with log level methods. - */ - function makeLogger () { - const enableLog = typeof module !== "undefined" - ? process.env.mmTestMode !== "true" - : typeof window === "object" && window.name !== "jsdom"; - - let logLevel; - - if (enableLog) { - logLevel = { - debug: console.debug.bind(console), - log: console.log.bind(console), - info: console.info.bind(console), - warn: console.warn.bind(console), - error: console.error.bind(console), - group: console.group.bind(console), - groupCollapsed: console.groupCollapsed.bind(console), - groupEnd: console.groupEnd.bind(console), - time: console.time.bind(console), - timeEnd: console.timeEnd.bind(console), - timeStamp: console.timeStamp.bind(console) - }; - - // Only these methods are affected by setLogLevel. - // Utility methods (group, time, etc.) are always active. - logLevel.setLogLevel = function (newLevel) { - for (const key of ["debug", "log", "info", "warn", "error"]) { - const disabled = newLevel && !newLevel.includes(key.toUpperCase()); - logLevel[key] = disabled ? function () {} : console[key].bind(console); - } - }; - } else { - logLevel = { - debug () {}, - log () {}, - info () {}, - warn () {}, - error () {}, - group () {}, - groupCollapsed () {}, - groupEnd () {}, - time () {}, - timeEnd () {}, - timeStamp () {} - }; - - logLevel.setLogLevel = function () {}; - } - - return logLevel; - } -}()); +})(); diff --git a/js/main.js b/js/main.js index c9e31f8c35..658f050349 100644 --- a/js/main.js +++ b/js/main.js @@ -11,7 +11,7 @@ let modules = []; /** * Create dom objects for all modules that are configured for a specific position. */ -async function createDomObjects () { +const createDomObjects = async () => { const domCreationPromises = []; modules.forEach((module) => { @@ -64,7 +64,7 @@ async function createDomObjects () { } catch (error) { Log.error(error); } -} +}; /** * Create and render a module DOM, then notify the module. @@ -72,7 +72,7 @@ async function createDomObjects () { * @param {string|null} haveAnimateIn Optional animateIn animation name. * @returns {Promise} Resolved when module DOM is created. */ -async function createModuleDom (module, haveAnimateIn) { +const createModuleDom = async (module, haveAnimateIn) => { if (haveAnimateIn) { await _updateDom(module, { options: { speed: 1000, animate: { in: haveAnimateIn } } }, true); } else { @@ -80,14 +80,14 @@ async function createModuleDom (module, haveAnimateIn) { } _sendNotification("MODULE_DOM_CREATED", null, null, module); -} +}; /** * Select the wrapper dom object for a specific position. * @param {string} position The name of the position. * @returns {HTMLElement | void} the wrapper element */ -function selectWrapper (position) { +const selectWrapper = (position) => { const classes = position.replace("_", " "); const parentWrapper = document.getElementsByClassName(classes); if (parentWrapper.length > 0) { @@ -96,7 +96,7 @@ function selectWrapper (position) { return wrapper[0]; } } -} +}; /** * Send a notification to all modules. @@ -105,14 +105,14 @@ function selectWrapper (position) { * @param {Module} sender The module that sent the notification. * @param {Module} [sendTo] The (optional) module to send the notification to. */ -function _sendNotification (notification, payload, sender, sendTo) { +const _sendNotification = (notification, payload, sender, sendTo) => { for (const m in modules) { const module = modules[m]; if (module !== sender && (!sendTo || module === sendTo)) { module.notificationReceived(notification, payload, sender); } } -} +}; /** * Update the dom for a specific module. @@ -121,7 +121,7 @@ function _sendNotification (notification, payload, sender, sendTo) { * @param {boolean} [createAnimatedDom] for displaying only animateIn (used on first start of MagicMirror) * @returns {Promise} Resolved when the dom is fully updated. */ -async function _updateDom (module, updateOptions, createAnimatedDom = false) { +const _updateDom = async (module, updateOptions, createAnimatedDom = false) => { let speed = updateOptions; let animateOut = null; let animateIn = null; @@ -143,7 +143,7 @@ async function _updateDom (module, updateOptions, createAnimatedDom = false) { const newHeader = module.getHeader(); const newContent = await module.getDom(); await updateDomWithContent(module, speed, newHeader, newContent, animateOut, animateIn, createAnimatedDom); -} +}; /** * Update the dom with the specified content @@ -156,7 +156,7 @@ async function _updateDom (module, updateOptions, createAnimatedDom = false) { * @param {boolean} [createAnimatedDom] If true, apply content and trigger only animateIn (used on first start). * @returns {Promise} Resolved after the module DOM update is applied or hide/show transition is scheduled. */ -async function updateDomWithContent (module, speed, newHeader, newContent, animateOut, animateIn, createAnimatedDom = false) { +const updateDomWithContent = async (module, speed, newHeader, newContent, animateOut, animateIn, createAnimatedDom = false) => { if (module.hidden || !speed) { updateModuleContent(module, newHeader, newContent); return; @@ -180,7 +180,7 @@ async function updateDomWithContent (module, speed, newHeader, newContent, anima if (!module.hidden) { await new Promise((resolve) => _showModule(module, speed / 2, resolve, { animate: animateIn })); } -} +}; /** * Check if the content has changed. @@ -189,7 +189,7 @@ async function updateDomWithContent (module, speed, newHeader, newContent, anima * @param {HTMLElement} newContent The new content that is generated. * @returns {boolean} True if the module need an update, false otherwise */ -function moduleNeedsUpdate (module, newHeader, newContent) { +const moduleNeedsUpdate = (module, newHeader, newContent) => { const moduleWrapper = document.getElementById(module.identifier); if (moduleWrapper === null) { return false; @@ -209,7 +209,7 @@ function moduleNeedsUpdate (module, newHeader, newContent) { const contentNeedsUpdate = tempContentWrapper.innerHTML !== contentWrapper[0].innerHTML; return headerNeedsUpdate || contentNeedsUpdate; -} +}; /** * Update the content of a module on screen. @@ -217,7 +217,7 @@ function moduleNeedsUpdate (module, newHeader, newContent) { * @param {string} newHeader The new header that is generated. * @param {HTMLElement} newContent The new content that is generated. */ -function updateModuleContent (module, newHeader, newContent) { +const updateModuleContent = (module, newHeader, newContent) => { const moduleWrapper = document.getElementById(module.identifier); if (moduleWrapper === null) { return; @@ -234,7 +234,7 @@ function updateModuleContent (module, newHeader, newContent) { } else { headerWrapper[0].style.display = "none"; } -} +}; /** * Hide the module. @@ -243,7 +243,7 @@ function updateModuleContent (module, newHeader, newContent) { * @param {() => void} callback Called when the animation is done. * @param {object} [options] Optional settings for the hide method. */ -function _hideModule (module, speed, callback, options = {}) { +const _hideModule = (module, speed, callback, options = {}) => { // set lockString if set in options. if (options.lockString) { if (module.lockStrings.indexOf(options.lockString) === -1) { @@ -318,7 +318,7 @@ function _hideModule (module, speed, callback, options = {}) { callback(); } } -} +}; /** * Show the module. @@ -327,7 +327,7 @@ function _hideModule (module, speed, callback, options = {}) { * @param {() => void} callback Called when the animation is done. * @param {object} [options] Optional settings for the show method. */ -function _showModule (module, speed, callback, options = {}) { +const _showModule = (module, speed, callback, options = {}) => { // remove lockString if set in options. if (options.lockString) { const index = module.lockStrings.indexOf(options.lockString); @@ -416,7 +416,7 @@ function _showModule (module, speed, callback, options = {}) { callback(); } } -} +}; /** * Checks for all positions if it has visible content. @@ -429,7 +429,7 @@ function _showModule (module, speed, callback, options = {}) { * an ugly top margin. By using this function, the top bar will be hidden if the * update notification is not visible. */ -function updateWrapperStates () { +const updateWrapperStates = () => { modulePositions.forEach((position) => { const wrapper = selectWrapper(position); const moduleWrappers = wrapper.getElementsByClassName("module"); @@ -444,13 +444,13 @@ function updateWrapperStates () { // move container definitions to main CSS wrapper.className = showWrapper ? "container" : "container hidden"; }); -} +}; /** * Loads the core config from the server (already combined with the system defaults). * @returns {Promise} The loaded config. */ -async function loadConfig () { +const loadConfig = async () => { const basePath = globalThis.config?.basePath ?? "/"; const res = await fetch(new URL("config/", `${location.origin}${basePath}`)); if (!res.ok) { @@ -472,31 +472,31 @@ async function loadConfig () { }); globalThis.config = config; return config; -} +}; /** * Adds special selectors on a collection of modules. * @param {Module[]} modules Array of modules. */ -function setSelectionMethodsForModules (modules) { +const setSelectionMethodsForModules = (modules) => { /** * Filter modules with the specified classes. * @param {string|string[]} className one or multiple classnames (array or space divided). * @returns {Module[]} Filtered collection of modules. */ - function withClass (className) { + const withClass = (className) => { return modulesByClass(className, true); - } + }; /** * Filter modules without the specified classes. * @param {string|string[]} className one or multiple classnames (array or space divided). * @returns {Module[]} Filtered collection of modules. */ - function exceptWithClass (className) { + const exceptWithClass = (className) => { return modulesByClass(className, false); - } + }; /** * Filters a collection of modules based on classname(s). @@ -504,7 +504,7 @@ function setSelectionMethodsForModules (modules) { * @param {boolean} include if the filter should include or exclude the modules with the specific classes. * @returns {Module[]} Filtered collection of modules. */ - function modulesByClass (className, include) { + const modulesByClass = (className, include) => { let searchClasses = className; if (typeof className === "string") { searchClasses = className.split(" "); @@ -524,31 +524,31 @@ function setSelectionMethodsForModules (modules) { setSelectionMethodsForModules(newModules); return newModules; - } + }; /** * Removes a module instance from the collection. * @param {object} module The module instance to remove from the collection. * @returns {Module[]} Filtered collection of modules. */ - function exceptModule (module) { + const exceptModule = (module) => { const newModules = modules.filter((mod) => { return mod.identifier !== module.identifier; }); setSelectionMethodsForModules(newModules); return newModules; - } + }; /** * Walks thru a collection of modules and executes the callback with the module as an argument. * @param {module} callback The function to execute with the module as an argument. */ - function enumerate (callback) { + const enumerate = (callback) => { modules.map((module) => { callback(module); }); - } + }; if (typeof modules.withClass === "undefined") { Object.defineProperty(modules, "withClass", { value: withClass, enumerable: false }); @@ -562,7 +562,7 @@ function setSelectionMethodsForModules (modules) { if (typeof modules.enumerate === "undefined") { Object.defineProperty(modules, "enumerate", { value: enumerate, enumerable: false }); } -} +}; export const MM = { diff --git a/js/module.js b/js/module.js index 11bae13945..f45a458969 100644 --- a/js/module.js +++ b/js/module.js @@ -368,13 +368,13 @@ export class Module { * @param {object} [options] Optional settings for the hide method. */ hide (speed, callback, options = {}) { - let usedCallback = callback || function () {}; + let usedCallback = callback || (() => {}); let usedOptions = options; if (typeof callback === "object") { Log.error("Parameter mismatch in module.hide: callback is not an optional parameter!"); usedOptions = callback; - usedCallback = function () {}; + usedCallback = () => {}; } MM.hideModule( @@ -395,13 +395,13 @@ export class Module { * @param {object} [options] Optional settings for the show method. */ show (speed, callback, options) { - let usedCallback = callback || function () {}; + let usedCallback = callback || (() => {}); let usedOptions = options; if (typeof callback === "object") { Log.error("Parameter mismatch in module.show: callback is not an optional parameter!"); usedOptions = callback; - usedCallback = function () {}; + usedCallback = () => {}; } MM.showModule( @@ -428,7 +428,7 @@ globalThis.Module = Module; * @param {...object} sources Objects whose properties are merged into target. * @returns {object} The merged target object. */ -function configMerge (target, ...sources) { +const configMerge = (target, ...sources) => { const isPlainObject = (value) => value?.constructor === Object; for (const source of sources) { @@ -444,7 +444,7 @@ function configMerge (target, ...sources) { } return target; -} +}; Module.definitions = {}; @@ -495,7 +495,7 @@ Module.register = function (name, moduleDefinition) { * @returns {number} A positive number if a is larger than b, a negative * number if a is smaller and 0 if they are the same */ -export function cmpVersions (a, b) { +export const cmpVersions = (a, b) => { const regExStrip0 = /(\.0+)+$/; const segmentsA = a.replace(regExStrip0, "").split("."); const segmentsB = b.replace(regExStrip0, "").split("."); @@ -508,14 +508,14 @@ export function cmpVersions (a, b) { } } return segmentsA.length - segmentsB.length; -} +}; /** * Define the clone method for later use. Helper Method. * @param {object} obj Object to be cloned * @returns {object} the cloned object */ -export function cloneObject (obj) { +export const cloneObject = (obj) => { if (obj === null || typeof obj !== "object") { return obj; } @@ -548,4 +548,4 @@ export function cloneObject (obj) { } return temp; -} +}; diff --git a/js/node_helper.js b/js/node_helper.js index 6ff435bf07..a19d3d68a5 100644 --- a/js/node_helper.js +++ b/js/node_helper.js @@ -9,7 +9,7 @@ const { replaceSecretPlaceholder } = require("#server_functions"); * @param {string} moduleName - Name of the module. * @returns {Set} The secret names the module may restore. */ -function getAllowedSecrets (moduleName) { +const getAllowedSecrets = (moduleName) => { const modules = global.configRedacted?.modules || []; const moduleConfig = modules.find((m) => m.module === moduleName); const allowed = new Set(); @@ -20,7 +20,7 @@ function getAllowedSecrets (moduleName) { } } return allowed; -} +}; class NodeHelper { init () { diff --git a/js/server.js b/js/server.js index 7b3c428cc2..249df5f8d7 100644 --- a/js/server.js +++ b/js/server.js @@ -29,7 +29,7 @@ function Server (configObj) { * Opens the server for incoming connections * @returns {Promise} A promise that is resolved when the server listens to connections */ - this.open = function () { + this.open = () => { return new Promise((resolve) => { if (config.useHttps) { const options = { @@ -162,7 +162,7 @@ function Server (configObj) { * Closes the server and destroys all lingering connections to it. * @returns {Promise} A promise that resolves when server has successfully shut down */ - this.close = function () { + this.close = () => { return new Promise((resolve) => { for (const socket of serverSockets.values()) { socket.destroy(); diff --git a/js/server_functions.js b/js/server_functions.js index 1d04a2f384..cf7b0627b1 100644 --- a/js/server_functions.js +++ b/js/server_functions.js @@ -12,9 +12,9 @@ const startUp = new Date(); * @param {Request} req - the request * @param {Response} res - the result */ -function getStartup (req, res) { +const getStartup = (req, res) => { res.send(startUp); -} +}; /** * Replace `**SECRET_ABC**` placeholders with the value of `process.env.SECRET_ABC`. @@ -26,7 +26,7 @@ function getStartup (req, res) { * @param {Set} [allowedSecrets] - Secret names that may be restored. * @returns {string} The input with the allowed placeholders replaced. */ -function replaceSecretPlaceholder (input, allowedSecrets) { +const replaceSecretPlaceholder = (input, allowedSecrets) => { if (global.config.cors === "allowAll") { if (input.includes("**SECRET_")) { Log.error("Replacing secrets doesn't work with CORS `allowAll`, you need to set `cors` to `disabled` or `allowWhitelist` in `config.js`"); @@ -42,7 +42,7 @@ function replaceSecretPlaceholder (input, allowedSecrets) { // Load the real value from the environment. Fallback to placeholder if missing. return process.env[secretName] || placeholder; }); -} +}; /** * A method that forwards HTTP Get-methods to the internet to avoid CORS-errors. @@ -54,7 +54,7 @@ function replaceSecretPlaceholder (input, allowedSecrets) { * @param {Response} res - the result * @returns {Promise} A promise that resolves when the response is sent */ -async function cors (req, res) { +const cors = async (req, res) => { if (global.config.cors === "disabled") { Log.error("CORS is disabled, you need to enable it in `config.js` by setting `cors` to `allowAll` or `allowWhitelist`"); return res.status(403).json({ error: "CORS proxy is disabled" }); @@ -142,14 +142,14 @@ async function cors (req, res) { } res.status(500).json({ error: error.message }); } -} +}; /** * Gets headers and values to attach to the web request. * @param {string} url - The url containing the headers and values to send. * @returns {object} An object specifying name and value of the headers. */ -function getHeadersToSend (url) { +const getHeadersToSend = (url) => { const headersToSend = { "User-Agent": getUserAgent() }; const headersToSendMatch = new RegExp("sendheaders=(.+?)(&|$)", "g").exec(url); if (headersToSendMatch) { @@ -163,14 +163,14 @@ function getHeadersToSend (url) { } } return headersToSend; -} +}; /** * Gets the headers expected from the response. * @param {string} url - The url containing the expected headers from the response. * @returns {string[]} headers - The name of the expected headers. */ -function geExpectedReceivedHeaders (url) { +const geExpectedReceivedHeaders = (url) => { const expectedReceivedHeaders = ["Content-Type"]; const expectedReceivedHeadersMatch = new RegExp("expectedheaders=(.+?)(&|$)", "g").exec(url); if (expectedReceivedHeadersMatch) { @@ -180,35 +180,35 @@ function geExpectedReceivedHeaders (url) { } } return expectedReceivedHeaders; -} +}; /** * Gets the HTML to display the magic mirror. * @param {Request} req - the request * @param {Response} res - the result */ -function getHtml (req, res) { +const getHtml = (req, res) => { let html = fs.readFileSync(path.resolve(`${global.root_path}/index.html`), { encoding: "utf8" }); html = html.replace("#VERSION#", global.version); html = html.replace("#TESTMODE#", global.mmTestMode); res.send(html); -} +}; /** * Gets the MagicMirror version. * @param {Request} req - the request * @param {Response} res - the result */ -function getVersion (req, res) { +const getVersion = (req, res) => { res.send(global.version); -} +}; /** * Gets the preferred `User-Agent` * @returns {string} `User-Agent` to be used */ -function getUserAgent () { +const getUserAgent = () => { const defaultUserAgent = `Mozilla/5.0 (Node.js ${Number(process.version.match(/^v(\d+\.\d+)/)[1])}) MagicMirror/${global.version}`; if (typeof global.config === "undefined") { @@ -223,13 +223,13 @@ function getUserAgent () { default: return defaultUserAgent; } -} +}; /** * Gets environment variables needed in the browser. * @returns {object} environment variables key: values */ -function getEnvVarsAsObj () { +const getEnvVarsAsObj = () => { const obj = { modulesDir: `${global.config.foreignModulesDir}`, defaultModulesDir: `${global.config.defaultModulesDir}`, customCss: `${global.config.customCss}` }; if (process.env.MM_MODULES_DIR) { obj.modulesDir = process.env.MM_MODULES_DIR.replace(`${global.root_path}/`, ""); @@ -239,17 +239,17 @@ function getEnvVarsAsObj () { } return obj; -} +}; /** * Gets environment variables needed in the browser. * @param {Request} req - the request * @param {Response} res - the result */ -function getEnvVars (req, res) { +const getEnvVars = (req, res) => { const obj = getEnvVarsAsObj(); res.send(obj); -} +}; /** * Resolves the HTTP server port. The `MM_PORT` environment variable takes @@ -257,15 +257,15 @@ function getEnvVars (req, res) { * @param {object} [config] the configuration to read the port from (defaults to global.config) * @returns {number} the port the server should listen on */ -function getServerPort (config = global.config) { +const getServerPort = (config = global.config) => { return Number(process.env.MM_PORT || config?.port || 8080); -} +}; /** * Get the config file path from environment or default location * @returns {string} The absolute config file path */ -function getConfigFilePath () { +const getConfigFilePath = () => { // Ensure root_path is set (for standalone contexts like watcher) if (!global.root_path) { global.root_path = path.resolve(`${__dirname}/../`); @@ -277,6 +277,6 @@ function getConfigFilePath () { } return path.resolve(global.configuration_file || `${global.root_path}/config/config.js`); -} +}; module.exports = { cors, getHtml, getVersion, getStartup, getEnvVars, getEnvVarsAsObj, getUserAgent, getServerPort, getConfigFilePath, replaceSecretPlaceholder }; diff --git a/js/socketclient.js b/js/socketclient.js index 8e52b694c0..9691247d61 100644 --- a/js/socketclient.js +++ b/js/socketclient.js @@ -23,7 +23,7 @@ export const MMSocket = function (moduleName) { pingTimeout: 120000 // wait up to 2 mins for a pong }); - let notificationCallback = function () {}; + let notificationCallback = () => {}; const onevent = this.socket.onevent; this.socket.onevent = (packet) => { diff --git a/js/translator.js b/js/translator.js index cf0b3827af..94c90e1f75 100644 --- a/js/translator.js +++ b/js/translator.js @@ -1,13 +1,13 @@ /* global translations */ -export const Translator = (function () { +export const Translator = (() => { /** * Load a JSON file via fetch. * @param {string} file Path of the file we want to load. * @returns {Promise} the translations in the specified file */ - async function loadJSON (file) { + const loadJSON = async (file) => { const baseHref = document.baseURI; const url = new URL(file, baseHref); @@ -21,7 +21,7 @@ export const Translator = (function () { Log.error(`Loading json file =${file} failed`); return null; } - } + }; return { coreTranslations: {}, @@ -47,7 +47,7 @@ export const Translator = (function () { * @param {object} variables Variables for the placeholder * @returns {string} the template filled with the variables */ - function createStringFromTemplate (template, variables) { + const createStringFromTemplate = (template, variables) => { if (Object.prototype.toString.call(template) !== "[object String]") { return template; } @@ -58,7 +58,7 @@ export const Translator = (function () { return templateToUse.replace(new RegExp("{([^}]+)}", "g"), (_unused, varName) => { return varName in variables ? variables[varName] : `{${varName}}`; }); - } + }; if (this.translations[module.name] && key in this.translations[module.name]) { return createStringFromTemplate(this.translations[module.name][key], variables); @@ -124,4 +124,4 @@ export const Translator = (function () { } } }; -}()); +})(); diff --git a/serveronly/watcher.js b/serveronly/watcher.js index b65d6f9917..9cc6119da1 100644 --- a/serveronly/watcher.js +++ b/serveronly/watcher.js @@ -24,7 +24,7 @@ const rootDir = path.join(__dirname, ".."); * Get the server configuration (port and address) * @returns {{port: number, address: string}} The server config */ -function getServerConfig () { +const getServerConfig = () => { if (serverConfig) return serverConfig; try { @@ -40,14 +40,14 @@ function getServerConfig () { } return serverConfig; -} +}; /** * Check if a port is available on the configured address * @param {number} port The port to check * @returns {Promise} True if port is available */ -function isPortAvailable (port) { +const isPortAvailable = (port) => { return new Promise((resolve) => { const server = net.createServer(); @@ -64,7 +64,7 @@ function isPortAvailable (port) { const { address } = getServerConfig(); server.listen(port, address); }); -} +}; /** * Wait until port is available @@ -72,7 +72,7 @@ function isPortAvailable (port) { * @param {number} maxAttempts Maximum number of attempts * @returns {Promise} */ -async function waitForPort (port, maxAttempts = PORT_CHECK_MAX_ATTEMPTS) { +const waitForPort = async (port, maxAttempts = PORT_CHECK_MAX_ATTEMPTS) => { for (let i = 0; i < maxAttempts; i++) { if (await isPortAvailable(port)) { Log.info(`Port ${port} is now available`); @@ -81,12 +81,12 @@ async function waitForPort (port, maxAttempts = PORT_CHECK_MAX_ATTEMPTS) { 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 () { +const startServer = () => { // Start node directly instead of via npm to avoid process tree issues child = spawn("node", ["./serveronly"], { stdio: "inherit", @@ -113,12 +113,12 @@ function startServer () { Log.error(`Server exited unexpectedly with code ${code} and signal ${signal}`); } }); -} +}; /** * Send reload notification to all connected clients */ -function notifyClientsToReload () { +const notifyClientsToReload = () => { const { port, address } = getServerConfig(); const options = { hostname: address, @@ -139,13 +139,13 @@ function notifyClientsToReload () { }); req.end(); -} +}; /** * Restart the server process * @param {string} reason The reason for the restart */ -function restartServer (reason) { +const restartServer = (reason) => { if (restartTimer) clearTimeout(restartTimer); restartTimer = setTimeout(() => { @@ -174,14 +174,14 @@ function restartServer (reason) { 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) { +const watchFile = (file) => { try { const fileName = path.basename(file); const dirName = path.dirname(file); @@ -207,7 +207,7 @@ function watchFile (file) { } catch (error) { Log.error(`Failed to watch file ${file}:`, error.message); } -} +}; startServer(); diff --git a/tests/configs/config_functions.js b/tests/configs/config_functions.js index d4c9689736..d06bc71422 100644 --- a/tests/configs/config_functions.js +++ b/tests/configs/config_functions.js @@ -14,7 +14,7 @@ const config = require(`${process.cwd()}/tests/configs/default.js`).configFactor return value; } }, - roundToInt2: function (value) { + roundToInt2: (value) => { try { return Math.round(parseFloat(value)); } catch { diff --git a/tests/e2e/animateCSS_spec.js b/tests/e2e/animateCSS_spec.js index eed01f4926..ba92c07b1c 100644 --- a/tests/e2e/animateCSS_spec.js +++ b/tests/e2e/animateCSS_spec.js @@ -16,11 +16,11 @@ describe("AnimateCSS integration Test", () => { * Get the compliments container element (waits until available). * @returns {Promise} */ - async function getComplimentsElement () { + const getComplimentsElement = async () => { await helpers.getDocument(); page = helpers.getPage(); await expect(page.locator(".compliments")).toBeVisible(); - } + }; /** * Wait for an Animate.css class to appear and persist briefly. @@ -28,20 +28,20 @@ describe("AnimateCSS integration Test", () => { * @param {{timeout?: number}} [options] Poll timeout in ms (default 6000) * @returns {Promise} */ - async function waitForAnimationClass (cls, { timeout = 6000 } = {}) { + const waitForAnimationClass = async (cls, { timeout = 6000 } = {}) => { const locator = page.locator(`.compliments.animate__animated.${cls}`); await locator.waitFor({ state: "attached", timeout }); // small stability wait await new Promise((r) => setTimeout(r, 50)); await expect(locator).toBeAttached(); - } + }; /** * Assert that no Animate.css animation class is applied within a time window. * @param {number} [ms] Observation period in ms (default 2000) * @returns {Promise} */ - async function assertNoAnimationWithin (ms = 2000) { + const assertNoAnimationWithin = async (ms = 2000) => { const start = Date.now(); const locator = page.locator(".compliments.animate__animated"); while (Date.now() - start < ms) { @@ -51,7 +51,7 @@ describe("AnimateCSS integration Test", () => { } await new Promise((r) => setTimeout(r, 100)); } - } + }; /** * Run one animation test scenario. @@ -59,7 +59,7 @@ describe("AnimateCSS integration Test", () => { * @param {string} [animationOut] Expected animate-out name * @returns {Promise} Throws on assertion failure */ - async function runAnimationTest (animationIn, animationOut) { + const runAnimationTest = async (animationIn, animationOut) => { await getComplimentsElement(); if (!animationIn && !animationOut) { await assertNoAnimationWithin(2000); @@ -71,7 +71,7 @@ describe("AnimateCSS integration Test", () => { await new Promise((r) => setTimeout(r, 2100)); await waitForAnimationClass(`animate__${animationOut}`); } - } + }; afterEach(async () => { await helpers.stopApplication(); diff --git a/tests/e2e/helpers/global-setup.js b/tests/e2e/helpers/global-setup.js index 3f99721bc5..07f7aa91b7 100644 --- a/tests/e2e/helpers/global-setup.js +++ b/tests/e2e/helpers/global-setup.js @@ -27,7 +27,7 @@ let page; * Ensure Playwright browser and context are available. * @returns {Promise} */ -async function ensureContext () { +const ensureContext = async () => { if (!browser) { // Additional args for CI stability to prevent crashes const launchOptions = { @@ -45,14 +45,14 @@ async function ensureContext () { if (!context) { context = await browser.newContext(); } -} +}; /** * Open a fresh page pointing to the provided url. * @param {string} url target url * @returns {Promise} initialized page instance */ -async function openPage (url) { +const openPage = async (url) => { await ensureContext(); if (page) { await page.close(); @@ -60,13 +60,13 @@ async function openPage (url) { page = await context.newPage(); await page.goto(url, { waitUntil: "load" }); return page; -} +}; /** * Close page, context and browser if they exist. * @returns {Promise} */ -async function closeBrowser () { +const closeBrowser = async () => { if (page) { await page.close(); page = null; @@ -79,7 +79,7 @@ async function closeBrowser () { await browser.close(); browser = null; } -} +}; exports.getPage = () => { if (!page) { diff --git a/tests/e2e/helpers/weather-functions.js b/tests/e2e/helpers/weather-functions.js index 8eb0c0699e..713a4e0636 100644 --- a/tests/e2e/helpers/weather-functions.js +++ b/tests/e2e/helpers/weather-functions.js @@ -9,7 +9,7 @@ const helpers = require("./global-setup"); * @param {object} page - Playwright page * @param {string} mockDataFile - Filename of mock data */ -async function injectMockWeatherData (page, mockDataFile) { +const injectMockWeatherData = async (page, mockDataFile) => { const rawData = JSON.parse(fs.readFileSync(path.resolve(__dirname, "../../mocks", mockDataFile)).toString()); // Validate that the fixture has at least one expected weather data type @@ -84,7 +84,7 @@ async function injectMockWeatherData (page, mockDataFile) { }); } }, { type, data }); -} +}; exports.startApplication = async (configFileName, mockDataFile) => { await helpers.startApplication(configFileName); diff --git a/tests/e2e/modules/newsfeed_spec.js b/tests/e2e/modules/newsfeed_spec.js index 88a6205bfd..615598e531 100644 --- a/tests/e2e/modules/newsfeed_spec.js +++ b/tests/e2e/modules/newsfeed_spec.js @@ -193,7 +193,7 @@ describe("Newsfeed module > Notifications", () => { const info = await page.evaluate(() => new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error("ARTICLE_INFO_RESPONSE timeout")), 3000); const origSend = MM.sendNotification.bind(MM); - MM.sendNotification = function (n, p, s) { + MM.sendNotification = (n, p, s) => { if (n === "ARTICLE_INFO_RESPONSE") { clearTimeout(timer); MM.sendNotification = origSend; diff --git a/tests/e2e/translations_spec.js b/tests/e2e/translations_spec.js index 93d9a477b6..3e16154312 100644 --- a/tests/e2e/translations_spec.js +++ b/tests/e2e/translations_spec.js @@ -14,12 +14,12 @@ const { * Create a fresh Translator state for each test. * @returns {Promise} Shared Translator singleton with cleared state */ -async function getFreshTranslator () { +const getFreshTranslator = async () => { setupTranslationTestEnvironment(3000); const { Translator } = await import(TRANSLATOR_MODULE_URL); resetTranslatorState(Translator); return Translator; -} +}; describe("translations", () => { let server; diff --git a/tests/electron/helpers/weather-setup.js b/tests/electron/helpers/weather-setup.js index 3bddaef1a1..05807f82e2 100644 --- a/tests/electron/helpers/weather-setup.js +++ b/tests/electron/helpers/weather-setup.js @@ -8,7 +8,7 @@ const helpers = require("./global-setup"); * This bypasses the weather provider and tests only client-side rendering * @param {string} mockDataFile - Filename of mock data in tests/mocks */ -async function injectMockWeatherData (mockDataFile) { +const injectMockWeatherData = async (mockDataFile) => { const rawData = JSON.parse(fs.readFileSync(path.resolve(__dirname, "../../mocks", mockDataFile)).toString()); const timezoneOffset = rawData.timezone_offset ? rawData.timezone_offset / 60 : 0; @@ -70,7 +70,7 @@ async function injectMockWeatherData (mockDataFile) { }); } }, { type, data }); -} +}; exports.getText = async (element, result) => { const elem = await helpers.getElement(element); diff --git a/tests/unit/classes/translator_spec.js b/tests/unit/classes/translator_spec.js index e50607d711..5a2f6f41d8 100644 --- a/tests/unit/classes/translator_spec.js +++ b/tests/unit/classes/translator_spec.js @@ -12,12 +12,12 @@ const { * Create a fresh Translator state for each test. * @returns {Promise} Shared Translator singleton with cleared state */ -async function getFreshTranslator () { +const getFreshTranslator = async () => { setupTranslationTestEnvironment(3001); const { Translator } = await import(TRANSLATOR_MODULE_URL); resetTranslatorState(Translator); return Translator; -} +}; describe("Translator", () => { let server; diff --git a/tests/unit/functions/ip_access_control_spec.js b/tests/unit/functions/ip_access_control_spec.js index 2a129fba71..0ac027e899 100644 --- a/tests/unit/functions/ip_access_control_spec.js +++ b/tests/unit/functions/ip_access_control_spec.js @@ -6,14 +6,12 @@ import { ipAccessControl, socketIpAccessControl } from "../../../js/ip_access_co * Creates a minimal Express-like response mock used by the middleware tests. * @returns {{ status: ReturnType, send: ReturnType }} Mock response object. */ -function createResponseMock () { - return { - status: vi.fn(function () { - return this; - }), - send: vi.fn() - }; -} +const createResponseMock = () => { + const res = {}; + res.status = vi.fn().mockReturnValue(res); + res.send = vi.fn().mockReturnValue(res); + return res; +}; describe("ip_access_control", () => { describe("ipAccessControl", () => { diff --git a/tests/unit/functions/update_helper_spec.js b/tests/unit/functions/update_helper_spec.js index 7b7635f47a..f4eb7a742e 100644 --- a/tests/unit/functions/update_helper_spec.js +++ b/tests/unit/functions/update_helper_spec.js @@ -29,24 +29,24 @@ describe("UpdateHelper", () => { * @param {string} moduleName - Name of the module directory to create. * @returns {{ root: string, modulePath: string }} Created paths. */ - function createTempModuleRoot (moduleName) { + const createTempModuleRoot = (moduleName) => { const root = mkdtempSync(join(tmpdir(), "mm-updater-")); const modulePath = join(root, "modules", moduleName); mkdirSync(modulePath, { recursive: true }); tempRoots.push(root); return { root, modulePath }; - } + }; /** * Creates a fresh UpdateHelper instance for testing. * @param {object} config - Optional config overrides. * @returns {Promise} Resolved UpdateHelper instance. */ - async function createUpdater (config = {}) { + const createUpdater = async (config = {}) => { const updateHelperModule = await import("../../../defaultmodules/updatenotification/update_helper"); const UpdateHelper = updateHelperModule.default || updateHelperModule; return new UpdateHelper({ updates: [], updateTimeout: 1000, updateAutorestart: false, ...config }); - } + }; it("marks update as requiring manual restart when autoRestart is disabled", async () => { const moduleName = "MMM-Test"; diff --git a/tests/unit/functions/updatenotification_spec.js b/tests/unit/functions/updatenotification_spec.js index f72e7ccc63..fb404ad0df 100644 --- a/tests/unit/functions/updatenotification_spec.js +++ b/tests/unit/functions/updatenotification_spec.js @@ -7,7 +7,7 @@ import { vi, describe, beforeEach, afterEach, it, expect } from "vitest"; * @param {{ current: import("vitest").MockInstance | null }} execGitSpyRef reference to the execGit spy. * @returns {Promise} resolved GitHelper instance. */ -async function createGitHelper (fsStatSyncMockRef, loggerMockRef, execGitSpyRef) { +const createGitHelper = async (fsStatSyncMockRef, loggerMockRef, execGitSpyRef) => { vi.resetModules(); fsStatSyncMockRef.current = vi.fn(); @@ -26,7 +26,7 @@ async function createGitHelper (fsStatSyncMockRef, loggerMockRef, execGitSpyRef) execGitSpyRef.current = vi.spyOn(instance, "execGit"); instance.__loggerMock = loggerMockRef.current; return instance; -} +}; describe("Updatenotification", () => { const fsStatSyncMockRef = { current: null }; diff --git a/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js b/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js index 657c52eee0..a61017f41b 100644 --- a/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js +++ b/tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js @@ -527,7 +527,7 @@ END:VCALENDAR`); const yearConfig = { ...defaultConfig, maximumNumberOfDays: 365 }; - const buildEvent = function (rrule, dtstart = "20231002", dtend = "20231003") { + const buildEvent = (rrule, dtstart = "20231002", dtend = "20231003") => { return ical.parseICS(`BEGIN:VCALENDAR BEGIN:VEVENT DTSTART;VALUE=DATE:${dtstart} diff --git a/tests/unit/modules/default/weather/node_helper_spec.js b/tests/unit/modules/default/weather/node_helper_spec.js index 81a49b7305..b5d156e9a0 100644 --- a/tests/unit/modules/default/weather/node_helper_spec.js +++ b/tests/unit/modules/default/weather/node_helper_spec.js @@ -5,7 +5,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; * Creates a fresh weather node helper instance with isolated mocks. * @returns {Promise} The mocked weather node helper. */ -async function loadWeatherNodeHelper () { +const loadWeatherNodeHelper = async () => { vi.resetModules(); const loggerMock = { @@ -42,7 +42,7 @@ async function loadWeatherNodeHelper () { helper.sendSocketNotification = vi.fn(); return helper; -} +}; afterEach(() => { vi.resetAllMocks(); diff --git a/tests/unit/modules/default/weather/providers/buienradar_spec.js b/tests/unit/modules/default/weather/providers/buienradar_spec.js index 25366f7c8e..2f57958c4e 100644 --- a/tests/unit/modules/default/weather/providers/buienradar_spec.js +++ b/tests/unit/modules/default/weather/providers/buienradar_spec.js @@ -14,7 +14,7 @@ const BUIENRADAR_URL = "https://forecast.buienradar.nl/2.0/forecast/*"; * Builds a stable Buienradar mock payload for parsing tests. * @returns {object} Buienradar forecast response fixture. */ -function buildBuienradarResponse () { +const buildBuienradarResponse = () => { const today = "2100-01-01"; const tomorrow = "2100-01-02"; @@ -71,7 +71,7 @@ function buildBuienradarResponse () { } ] }; -} +}; let server; diff --git a/tests/unit/modules/default/weather/providers/ukmetofficedatahub_spec.js b/tests/unit/modules/default/weather/providers/ukmetofficedatahub_spec.js index 7bda9a6b09..397345980d 100644 --- a/tests/unit/modules/default/weather/providers/ukmetofficedatahub_spec.js +++ b/tests/unit/modules/default/weather/providers/ukmetofficedatahub_spec.js @@ -19,7 +19,7 @@ const UKMETOFFICE_DAILY_URL = "https://data.hub.api.metoffice.gov.uk/sitespecifi * @param {object} source Source UK Met Office daily mock payload. * @returns {object} Cloned payload with deterministic future dates in `timeSeries`. */ -function withFutureDailyTimes (source) { +const withFutureDailyTimes = (source) => { const clone = JSON.parse(JSON.stringify(source)); const today = new Date(); today.setUTCHours(0, 0, 0, 0); @@ -35,7 +35,7 @@ function withFutureDailyTimes (source) { }); return clone; -} +}; let server; diff --git a/tests/unit/modules/default/weather/providers/weatherapi_spec.js b/tests/unit/modules/default/weather/providers/weatherapi_spec.js index e53713cc65..5242d2f439 100644 --- a/tests/unit/modules/default/weather/providers/weatherapi_spec.js +++ b/tests/unit/modules/default/weather/providers/weatherapi_spec.js @@ -13,7 +13,7 @@ const WEATHER_API_URL = "https://api.weatherapi.com/v1/forecast.json*"; * Builds a stable WeatherAPI mock payload for current, daily, and hourly parsing tests. * @returns {object} WeatherAPI forecast response fixture. */ -function buildWeatherApiResponse () { +const buildWeatherApiResponse = () => { return { location: { name: "Toronto", @@ -127,7 +127,7 @@ function buildWeatherApiResponse () { ] } }; -} +}; let server; diff --git a/tests/utils/translation_test_environment.js b/tests/utils/translation_test_environment.js index 8cebb7c53c..e192f25c98 100644 --- a/tests/utils/translation_test_environment.js +++ b/tests/utils/translation_test_environment.js @@ -10,7 +10,7 @@ const TRANSLATOR_MODULE_URL = pathToFileURL(path.join(__dirname, "..", "..", "js * when additional translation stores are introduced. * @param {object} Translator The shared Translator module instance. */ -function resetTranslatorState (Translator) { +const resetTranslatorState = (Translator) => { for (const [key, value] of Object.entries(Translator)) { if (typeof value === "function") { continue; @@ -20,21 +20,21 @@ function resetTranslatorState (Translator) { Translator[key] = {}; } } -} +}; /** * Set up DOM globals used by translation tests. * @param {number} [port] Base URI port used to resolve relative translation paths. * @returns {void} */ -function setupTranslationTestEnvironment (port = 3000) { +const setupTranslationTestEnvironment = (port = 3000) => { const dom = new JSDOM("", { url: `http://localhost:${port}` }); global.document = dom.window.document; if (!global.Log) { global.Log = { log: vi.fn(), error: vi.fn() }; } -} +}; module.exports = { setupTranslationTestEnvironment,