Merge pull request #1820 from thelounge/astorije/config-options

Deprecate existing options of `thelounge start` and add a generic `--config` override
This commit is contained in:
Jérémie Astori 2017-12-12 00:48:59 -05:00 committed by GitHub
commit d0f5d5025e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 269 additions and 17 deletions

View File

@ -2,6 +2,7 @@
global.log = require("../log.js"); global.log = require("../log.js");
const _ = require("lodash");
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
const program = require("commander"); const program = require("commander");
@ -10,18 +11,24 @@ const Helper = require("../helper");
const Utils = require("./utils"); const Utils = require("./utils");
if (require("semver").lt(process.version, "6.0.0")) { if (require("semver").lt(process.version, "6.0.0")) {
log.warn(`Support of Node.js v4 is ${colors.bold("deprecated")} and will be removed in The Lounge v3.`); log.warn(`Support of Node.js v4 is ${colors.bold.red("deprecated")} and will be removed in The Lounge v3.`);
log.warn("Please upgrade to Node.js v6 or more recent."); log.warn("Please upgrade to Node.js v6 or more recent.");
} }
program.version(Helper.getVersion(), "-v, --version") program.version(Helper.getVersion(), "-v, --version")
.option("--home <path>", `${colors.bold("[DEPRECATED]")} Use the ${colors.green("THELOUNGE_HOME")} environment variable instead.`) .option("--home <path>", `${colors.bold.red("[DEPRECATED]")} Use the ${colors.green("THELOUNGE_HOME")} environment variable instead.`)
.on("--help", Utils.extraHelp) .option(
.parseOptions(process.argv); "-c, --config <key=value>",
"override entries of the configuration file, must be specified for each entry that needs to be overriden",
Utils.parseConfigOptions
)
.on("--help", Utils.extraHelp);
// Parse options from `argv` returning `argv` void of these options.
const argvWithoutOptions = program.parseOptions(process.argv);
if (program.home) { if (program.home) {
log.warn(`${colors.green("--home")} is ${colors.bold("deprecated")} and will be removed in The Lounge v3.`); log.warn(`${colors.bold("--home")} is ${colors.bold.red("deprecated")} and will be removed in The Lounge v3. Use the ${colors.bold("THELOUNGE_HOME")} environment variable instead.`);
log.warn(`Use the ${colors.green("THELOUNGE_HOME")} environment variable instead.`);
} }
// Check if the app was built before calling setHome as it wants to load manifest.json from the public folder // Check if the app was built before calling setHome as it wants to load manifest.json from the public folder
@ -37,7 +44,7 @@ if (!fs.existsSync(path.join(
} }
if (process.env.LOUNGE_HOME) { if (process.env.LOUNGE_HOME) {
log.warn(`${colors.green("LOUNGE_HOME")} is ${colors.bold("deprecated")} and will be removed in The Lounge v3.`); log.warn(`${colors.green("LOUNGE_HOME")} is ${colors.bold.red("deprecated")} and will be removed in The Lounge v3.`);
log.warn(`Use ${colors.green("THELOUNGE_HOME")} instead.`); log.warn(`Use ${colors.green("THELOUNGE_HOME")} instead.`);
} }
@ -49,6 +56,9 @@ if (!home) {
Helper.setHome(home); Helper.setHome(home);
// Merge config key-values passed as CLI options into the main config
_.merge(Helper.config, program.config);
require("./start"); require("./start");
require("./config"); require("./config");
if (!Helper.config.public && !Helper.config.ldap.enable) { if (!Helper.config.public && !Helper.config.ldap.enable) {
@ -58,12 +68,18 @@ require("./install");
// TODO: Remove this when releasing The Lounge v3 // TODO: Remove this when releasing The Lounge v3
if (process.argv[1].endsWith(`${require("path").sep}lounge`)) { if (process.argv[1].endsWith(`${require("path").sep}lounge`)) {
log.warn(`The ${colors.red("lounge")} CLI is ${colors.bold("deprecated")} and will be removed in v3.`); log.warn(`The ${colors.red("lounge")} CLI is ${colors.bold.red("deprecated")} and will be removed in v3.`);
log.warn(`Use ${colors.green("thelounge")} instead.`); log.warn(`Use ${colors.green("thelounge")} instead.`);
process.argv[1] = "thelounge"; process.argv[1] = "thelounge";
} }
program.parse(process.argv); // `parse` expects to be passed `process.argv`, but we need to remove to give it
// a version of `argv` that does not contain options already parsed by
// `parseOptions` above.
// This is done by giving it the updated `argv` that `parseOptions` returned,
// except it returns an object with `args`/`unknown`, so we need to concat them.
// See https://github.com/tj/commander.js/blob/fefda77f463292/index.js#L686-L763
program.parse(argvWithoutOptions.args.concat(argvWithoutOptions.unknown));
if (!program.args.length) { if (!program.args.length) {
program.help(); program.help();

View File

@ -10,11 +10,11 @@ const Utils = require("./utils");
program program
.command("start") .command("start")
.option("-H, --host <ip>", "set the IP address or hostname for the web server to listen on") .option("-H, --host <ip>", `${colors.bold.red("[DEPRECATED]")} to set the IP address or hostname for the web server to listen on, use ${colors.bold("-c host=<ip>")} instead`)
.option("-P, --port <port>", "set the port to listen on") .option("-P, --port <port>", `${colors.bold.red("[DEPRECATED]")} to set the port to listen on, use ${colors.bold("-c port=<port>")} instead`)
.option("-B, --bind <ip>", "set the local IP to bind to for outgoing connections") .option("-B, --bind <ip>", `${colors.bold.red("[DEPRECATED]")} to set the local IP to bind to for outgoing connections, use ${colors.bold("-c bind=<ip>")} instead`)
.option(" --public", "start in public mode") .option(" --public", `${colors.bold.red("[DEPRECATED]")} to start in public mode, use ${colors.bold("-c public=true")} instead`)
.option(" --private", "start in private mode") .option(" --private", `${colors.bold.red("[DEPRECATED]")} to start in private mode, use ${colors.bold("-c public=false")} instead`)
.description("Start the server") .description("Start the server")
.on("--help", Utils.extraHelp) .on("--help", Utils.extraHelp)
.action(function(options) { .action(function(options) {
@ -22,6 +22,22 @@ program
const server = require("../server"); const server = require("../server");
if (options.host) {
log.warn(`${colors.bold("-H, --host <ip>")} is ${colors.bold.red("deprecated")} and will be removed in The Lounge v3. Use ${colors.bold("-c host=<ip>")} instead.`);
}
if (options.port) {
log.warn(`${colors.bold("-P, --port <port>")} is ${colors.bold.red("deprecated")} and will be removed in The Lounge v3. Use ${colors.bold("-c port=<port>")} instead.`);
}
if (options.bind) {
log.warn(`${colors.bold("-B, --bind <ip>")} is ${colors.bold.red("deprecated")} and will be removed in The Lounge v3. Use ${colors.bold("-c bind=<ip>")} instead.`);
}
if (options.public) {
log.warn(`${colors.bold("--public")} is ${colors.bold.red("deprecated")} and will be removed in The Lounge v3. Use ${colors.bold("-c public=true")} instead.`);
}
if (options.private) {
log.warn(`${colors.bold("--private")} is ${colors.bold.red("deprecated")} and will be removed in The Lounge v3. Use ${colors.bold("-c public=false")} instead.`);
}
var mode = Helper.config.public; var mode = Helper.config.public;
if (options.public) { if (options.public) {
mode = true; mode = true;

View File

@ -1,5 +1,6 @@
"use strict"; "use strict";
const _ = require("lodash");
const colors = require("colors/safe"); const colors = require("colors/safe");
const fs = require("fs"); const fs = require("fs");
const Helper = require("../helper"); const Helper = require("../helper");
@ -16,7 +17,7 @@ class Utils {
"", "",
` THELOUNGE_HOME Path for all configuration files and folders. Defaults to ${colors.green(Helper.expandHome(Utils.defaultHome()))}.`, ` THELOUNGE_HOME Path for all configuration files and folders. Defaults to ${colors.green(Helper.expandHome(Utils.defaultHome()))}.`,
"", "",
].forEach((e) => console.log(e)); // eslint-disable-line no-console ].forEach((e) => log.raw(e));
} }
static defaultHome() { static defaultHome() {
@ -34,7 +35,7 @@ class Utils {
".lounge_home" ".lounge_home"
)); ));
if (fs.existsSync(deprecatedDistConfig)) { if (fs.existsSync(deprecatedDistConfig)) {
log.warn(`${colors.green(".lounge_home")} is ${colors.bold("deprecated")} and will be ignored as of The Lounge v3.`); log.warn(`${colors.green(".lounge_home")} is ${colors.bold.red("deprecated")} and will be ignored as of The Lounge v3.`);
log.warn(`Use ${colors.green(".thelounge_home")} instead.`); log.warn(`Use ${colors.green(".thelounge_home")} instead.`);
distConfig = deprecatedDistConfig; distConfig = deprecatedDistConfig;
@ -51,6 +52,55 @@ class Utils {
return home; return home;
} }
// Parses CLI options such as `-c public=true`, `-c debug.raw=true`, etc.
static parseConfigOptions(val, memo) {
// Invalid option that is not of format `key=value`, do nothing
if (!val.includes("=")) {
return memo;
}
const parseValue = (value) => {
if (value === "true") {
return true;
} else if (value === "false") {
return false;
} else if (value === "undefined") {
return undefined;
} else if (value === "null") {
return null;
} else if (/^\[.*\]$/.test(value)) { // Arrays
// Supporting arrays `[a,b]` and `[a, b]`
const array = value.slice(1, -1).split(/,\s*/);
// If [] is given, it will be parsed as `[ "" ]`, so treat this as empty
if (array.length === 1 && array[0] === "") {
return [];
}
return array.map(parseValue); // Re-parses all values of the array
}
return value;
};
// First time the option is parsed, memo is not set
if (memo === undefined) {
memo = {};
}
// Note: If passed `-c foo="bar=42"` (with single or double quotes), `val`
// will always be passed as `foo=bar=42`, never with quotes.
const position = val.indexOf("="); // Only split on the first = found
const key = val.slice(0, position);
const value = val.slice(position + 1);
const parsedValue = parseValue(value);
if (_.has(memo, key)) {
log.warn(`Configuration key ${colors.bold(key)} was already specified, ignoring...`);
} else {
memo = _.set(memo, key, parsedValue);
}
return memo;
}
} }
module.exports = Utils; module.exports = Utils;

View File

@ -109,7 +109,7 @@ function setHome(newPath) {
// TODO: Remove in future release // TODO: Remove in future release
// Backwards compatibility for old way of specifying themes in settings // Backwards compatibility for old way of specifying themes in settings
if (this.config.theme.includes(".css")) { if (this.config.theme.includes(".css")) {
log.warn(`Referring to CSS files in the ${colors.green("theme")} setting of ${colors.green(configPath)} is ${colors.bold("deprecated")} and will be removed in a future version.`); log.warn(`Referring to CSS files in the ${colors.green("theme")} setting of ${colors.green(configPath)} is ${colors.bold.red("deprecated")} and will be removed in a future version.`);
} else { } else {
this.config.theme = `themes/${this.config.theme}.css`; this.config.theme = `themes/${this.config.theme}.css`;
} }

View File

@ -32,6 +32,11 @@ exports.info = function() {
exports.debug = function() { exports.debug = function() {
console.log.apply(console, timestamp(colors.green("[DEBUG]"), arguments)); console.log.apply(console, timestamp(colors.green("[DEBUG]"), arguments));
}; };
exports.raw = function() {
console.log.apply(console, arguments);
};
/* eslint-enable no-console */ /* eslint-enable no-console */
exports.prompt = (options, callback) => { exports.prompt = (options, callback) => {

View File

@ -0,0 +1,150 @@
"use strict";
const expect = require("chai").expect;
const TestUtil = require("../../util");
const Utils = require("../../../src/command-line/utils");
describe("Utils", function() {
describe(".extraHelp", function() {
let originalRaw;
beforeEach(function() {
originalRaw = log.raw;
});
afterEach(function() {
log.raw = originalRaw;
});
it("should start and end with empty lines to display correctly with --help", function() {
// Mock `log.raw` to extract its effect into an array
const stdout = [];
log.raw = TestUtil.mockLogger((str) => stdout.push(str));
Utils.extraHelp();
// Starts with 2 empty lines
expect(stdout[0]).to.equal("\n");
expect(stdout[1]).to.equal("\n");
expect(stdout[2]).to.not.equal("\n");
// Ends with 1 empty line
expect(stdout[stdout.length - 2]).to.not.equal("\n");
expect(stdout[stdout.length - 1]).to.equal("\n");
});
it("should contain information about THELOUNGE_HOME env var", function() {
// Mock `log.raw` to extract its effect into a concatenated string
let stdout = "";
log.raw = TestUtil.mockLogger((str) => stdout += str);
Utils.extraHelp();
expect(stdout).to.include("THELOUNGE_HOME");
});
});
describe(".parseConfigOptions", function() {
describe("when it's the first option given", function() {
it("should return nothing when passed an invalid config", function() {
expect(Utils.parseConfigOptions("foo")).to.be.undefined;
});
it("should correctly parse boolean values", function() {
expect(Utils.parseConfigOptions("foo=true")).to.deep.equal({foo: true});
expect(Utils.parseConfigOptions("foo=false")).to.deep.equal({foo: false});
});
it("should correctly parse empty strings", function() {
expect(Utils.parseConfigOptions("foo=")).to.deep.equal({foo: ""});
});
it("should correctly parse null values", function() {
expect(Utils.parseConfigOptions("foo=null")).to.deep.equal({foo: null});
});
it("should correctly parse undefined values", function() {
expect(Utils.parseConfigOptions("foo=undefined"))
.to.deep.equal({foo: undefined});
});
it("should correctly parse array values", function() {
expect(Utils.parseConfigOptions("foo=[bar,true]"))
.to.deep.equal({foo: ["bar", true]});
expect(Utils.parseConfigOptions("foo=[bar, true]"))
.to.deep.equal({foo: ["bar", true]});
});
it("should correctly parse empty array values", function() {
expect(Utils.parseConfigOptions("foo=[]"))
.to.deep.equal({foo: []});
});
it("should correctly parse values that contain `=` sign", function() {
expect(Utils.parseConfigOptions("foo=bar=42"))
.to.deep.equal({foo: "bar=42"});
});
it("should correctly parse keys using dot-notation", function() {
expect(Utils.parseConfigOptions("foo.bar=value"))
.to.deep.equal({foo: {bar: "value"}});
});
it("should correctly parse keys using array-notation", function() {
expect(Utils.parseConfigOptions("foo[0]=value"))
.to.deep.equal({foo: ["value"]});
});
});
describe("when some options have already been parsed", function() {
it("should not modify existing options when passed an invalid config", function() {
const memo = {foo: "bar"};
expect(Utils.parseConfigOptions("foo", memo)).to.equal(memo);
});
it("should combine a new option with previously parsed ones", function() {
expect(Utils.parseConfigOptions("bar=false", {foo: true}))
.to.deep.equal({foo: true, bar: false});
});
it("should maintain existing properties of a nested object", function() {
expect(Utils.parseConfigOptions("foo.bar=true", {foo: {baz: false}}))
.to.deep.equal({foo: {bar: true, baz: false}});
});
it("should maintain existing entries of an array", function() {
expect(Utils.parseConfigOptions("foo[1]=baz", {foo: ["bar"]}))
.to.deep.equal({foo: ["bar", "baz"]});
});
describe("when given the same key multiple times", function() {
let originalWarn;
beforeEach(function() {
originalWarn = log.warn;
});
afterEach(function() {
log.warn = originalWarn;
});
it("should not override options", function() {
log.warn = () => {};
expect(Utils.parseConfigOptions("foo=baz", {foo: "bar"}))
.to.deep.equal({foo: "bar"});
});
it("should display a warning", function() {
let warning = "";
log.warn = TestUtil.mockLogger((str) => warning += str);
Utils.parseConfigOptions("foo=bar", {foo: "baz"});
expect(warning).to.include("foo was already specified");
});
});
});
});
});

View File

@ -23,6 +23,20 @@ MockClient.prototype.createMessage = function(opts) {
return message; return message;
}; };
function mockLogger(callback) {
return function() {
// TODO: Use ...args with The Lounge v3: add `...args` as function argument
// and replaced the next line with `args.join(", ")`
const stdout = Array.prototype.slice.call(arguments).join(", ")
.replace( // Removes ANSI colors. See https://stackoverflow.com/a/29497680
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
""
);
callback(stdout + "\n");
};
}
module.exports = { module.exports = {
createClient: function() { createClient: function() {
return new MockClient(); return new MockClient();
@ -38,4 +52,5 @@ module.exports = {
createWebserver: function() { createWebserver: function() {
return express(); return express();
}, },
mockLogger,
}; };