Skip to content

Commit

Permalink
[FEATURE] Add config command (#618)
Browse files Browse the repository at this point in the history
This feature depends on SAP/ui5-project#575 and makes the configuration of the `mavenSnapshotEndpointUrl` option possible, which is used by  SAP/ui5-project#570

Co-authored-by: Merlin Beutlberger <m.beutlberger@sap.com>
Co-authored-by: Günter Klatt <57760635+KlattG@users.noreply.github.com>
  • Loading branch information
3 people authored Apr 25, 2023
1 parent f994468 commit 9910e30
Show file tree
Hide file tree
Showing 4 changed files with 351 additions and 3 deletions.
4 changes: 2 additions & 2 deletions lib/cli/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import ConsoleWriter from "@ui5/logger/writers/Console";

export default function(cli) {
cli.usage("Usage: ui5 <command> [options]")
.demandCommand(1, "Command required! Please have a look at the help documentation above.")
.demandCommand(1, "Command required")
.option("config", {
alias: "c",
describe: "Path to project configuration file in YAML format",
Expand Down Expand Up @@ -121,7 +121,7 @@ export default function(cli) {
console.log(chalk.bold.yellow("Command Failed:"));
console.log(`${msg}`);
console.log("");
console.log(chalk.dim(`See 'ui5 --help' or 'ui5 build --help' for help`));
console.log(chalk.dim(`See 'ui5 --help'`));
}
process.exit(1);
});
Expand Down
88 changes: 88 additions & 0 deletions lib/cli/commands/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import chalk from "chalk";
import process from "node:process";
import baseMiddleware from "../middlewares/base.js";

const configCommand = {
command: "config",
describe: "Get and set UI5 Tooling configuration options",
middlewares: [baseMiddleware],
handler: handleConfig
};

configCommand.builder = function(cli) {
return cli
.demandCommand(1, "Command required. Available commands are 'set', 'get', and 'list'")
.command("set <key> [value]", "Set the value for a given configuration key. " +
"Clear an existing configuration by omitting the value", {
handler: handleConfig,
builder: noop,
middlewares: [baseMiddleware],
})
.command("get <key>", "Get the value for a given configuration key", {
handler: handleConfig,
builder: noop,
middlewares: [baseMiddleware],
})
.command("list", "Display the current configuration", {
handler: handleConfig,
builder: noop,
middlewares: [baseMiddleware],
})
.example("$0 set mavenSnapshotEndpointUrl http://example.com/snapshots/",
"Set a value for the mavenSnapshotEndpointUrl configuration")
.example("$0 set mavenSnapshotEndpointUrl",
"Unset the current value of the mavenSnapshotEndpointUrl configuration");
};

function noop() {}

async function handleConfig(argv) {
const {_: commandArgs, key, value} = argv;
const command = commandArgs[commandArgs.length - 1];

const {default: Configuration} = await import( "@ui5/project/config/Configuration");
const allowedKeys = ["mavenSnapshotEndpointUrl"];

if (["set", "get"].includes(command) && !allowedKeys.includes(key)) {
throw new Error(
`The provided key is not a valid configuration option. Valid options are: ${allowedKeys.join(", ")}`);
}

const config = await Configuration.fromFile();
if (command === "list") {
// Print all configuration values to stdout
process.stdout.write(formatJsonForOutput(config.toJson()));
} else if (command === "get") {
// Get a single configuration value and print to stdout
let configValue = config.toJson()[key];
if (configValue === undefined) {
configValue = "";
}
process.stdout.write(`${configValue}\n`);
} else if (command === "set") {
const jsonConfig = config.toJson();
if (value === undefined || value === "") {
delete jsonConfig[key];
process.stderr.write(`Configuration option ${chalk.bold(key)} has been unset\n`);
} else {
jsonConfig[key] = value;
process.stderr.write(`Configuration option ${chalk.bold(key)} has been updated:
${formatJsonForOutput(jsonConfig, key)}`);
}

await Configuration.toFile(new Configuration(jsonConfig));
} else {
throw new Error(`Unknown 'ui5 config' command '${command}'`);
}
}

function formatJsonForOutput(config, filterKey) {
return Object.keys(config)
.filter((key) => !filterKey || filterKey === key)
.filter((key) => config[key] !== undefined) // Don't print undefined config values
.map((key) => {
return ` ${key} = ${config[key]}\n`;
}).join("");
}

export default configCommand;
2 changes: 1 addition & 1 deletion test/lib/cli/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ test.serial("Yargs error handling", async (t) => {
t.deepEqual(consoleLogStub.getCall(1).args, ["Unknown argument: invalid"], "Correct error log");
t.deepEqual(consoleLogStub.getCall(2).args, [""], "Correct error log");
t.deepEqual(consoleLogStub.getCall(3).args, [
chalk.dim(`See 'ui5 --help' or 'ui5 build --help' for help`)
chalk.dim(`See 'ui5 --help'`)
], "Correct error log");
});

Expand Down
260 changes: 260 additions & 0 deletions test/lib/cli/commands/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
import test from "ava";
import sinon from "sinon";
import esmock from "esmock";
import chalk from "chalk";

function getDefaultArgv() {
// This has been taken from the actual argv object yargs provides
return {
"_": ["build"],
"loglevel": "info",
"log-level": "info",
"logLevel": "info",
"perf": false,
"silent": false,
"include-all-dependencies": false,
"all": false,
"a": false,
"includeAllDependencies": false,
"create-build-manifest": false,
"createBuildManifest": false,
"dest": "./dist",
"clean-dest": false,
"cleanDest": false,
"experimental-css-variables": false,
"experimentalCssVariables": false,
"$0": "ui5"
};
}

test.beforeEach(async (t) => {
t.context.argv = getDefaultArgv();

t.context.builder = sinon.stub().resolves();
t.context.stdoutWriteStub = sinon.stub(process.stdout, "write");
t.context.stderrWriteStub = sinon.stub(process.stderr, "write");
t.context.configToJson = sinon.stub().returns({});

const {default: Configuration} = await import( "@ui5/project/config/Configuration");
t.context.Configuration = Configuration;
sinon.stub(Configuration, "fromFile").resolves(new Configuration({}));
sinon.stub(Configuration, "toFile").resolves();

t.context.config = await esmock.p("../../../../lib/cli/commands/config.js", {
"@ui5/project/config/Configuration": t.context.Configuration
});
});

test.afterEach.always((t) => {
sinon.restore();
esmock.purge(t.context.config);
});

test.serial("ui5 config list", async (t) => {
const {config, argv, stdoutWriteStub, stderrWriteStub, Configuration} = t.context;

const configurationStub = new Configuration({});
sinon.stub(configurationStub, "toJson").returns({
mavenSnapshotEndpointUrl: "my/url",
otherConfig: false,
pony: 130,
horses: null,
cows: undefined // Won't be rendered
});
Configuration.fromFile.resolves(configurationStub);

argv["_"] = ["list"];
await config.handler(argv);

t.is(stdoutWriteStub.firstCall.firstArg, ` mavenSnapshotEndpointUrl = my/url
otherConfig = false
pony = 130
horses = null
`, "Logged expected text to stdout");
t.is(stderrWriteStub.callCount, 0, "Nothing written to stderr");
});

test.serial("ui5 config list: No config", async (t) => {
const {config, argv, stdoutWriteStub, stderrWriteStub} = t.context;

argv["_"] = ["list"];
await config.handler(argv);

t.is(stdoutWriteStub.firstCall.firstArg, "",
"Logged no text to stdout");
t.is(stderrWriteStub.callCount, 0, "Nothing written to stderr");
});

test.serial("ui5 config get", async (t) => {
const {config, argv, stdoutWriteStub, stderrWriteStub, Configuration} = t.context;

Configuration.fromFile.resolves(new Configuration({
mavenSnapshotEndpointUrl: "my/url"
}));

argv["_"] = ["get"];
argv["key"] = "mavenSnapshotEndpointUrl";
await config.handler(argv);

t.is(stdoutWriteStub.firstCall.firstArg, "my/url\n",
"Logged configuration value to stdout");
t.is(stderrWriteStub.callCount, 0, "Nothing written to stderr");
});

test.serial("ui5 config get: Empty value", async (t) => {
const {config, argv, stdoutWriteStub} = t.context;

argv["_"] = ["get"];
argv["key"] = "mavenSnapshotEndpointUrl";
await config.handler(argv);

t.is(stdoutWriteStub.firstCall.firstArg, "\n", "Logged no value to console");
});

test.serial("ui5 config set", async (t) => {
const {config, argv, stderrWriteStub, stdoutWriteStub, Configuration} = t.context;

const configurationStub = new Configuration({});
sinon.stub(configurationStub, "toJson").returns({
mavenSnapshotEndpointUrl: "my/url"
});

Configuration.fromFile.resolves(configurationStub);

argv["_"] = ["set"];
argv["key"] = "mavenSnapshotEndpointUrl";
argv["value"] = "https://_snapshot_endpoint_";
await config.handler(argv);

t.is(stderrWriteStub.firstCall.firstArg,
`Configuration option ${chalk.bold("mavenSnapshotEndpointUrl")} has been updated:
mavenSnapshotEndpointUrl = https://_snapshot_endpoint_\n`,
"Logged expected message to stderr");
t.is(stdoutWriteStub.callCount, 0, "Nothing written to stdout");

t.is(Configuration.toFile.callCount, 1, "Configuration#toFile got called once");
t.deepEqual(Configuration.toFile.firstCall.firstArg.toJson(), {
mavenSnapshotEndpointUrl: "https://_snapshot_endpoint_"
}, "Configuration#toFile got called with expected argument");
});

test.serial("ui5 config set without a value should delete the configuration", async (t) => {
const {config, argv, stderrWriteStub, stdoutWriteStub, Configuration} = t.context;

argv["_"] = ["set"];
argv["key"] = "mavenSnapshotEndpointUrl";
argv["value"] = undefined; // Simulating no value parameter provided to Yargs
await config.handler(argv);

t.is(stderrWriteStub.firstCall.firstArg,
`Configuration option ${chalk.bold("mavenSnapshotEndpointUrl")} has been unset\n`,
"Logged expected message to stderr");
t.is(stdoutWriteStub.callCount, 0, "Nothing written to stdout");

t.is(Configuration.toFile.callCount, 1, "Configuration#toFile got called once");
t.deepEqual(Configuration.toFile.firstCall.firstArg.toJson(), {
mavenSnapshotEndpointUrl: undefined
}, "Configuration#toFile got called with expected argument");
});

test.serial("ui5 config set with an empty value should delete the configuration", async (t) => {
const {config, argv, stderrWriteStub, stdoutWriteStub, Configuration} = t.context;

argv["_"] = ["set"];
argv["key"] = "mavenSnapshotEndpointUrl";
argv["value"] = ""; // Simulating empty value provided to Yargs using quotes ""
await config.handler(argv);

t.is(stderrWriteStub.firstCall.firstArg,
`Configuration option ${chalk.bold("mavenSnapshotEndpointUrl")} has been unset\n`,
"Logged expected message to stderr");
t.is(stdoutWriteStub.callCount, 0, "Nothing written to stdout");

t.is(Configuration.toFile.callCount, 1, "Configuration#toFile got called once");
t.deepEqual(Configuration.toFile.firstCall.firstArg.toJson(), {
mavenSnapshotEndpointUrl: undefined
}, "Configuration#toFile got called with expected argument");
});

test.serial("ui5 config set null should update the configuration", async (t) => {
const {config, argv, stderrWriteStub, stdoutWriteStub, Configuration} = t.context;

argv["_"] = ["set"];
argv["key"] = "mavenSnapshotEndpointUrl";

// Yargs would never provide us with other types than string. Still, our code should
// check for empty strings and nothing else (like falsy)
argv["value"] = 0;
await config.handler(argv);

t.is(stderrWriteStub.firstCall.firstArg,
`Configuration option ${chalk.bold("mavenSnapshotEndpointUrl")} has been updated:
mavenSnapshotEndpointUrl = 0\n`,
"Logged expected message to stderr");
t.is(stdoutWriteStub.callCount, 0, "Nothing written to stdout");

t.is(Configuration.toFile.callCount, 1, "Configuration#toFile got called once");
t.deepEqual(Configuration.toFile.firstCall.firstArg.toJson(), {
mavenSnapshotEndpointUrl: 0
}, "Configuration#toFile got called with expected argument");
});

test.serial("ui5 config set false should update the configuration", async (t) => {
const {config, argv, stderrWriteStub, stdoutWriteStub, Configuration} = t.context;

argv["_"] = ["set"];
argv["key"] = "mavenSnapshotEndpointUrl";

// Yargs would never provide us with other types than string. Still, our code should
// check for empty strings and nothing else (like falsyness)
argv["value"] = false;
await config.handler(argv);

t.is(stderrWriteStub.firstCall.firstArg,
`Configuration option ${chalk.bold("mavenSnapshotEndpointUrl")} has been updated:
mavenSnapshotEndpointUrl = false\n`,
"Logged expected message to stderr");
t.is(stdoutWriteStub.callCount, 0, "Nothing written to stdout");

t.is(Configuration.toFile.callCount, 1, "Configuration#toFile got called once");
t.deepEqual(Configuration.toFile.firstCall.firstArg.toJson(), {
mavenSnapshotEndpointUrl: false
}, "Configuration#toFile got called with expected argument");
});

test.serial("ui5 config invalid key", async (t) => {
const {config, argv} = t.context;

argv["_"] = ["get"];
argv["key"] = "_invalid_key_";
argv["value"] = "https://_snapshot_endpoint_";

await t.throwsAsync(config.handler(argv), {
message:
"The provided key is not a valid configuration option. Valid options are: mavenSnapshotEndpointUrl",
});
});

test.serial("ui5 config empty key", async (t) => {
const {config, argv} = t.context;

argv["_"] = ["set"];
argv["key"] = "";
argv["value"] = undefined;

await t.throwsAsync(config.handler(argv), {
message:
"The provided key is not a valid configuration option. Valid options are: mavenSnapshotEndpointUrl",
});
});

test.serial("ui5 config unknown command", async (t) => {
const {config, argv} = t.context;

argv["_"] = ["foo"];

await t.throwsAsync(config.handler(argv), {
message:
"Unknown 'ui5 config' command 'foo'",
});
});

0 comments on commit 9910e30

Please sign in to comment.