Skip to content
Merged
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
5 changes: 5 additions & 0 deletions cspell.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@
"Lightspeed",
"loadingcircle",
"locationforecast",
"logg",
"lockstring",
"lstrip",
"Luciella",
Expand Down Expand Up @@ -285,6 +286,8 @@
"Teeuw",
"Teil",
"TESTMODE",
"testpass",
"testuser",
"thomasrockhu",
"thumbslider",
"timeformat",
Expand All @@ -307,6 +310,7 @@
"VEVENT",
"vgtu",
"Vitest",
"VCALENDAR",
"Voelt",
"Vorberechnung",
"vppencilsharpener",
Expand All @@ -326,6 +330,7 @@
"winddirection",
"windgusts",
"windspeed",
"WKST",
"Woolridge",
"worktree",
"Wsymb",
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import stylistic from "@stylistic/eslint-plugin";
import vitest from "@vitest/eslint-plugin";

export default defineConfig([
globalIgnores(["config/**", "modules/**/*", "js/positions.js"]),
globalIgnores(["config/**", "modules/**/*", "js/positions.js", "tests/configs/port_variable.js"]),
{
files: ["**/*.js"],
languageOptions: {
Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@
<script type="text/javascript" src="socket.io/socket.io.js"></script>
<script type="text/javascript" src="node_modules/nunjucks/browser/nunjucks.min.js"></script>
<script type="text/javascript" src="js/defaults.js"></script>
<script type="text/javascript" src="#CONFIG_FILE#"></script>
<script type="text/javascript" src="js/vendor.js"></script>
<script type="text/javascript" src="defaultmodules/defaultmodules.js"></script>
<script type="text/javascript" src="defaultmodules/utils.js"></script>
<script type="text/javascript" src="js/logger.js"></script>
<script type="text/javascript" src="translations/translations.js"></script>
<script type="text/javascript" src="js/translator.js"></script>
<script type="text/javascript" src="js/class.js"></script>
<script type="text/javascript" src="config/basepath.js"></script>
<script type="text/javascript" src="js/module.js"></script>
<script type="text/javascript" src="js/loader.js"></script>
<script type="text/javascript" src="js/socketclient.js"></script>
Expand Down
125 changes: 5 additions & 120 deletions js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ require("./alias-resolver");
const fs = require("node:fs");
const path = require("node:path");
const Spawn = require("node:child_process").spawn;
const envsub = require("envsub");
const Log = require("logger");

// global absolute root path
Expand All @@ -15,7 +14,7 @@ const Utils = require(`${__dirname}/utils`);

// used to control fetch timeout for node_helpers
const { setGlobalDispatcher, Agent } = require("undici");
const { getEnvVarsAsObj, getConfigFilePath } = require("#server_functions");
const { getEnvVarsAsObj } = require("#server_functions");
// common timeout value, provide environment override in case
const fetch_timeout = process.env.mmFetchTimeout !== undefined ? process.env.mmFetchTimeout : 30000;

Expand Down Expand Up @@ -59,122 +58,6 @@ function App () {
let defaultModules;
let env;

/**
* Loads the config file. Combines it with the defaults and returns the config
* @async
* @returns {Promise<object>} the loaded config or the defaults if something goes wrong
*/
async function loadConfig () {
Log.log("Loading config ...");
const defaults = require(`${__dirname}/defaults`);
if (global.mmTestMode) {
// if we are running in test mode
defaults.address = "0.0.0.0";
}

// For this check proposed to TestSuite
// https://forum.magicmirror.builders/topic/1456/test-suite-for-magicmirror/8
const configFilename = getConfigFilePath();
let templateFile = `${configFilename}.template`;

// check if templateFile exists
try {
fs.accessSync(templateFile, fs.constants.F_OK);
} catch (err) {
templateFile = null;
Log.log("config template file not exists, no envsubst");
}

if (templateFile) {
// save current config.js
try {
if (fs.existsSync(configFilename)) {
fs.copyFileSync(configFilename, `${configFilename}-old`);
}
} catch (err) {
Log.warn(`Could not copy ${configFilename}: ${err.message}`);
}

// check if config.env exists
const envFiles = [];
const configEnvFile = `${configFilename.substr(0, configFilename.lastIndexOf("."))}.env`;
try {
if (fs.existsSync(configEnvFile)) {
envFiles.push(configEnvFile);
}
} catch (err) {
Log.log(`${configEnvFile} does not exist. ${err.message}`);
}

let options = {
all: true,
diff: false,
envFiles: envFiles,
protect: false,
syntax: "default",
system: true
};

// envsubst variables in templateFile and create new config.js
// naming for envsub must be templateFile and outputFile
const outputFile = configFilename;
try {
await envsub({ templateFile, outputFile, options });
} catch (err) {
Log.error(`Could not envsubst variables: ${err.message}`);
}
}

require(`${global.root_path}/js/check_config.js`);

try {
fs.accessSync(configFilename, fs.constants.F_OK);
const c = require(configFilename);
if (Object.keys(c).length === 0) {
Log.error("WARNING! Config file appears empty, maybe missing module.exports last line?");
}
checkDeprecatedOptions(c);
return Object.assign(defaults, c);
} catch (e) {
if (e.code === "ENOENT") {
Log.error("WARNING! Could not find config file. Please create one. Starting with default configuration.");
} else if (e instanceof ReferenceError || e instanceof SyntaxError) {
Log.error(`WARNING! Could not validate config file. Starting with default configuration. Please correct syntax errors at or above this line: ${e.stack}`);
} else {
Log.error(`WARNING! Could not load config file. Starting with default configuration. Error found: ${e}`);
}
}

return defaults;
}

/**
* Checks the config for deprecated options and throws a warning in the logs
* if it encounters one option from the deprecated.js list
* @param {object} userConfig The user config
*/
function checkDeprecatedOptions (userConfig) {
const deprecated = require(`${global.root_path}/js/deprecated`);

// check for deprecated core options
const deprecatedOptions = deprecated.configs;
const usedDeprecated = deprecatedOptions.filter((option) => userConfig.hasOwnProperty(option));
if (usedDeprecated.length > 0) {
Log.warn(`WARNING! Your config is using deprecated option(s): ${usedDeprecated.join(", ")}. Check README and Documentation for more up-to-date ways of getting the same functionality.`);
}

// check for deprecated module options
for (const element of userConfig.modules) {
if (deprecated[element.module] !== undefined && element.config !== undefined) {
const deprecatedModuleOptions = deprecated[element.module];
const usedDeprecatedModuleOptions = deprecatedModuleOptions.filter((option) => element.config.hasOwnProperty(option));
if (usedDeprecatedModuleOptions.length > 0) {
Log.warn(`WARNING! Your config for module ${element.module} is using deprecated option(s): ${usedDeprecatedModuleOptions.join(", ")}. Check README and Documentation for more up-to-date ways of getting the same functionality.`);
}
}
}
}

/**
* Loads a specific module.
* @param {string} module The name of the module (including subpath).
Expand Down Expand Up @@ -289,7 +172,9 @@ function App () {
* @returns {Promise<object>} the config used
*/
this.start = async function () {
config = await loadConfig();
const configObj = Utils.loadConfig();
config = configObj.fullConf;
Utils.checkConfigFile(configObj);

global.defaultModulesDir = config.defaultModulesDir;
defaultModules = require(`${global.root_path}/${global.defaultModulesDir}/defaultmodules`);
Expand Down Expand Up @@ -331,7 +216,7 @@ function App () {

await loadModules(modules);

httpServer = new Server(config);
httpServer = new Server(configObj);
const { app, io } = await httpServer.open();
Log.log("Server started ...");

Expand Down
143 changes: 1 addition & 142 deletions js/check_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,154 +2,13 @@
require("./alias-resolver");

const path = require("node:path");
const fs = require("node:fs");
const { styleText } = require("node:util");
const Ajv = require("ajv");
const globals = require("globals");
const { Linter } = require("eslint");
const Log = require("logger");

const rootPath = path.resolve(`${__dirname}/../`);
const Utils = require(`${rootPath}/js/utils.js`);

const linter = new Linter({ configType: "flat" });
const ajv = new Ajv();

/**
* Returns a string with path of configuration file.
* Check if set by environment variable MM_CONFIG_FILE
* @returns {string} path and filename of the config file
*/
function getConfigFile () {
// FIXME: This function should be in core. Do you want refactor me ;) ?, be good!
return path.resolve(process.env.MM_CONFIG_FILE || `${rootPath}/config/config.js`);
}

/**
* Checks the config file using eslint.
*/
function checkConfigFile () {
const configFileName = getConfigFile();

// Check if file exists and is accessible
try {
fs.accessSync(configFileName, fs.constants.R_OK);
} catch (error) {
if (error.code === "ENOENT") {
Log.error(`File not found: ${configFileName}`);
} else if (error.code === "EACCES") {
Log.error(`No permission to read config file: ${configFileName}`);
} else {
Log.error(`Cannot access config file: ${configFileName}\n${error.message}`);
}
process.exit(1);
}

// Validate syntax of the configuration file.
Log.info(`Checking config file ${configFileName} ...`);

// I'm not sure if all ever is utf-8
const configFile = fs.readFileSync(configFileName, "utf-8");

const errors = linter.verify(
configFile,
{
languageOptions: {
ecmaVersion: "latest",
globals: {
...globals.browser,
...globals.node
}
},
rules: {
"no-sparse-arrays": "error",
"no-undef": "error"
}
},
configFileName
);

if (errors.length === 0) {
Log.info(styleText("green", "Your configuration file doesn't contain syntax errors :)"));
validateModulePositions(configFileName);
} else {
let errorMessage = "Your configuration file contains syntax errors :(";

for (const error of errors) {
errorMessage += `\nLine ${error.line} column ${error.column}: ${error.message}`;
}
Log.error(errorMessage);
process.exit(1);
}
}

/**
*
* @param {string} configFileName - The path and filename of the configuration file to validate.
*/
function validateModulePositions (configFileName) {
Log.info("Checking modules structure configuration ...");

const positionList = Utils.getModulePositions();

// Make Ajv schema configuration of modules config
// Only scan "module" and "position"
const schema = {
type: "object",
properties: {
modules: {
type: "array",
items: {
type: "object",
properties: {
module: {
type: "string"
},
position: {
type: "string"
}
},
required: ["module"]
}
}
}
};

// Scan all modules
const validate = ajv.compile(schema);
const data = require(configFileName);

const valid = validate(data);
if (valid) {
Log.info(styleText("green", "Your modules structure configuration doesn't contain errors :)"));

// Check for unknown positions (warning only, not an error)
if (data.modules) {
for (const [index, module] of data.modules.entries()) {
if (module.position && !positionList.includes(module.position)) {
Log.warn(`Module ${index} ("${module.module}") uses unknown position: "${module.position}"`);
Log.warn(`Known positions are: ${positionList.join(", ")}`);
}
}
}
} else {
const module = validate.errors[0].instancePath.split("/")[2];
const position = validate.errors[0].instancePath.split("/")[3];
let errorMessage = "This module configuration contains errors:";
errorMessage += `\n${JSON.stringify(data.modules[module], null, 2)}`;
if (position) {
errorMessage += `\n${position}: ${validate.errors[0].message}`;
errorMessage += `\n${JSON.stringify(validate.errors[0].params.allowedValues, null, 2).slice(1, -1)}`;
} else {
errorMessage += validate.errors[0].message;
}
Log.error(errorMessage);
process.exit(1);
}
}

try {
checkConfigFile();
Utils.checkConfigFile();
} catch (error) {
const message = error && error.message ? error.message : error;
Log.error(`Unexpected error: ${message}`);
Expand Down
1 change: 1 addition & 0 deletions js/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const defaults = {
customCss: "config/custom.css",
foreignModulesDir: "modules",
defaultModulesDir: "defaultmodules",
hideConfigSecrets: false,
// httpHeaders used by helmet, see https://helmetjs.github.io/. You can add other/more object values by overriding this in config.js,
// e.g. you need to add `frameguard: false` for embedding MagicMirror in another website, see https://github.com/MagicMirrorOrg/MagicMirror/issues/2847
httpHeaders: { contentSecurityPolicy: false, crossOriginOpenerPolicy: false, crossOriginEmbedderPolicy: false, crossOriginResourcePolicy: false, originAgentCluster: false },
Expand Down
Loading