diff --git a/README.md b/README.md index 5bccc0d..0f60f55 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ SquadJS relies on being able to access the Squad server log directory in order t
## **Configuring SquadJS** -SquadJS can be configured via a JSON configuration file. The default location for the config is [config.json](./config.json). This file has to be created manualy. +SquadJS can be configured via a JSON configuration file. The default location for the config is [config.json](./config.json). This file has to be created manually. `config.example.json` can be used as template for new configurations. You can maintain multiple configs using this naming scheme: `config..json`. This naming scheme is useful for multiple servers or [config merging](#merged-configurations). @@ -203,6 +203,9 @@ Early stages of SquadJS initialization, before the configuration has been read a ### Merged Configurations The configuration can be spread over multiple config files. This can be helpful if run multiple servers and want to ensure all servers run the same configuration. Or if you have a large configuration for a plugin (e.g. a lot of [ChatCommands](#chatcommands) ) that make your config unreadable. +Config files can be included by specifying their path in the `baseincludes` list your config. These files will be loaded in order and overwrite values from previously loaded configs. There is also a `overwrites` list that allows you to specify configs that should overwrite values in this config. Normal users should use the `baseincludes`. All relative paths have a small search order in which the file is first looked for, first in the directory relative to which the config that includes it, then inside the SquadJS directory. +This allows sharing of a 'config folder' that references configs relative to other configs in that folder and makes sharing configs easier. +
Example with multiple config files @@ -210,14 +213,15 @@ The configuration can be spread over multiple config files. This can be helpful In this example we have 2 servers running and want the same configuration on both servers. The only diffrences are each server uses a diffrent discord bot and server 1 has verbose logging for the RCON module enabled. -Server 1 will be started using `node index.js config.base.json config.server1.json`. -Server 2 will be started using `node index.js config.base.json config.server2.json`. +Server 1 will be started using `node index.js config.server1.json`. +Server 2 will be started using `node index.js config.server2.json`. -
config.server1.json ```json { + "baseincludes": ["./config.base.json"], "server": { "id": 1, "host": "localhost", @@ -244,6 +248,7 @@ Server 2 will be started using `node index.js config.base.json config.server2.js ```json { + "baseincludes": ["./config.base.json"], "server": { "id": 2, "host": "localhost", @@ -265,6 +270,7 @@ Server 2 will be started using `node index.js config.base.json config.server2.js ```json { + "baseincludes": ["./config.example.json"], "server": { "ftp": { "port": 21, @@ -309,6 +315,16 @@ Server 2 will be started using `node index.js config.base.json config.server2.js
---
+
+
+ Precise loading order of configs + + Configs are parsed recursively through their `baseincludes`, their own json, and finally their `overwrites` and stored sequentially to a list that contains the json from each config. So in the above example the resultant list of configs loaded from loading `config.server1.json` would be `config.example.json, config.base.json, config.server1.json`. These configs then overwrite the current configuration in the order that they are in this list. This means that if you have `base-includes = ["config.external.json", "config.example.json"]` set in `config.myconfig.json`, with `config.external.json` also including `config.example.json`, the resultant list would like: `config.example.json, config.external.json, config.example.json, config.myconfig.json` and be merged as such. + Due to this, the ordering of the included jsons was most likely an error here, and you would actually want `config.external.json` second and `config.example.json` first. + +
+ +Please remember to avoid circular dependencies and it is considered good practice to include `config.example.json` as the base for other configs. If a new property is added to the example config that is required by SquadJS due to an update, your configs will prevent SquadJS from running. If you have problems with this, you can view the merged config by forcing SquadJS to log everything to the console. See [here](#console-output-configuration) for more information on console output. diff --git a/config.example.json b/config.example.json index 62fce4a..adeb21e 100644 --- a/config.example.json +++ b/config.example.json @@ -1,4 +1,6 @@ { + "baseincludes": [], + "overwrites": [], "server": { "id": 1, "host": "xxx.xxx.xxx.xxx", diff --git a/index.js b/index.js index 92a1803..1c138c7 100644 --- a/index.js +++ b/index.js @@ -5,21 +5,20 @@ async function main() { await printLogo(); const config = process.env.config; - const args = process.argv.slice(2); - const configPaths = args.length ? args : ['./config.json']; + const configPath = process.argv.length > 2 ? process.argv[2] : null; - if (config && args.length) throw new Error('Cannot accept both a config and config paths.'); + if (config && configPath) throw new Error('Cannot accept both a config and config path.'); // create a SquadServer instance const server = config ? await SquadServerFactory.buildFromConfigString(config) - : await SquadServerFactory.buildFromConfigFiles(configPaths); + : await SquadServerFactory.buildFromConfigFile(configPath ? configPath : './config.json'); // watch the server - await server.watch(); + // await server.watch(); // now mount the plugins - await Promise.all(server.plugins.map(async (plugin) => await plugin.mount())); + // await Promise.all(server.plugins.map(async (plugin) => await plugin.mount())); } main(); diff --git a/squad-server/factory.js b/squad-server/factory.js index f7bcae6..5f9c46f 100644 --- a/squad-server/factory.js +++ b/squad-server/factory.js @@ -1,6 +1,6 @@ import fs from 'fs'; import path from 'path'; -import { fileURLToPath } from 'url'; +import {fileURLToPath} from 'url'; import Discord from 'discord.js'; import sequelize from 'sequelize'; @@ -149,9 +149,33 @@ export default class SquadServerFactory { throw new Error(`${type.connector} is an unsupported connector type.`); } - static parseConfig(configString) { + static parseConfig(configString, configfile = "", previousconfigfiles = null) { + configfile = path.resolve(configfile); + if (!previousconfigfiles) previousconfigfiles = [] + if (previousconfigfiles.indexOf(configfile) > -1){ + Logger.verbose('SquadServerFactory', 1, `Found circular config with ${previousconfigfiles.slice(1)}`); + throw new Error(`Config has circular dependency at ${configfile} in with previous configs ${previousconfigfiles}!`); + } + else previousconfigfiles.push(configfile) try { - return JSON.parse(configString); + const jsonconfig = JSON.parse(configString); + const beforeincludes = jsonconfig.baseincludes ? jsonconfig.baseincludes.flatMap((filename) => { + try { + return SquadServerFactory.parseConfig( + SquadServerFactory.readConfigFile( + filename, + path.dirname(configfile) + ), + filename, + previousconfigfiles + ); + } catch (err) { + throw new Error(`Config file ${filename} from ${configfile} did not load. ${err}`) + } + }) : []; + const overwrites = jsonconfig.overwrites ? jsonconfig.overwrites.flatMap((filename) => SquadServerFactory.parseConfig(SquadServerFactory.readConfigFile(filename), filename, previousconfigfiles)) : []; + const retconfig = [beforeincludes, jsonconfig, overwrites].flat(1); + return retconfig; } catch (err) { throw new Error(`Unable to parse config file. ${err}`); } @@ -162,23 +186,22 @@ export default class SquadServerFactory { return SquadServerFactory.buildFromConfig(SquadServerFactory.parseConfig(configString)); } - static readConfigFile(configPath = './config.json') { - configPath = path.resolve(__dirname, '../', configPath); - if (!fs.existsSync(configPath)) throw new Error(`Config file does not exist. ${configPath}`); + static readConfigFile(configPath = './config.json', dirlocation = null) { + const configLoc = dirlocation ? path.resolve(__dirname, '../', dirlocation, configPath) : path.resolve(__dirname, '../', configPath); + if (!fs.existsSync(configLoc)) throw new Error(`Config file does not exist. ${configLoc}`); - Logger.verbose('SquadServerFactory', 1, `Reading config file ${configPath}`); - return fs.readFileSync(configPath, 'utf8'); + Logger.verbose('SquadServerFactory', 1, `Reading config file ${configLoc}`); + return fs.readFileSync(configLoc, 'utf8'); } - static buildFromConfigFiles(configPaths) { - Logger.verbose('SquadServerFactory', 1, 'Reading config files...'); - Logger.verbose('SquadServerFactory', 4, JSON.stringify(configPaths)); + static buildFromConfigFile(configPath) { + Logger.verbose('SquadServerFactory', 1, 'Reading config file...'); + Logger.verbose('SquadServerFactory', 4, JSON.stringify(configPath)); + let configs = SquadServerFactory.parseConfig(SquadServerFactory.readConfigFile(configPath)); - const configs = configPaths.map((configPath) => - SquadServerFactory.parseConfig(SquadServerFactory.readConfigFile(configPath)) - ); - - return SquadServerFactory.buildFromConfig(ConfigTools.mergeConfigs({}, ...configs)); + const mergedconfig = ConfigTools.mergeConfigs({}, ...configs); + Logger.verbose('SquadServerFactory', 4, `Merged config: ${JSON.stringify(mergedconfig)}`) + return SquadServerFactory.buildFromConfig(mergedconfig); } static async buildConfig() {