From 35cdf3cd8254c3a7004beab8aed0e72f0c177860 Mon Sep 17 00:00:00 2001 From: Yavor Ivanov Date: Fri, 31 Mar 2023 14:58:49 +0300 Subject: [PATCH 01/15] Add .ui5rc configuration --- lib/cli/commands/config.js | 84 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 lib/cli/commands/config.js diff --git a/lib/cli/commands/config.js b/lib/cli/commands/config.js new file mode 100644 index 00000000..26df5e56 --- /dev/null +++ b/lib/cli/commands/config.js @@ -0,0 +1,84 @@ +import path from "node:path"; +import os from "node:os"; +import baseMiddleware from "../middlewares/base.js"; + +const ALLOWED_KEYS = ["snapshotEndpointUrl"]; + +const configCommand = { + command: "config", + describe: "Configures the `.ui5rc` file.", + middlewares: [baseMiddleware], +}; + +configCommand.builder = function(cli) { + return cli + .command("set ", "Sets a property in `.ui5rc`", { + handler: handleConfig, + builder: noop, + middlewares: [baseMiddleware], + }) + .command("get ", "Gets a property from `.ui5rc`", { + handler: handleConfig, + builder: noop, + middlewares: [baseMiddleware], + }) + .command("list", "List settings stored in `.ui5rc`", { + handler: handleConfig, + builder: noop, + middlewares: [baseMiddleware], + }); +}; + +function noop() {} + +async function handleConfig(argv) { + const {_: commandArgs, key, value} = argv; + const config = await readConfig(); + + if (key && !ALLOWED_KEYS.includes(key)) { + throw new Error(`The provided key is not part of the .ui5rc allowed options: ${ALLOWED_KEYS.join(", ")}`); + } + + if (commandArgs.includes("list")) { + console.log(config); + } else if (commandArgs.includes("get")) { + console.log(config[key]); + } else if (commandArgs.includes("set")) { + const newConfig = {}; + newConfig[key] = value; + await saveConfig({...config, ...newConfig}); + } +} + +async function readConfig() { + const filePath = path.resolve(path.join(os.homedir(), ".ui5rc")); + + const {default: fs} = await import("graceful-fs"); + const {promisify} = await import("node:util"); + const readFile = promisify(fs.readFile); + let config; + try { + const fileContent = await readFile(filePath); + config = JSON.parse(fileContent); + } catch (err) { + if (err.code === "ENOENT") { + // "File or directory does not exist" + config = {}; + } else { + throw err; + } + } + return config; +} + +async function saveConfig(config) { + const filePath = path.resolve(path.join(os.homedir(), ".ui5rc")); + + const {default: fs} = await import("graceful-fs"); + const {promisify} = await import("node:util"); + const writeFile = promisify(fs.writeFile); + + return writeFile(filePath, JSON.stringify(config)); +} + +export default configCommand; From 2b37979bdfc4b8000d0ac9ecd241cc8a438e2ed0 Mon Sep 17 00:00:00 2001 From: Yavor Ivanov Date: Tue, 4 Apr 2023 11:39:50 +0300 Subject: [PATCH 02/15] Format the output --- lib/cli/commands/config.js | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/lib/cli/commands/config.js b/lib/cli/commands/config.js index 26df5e56..0d0e1200 100644 --- a/lib/cli/commands/config.js +++ b/lib/cli/commands/config.js @@ -1,8 +1,10 @@ import path from "node:path"; import os from "node:os"; +import chalk from "chalk"; import baseMiddleware from "../middlewares/base.js"; const ALLOWED_KEYS = ["snapshotEndpointUrl"]; +const ui5RcFilePath = path.resolve(path.join(os.homedir(), ".ui5rc")); const configCommand = { command: "config", @@ -40,25 +42,37 @@ async function handleConfig(argv) { } if (commandArgs.includes("list")) { - console.log(config); + console.log(`Listing properties from ${chalk.dim(ui5RcFilePath)}: +${formatJsonForOutput(config)}`); } else if (commandArgs.includes("get")) { - console.log(config[key]); + console.log(`Getting property ${chalk.bold(key)} from ${chalk.dim(ui5RcFilePath)}: +${formatJsonForOutput(config, key)}`); } else if (commandArgs.includes("set")) { const newConfig = {}; newConfig[key] = value; + + console.log(`Set property ${chalk.bold(key)} into ${chalk.dim(ui5RcFilePath)}: +${formatJsonForOutput(newConfig, key)}`); + await saveConfig({...config, ...newConfig}); } } -async function readConfig() { - const filePath = path.resolve(path.join(os.homedir(), ".ui5rc")); +function formatJsonForOutput(config, filterKey) { + return Object.keys(config) + .filter((key) => !filterKey || filterKey === key) + .map( + (key) => ` ${key} = ${config[key]}\n` + ).join(""); +} +async function readConfig() { const {default: fs} = await import("graceful-fs"); const {promisify} = await import("node:util"); const readFile = promisify(fs.readFile); let config; try { - const fileContent = await readFile(filePath); + const fileContent = await readFile(ui5RcFilePath); config = JSON.parse(fileContent); } catch (err) { if (err.code === "ENOENT") { @@ -72,13 +86,11 @@ async function readConfig() { } async function saveConfig(config) { - const filePath = path.resolve(path.join(os.homedir(), ".ui5rc")); - const {default: fs} = await import("graceful-fs"); const {promisify} = await import("node:util"); const writeFile = promisify(fs.writeFile); - return writeFile(filePath, JSON.stringify(config)); + return writeFile(ui5RcFilePath, JSON.stringify(config)); } export default configCommand; From 740eceb5e6789cb209c96c4eef0cbf9af702906d Mon Sep 17 00:00:00 2001 From: Yavor Ivanov Date: Tue, 4 Apr 2023 15:42:24 +0300 Subject: [PATCH 03/15] Format output --- lib/cli/commands/config.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/cli/commands/config.js b/lib/cli/commands/config.js index 0d0e1200..161d6023 100644 --- a/lib/cli/commands/config.js +++ b/lib/cli/commands/config.js @@ -10,6 +10,7 @@ const configCommand = { command: "config", describe: "Configures the `.ui5rc` file.", middlewares: [baseMiddleware], + handler: handleConfig }; configCommand.builder = function(cli) { @@ -48,13 +49,16 @@ ${formatJsonForOutput(config)}`); console.log(`Getting property ${chalk.bold(key)} from ${chalk.dim(ui5RcFilePath)}: ${formatJsonForOutput(config, key)}`); } else if (commandArgs.includes("set")) { - const newConfig = {}; - newConfig[key] = value; + if (value) { + config[key] = value; + } else { + delete config[key]; + } console.log(`Set property ${chalk.bold(key)} into ${chalk.dim(ui5RcFilePath)}: -${formatJsonForOutput(newConfig, key)}`); +${formatJsonForOutput(config, key)}`); - await saveConfig({...config, ...newConfig}); + await saveConfig(config); } } @@ -66,6 +70,7 @@ function formatJsonForOutput(config, filterKey) { ).join(""); } +// TODO: Migrate to config/Configuration.js when available async function readConfig() { const {default: fs} = await import("graceful-fs"); const {promisify} = await import("node:util"); @@ -85,6 +90,7 @@ async function readConfig() { return config; } +// TODO: Migrate to config/Configuration.js when available async function saveConfig(config) { const {default: fs} = await import("graceful-fs"); const {promisify} = await import("node:util"); From 7bab8d8ddb865c67c8398544081a0498cf57ad76 Mon Sep 17 00:00:00 2001 From: Yavor Ivanov Date: Tue, 4 Apr 2023 15:42:29 +0300 Subject: [PATCH 04/15] Add tests --- test/lib/cli/commands/config.js | 121 ++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 test/lib/cli/commands/config.js diff --git a/test/lib/cli/commands/config.js b/test/lib/cli/commands/config.js new file mode 100644 index 00000000..4fcc6acf --- /dev/null +++ b/test/lib/cli/commands/config.js @@ -0,0 +1,121 @@ +import test from "ava"; +import sinon from "sinon"; +import esmock from "esmock"; +const {default: fs} = await import("graceful-fs"); + +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.consoleLog = sinon.stub(console, "log"); + t.context.promisifyStub = sinon.stub(); + + t.context.promisifyStub + .withArgs(fs.readFile) + .returns(() => JSON.stringify({snapshotEndpointUrl: "__url__"})); + + t.context.promisifyStub.withArgs(fs.writeFile).returns(() => {}); + + t.context.config = await esmock.p("../../../../lib/cli/commands/config.js", { + "node:util": { + "promisify": t.context.promisifyStub + }, + }); +}); + +test.afterEach.always((t) => { + sinon.restore(); + esmock.purge(t.context.config); +}); + +test.serial("ui5 config list", async (t) => { + const {config, argv, consoleLog} = t.context; + + // console.log(promisifyStub); + argv["_"] = ["list"]; + await config.handler(argv); + + t.true(consoleLog.getCall(0).args[0].startsWith("Listing properties from "), "Lists properties via console.log"); +}); + +test.serial("ui5 config get", async (t) => { + const {config, argv, consoleLog} = t.context; + + argv["_"] = ["get"]; + argv["key"] = "snapshotEndpointUrl"; + await config.handler(argv); + + t.true( + consoleLog.getCall(0) + .args[0].startsWith("Getting property snapshotEndpointUrl"), + "Getting key from config file" + ); +}); + +test.serial("ui5 config set", async (t) => { + const {config, argv, consoleLog} = t.context; + + argv["_"] = ["set"]; + argv["key"] = "snapshotEndpointUrl"; + argv["value"] = "https://_snapshot_endpoint_"; + await config.handler(argv); + + t.true( + consoleLog.getCall(0) + .args[0].startsWith("Set property snapshotEndpointUrl into "), + "Setting value into file" + ); +}); + +test.serial("ui5 config set empty value should delete", async (t) => { + const {config, argv, consoleLog} = t.context; + + argv["_"] = ["set"]; + argv["key"] = "snapshotEndpointUrl"; + argv["value"] = ""; + await config.handler(argv); + + t.true( + consoleLog.getCall(0) + .args[0].startsWith("Set property snapshotEndpointUrl into "), + "Setting value into file" + ); +}); + +test.serial("ui5 config invalid key", async (t) => { + const {config, argv} = t.context; + + argv["_"] = ["set"]; + argv["key"] = "_invalid_key_"; + argv["value"] = "https://_snapshot_endpoint_"; + + await t.throwsAsync(config.handler(argv), { + message: + "The provided key is not part of the .ui5rc allowed options: snapshotEndpointUrl", + }); +}); + From 0af6e4620647f4f7884a5e08e131efefce9bcee8 Mon Sep 17 00:00:00 2001 From: Yavor Ivanov Date: Wed, 5 Apr 2023 10:04:53 +0300 Subject: [PATCH 05/15] Fix tests --- test/lib/cli/commands/config.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/lib/cli/commands/config.js b/test/lib/cli/commands/config.js index 4fcc6acf..a981eb38 100644 --- a/test/lib/cli/commands/config.js +++ b/test/lib/cli/commands/config.js @@ -55,7 +55,6 @@ test.afterEach.always((t) => { test.serial("ui5 config list", async (t) => { const {config, argv, consoleLog} = t.context; - // console.log(promisifyStub); argv["_"] = ["list"]; await config.handler(argv); @@ -71,7 +70,7 @@ test.serial("ui5 config get", async (t) => { t.true( consoleLog.getCall(0) - .args[0].startsWith("Getting property snapshotEndpointUrl"), + .args[0].startsWith("Getting property "), "Getting key from config file" ); }); @@ -86,7 +85,7 @@ test.serial("ui5 config set", async (t) => { t.true( consoleLog.getCall(0) - .args[0].startsWith("Set property snapshotEndpointUrl into "), + .args[0].startsWith("Set property "), "Setting value into file" ); }); @@ -101,7 +100,7 @@ test.serial("ui5 config set empty value should delete", async (t) => { t.true( consoleLog.getCall(0) - .args[0].startsWith("Set property snapshotEndpointUrl into "), + .args[0].startsWith("Set property "), "Setting value into file" ); }); From 3f27832f0a2472b668874398459770e328fcc38d Mon Sep 17 00:00:00 2001 From: Yavor Ivanov Date: Wed, 5 Apr 2023 10:14:25 +0300 Subject: [PATCH 06/15] Update missing dependencies --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 645a32cb..5360def5 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,8 @@ "pretty-hrtime": "^1.0.3", "semver": "^7.5.0", "update-notifier": "^6.0.2", - "yargs": "^17.7.1" + "yargs": "^17.7.1", + "graceful-fs": "^4.2.11" }, "devDependencies": { "@istanbuljs/esm-loader-hook": "^0.2.0", From 8a0ce41ff3899d8006d9510f806e61f8a2250ce4 Mon Sep 17 00:00:00 2001 From: Yavor Ivanov Date: Tue, 18 Apr 2023 15:15:12 +0300 Subject: [PATCH 07/15] Use Configuration from ui5-project https://github.com/SAP/ui5-project/pull/575 --- lib/cli/commands/config.js | 47 +++++++++----------------------------- 1 file changed, 11 insertions(+), 36 deletions(-) diff --git a/lib/cli/commands/config.js b/lib/cli/commands/config.js index 161d6023..2e38fb71 100644 --- a/lib/cli/commands/config.js +++ b/lib/cli/commands/config.js @@ -2,6 +2,8 @@ import path from "node:path"; import os from "node:os"; import chalk from "chalk"; import baseMiddleware from "../middlewares/base.js"; +// Internal module that is not exposed in package.json. Workaround in order to make it available for the CLI. +import Configuration from "../../../node_modules/@ui5/project/lib/config/Configuration.js"; const ALLOWED_KEYS = ["snapshotEndpointUrl"]; const ui5RcFilePath = path.resolve(path.join(os.homedir(), ".ui5rc")); @@ -36,29 +38,31 @@ function noop() {} async function handleConfig(argv) { const {_: commandArgs, key, value} = argv; - const config = await readConfig(); if (key && !ALLOWED_KEYS.includes(key)) { throw new Error(`The provided key is not part of the .ui5rc allowed options: ${ALLOWED_KEYS.join(", ")}`); } + const config = await Configuration.fromFile(); + if (commandArgs.includes("list")) { console.log(`Listing properties from ${chalk.dim(ui5RcFilePath)}: -${formatJsonForOutput(config)}`); +${formatJsonForOutput(config.toJSON())}`); } else if (commandArgs.includes("get")) { console.log(`Getting property ${chalk.bold(key)} from ${chalk.dim(ui5RcFilePath)}: -${formatJsonForOutput(config, key)}`); +${formatJsonForOutput(config.toJSON(), key)}`); } else if (commandArgs.includes("set")) { + const jsonConfig = config.toJSON(); if (value) { - config[key] = value; + jsonConfig[key] = value; } else { - delete config[key]; + delete jsonConfig[key]; } console.log(`Set property ${chalk.bold(key)} into ${chalk.dim(ui5RcFilePath)}: -${formatJsonForOutput(config, key)}`); +${formatJsonForOutput(jsonConfig, key)}`); - await saveConfig(config); + await Configuration.toFile(new Configuration(jsonConfig)); } } @@ -70,33 +74,4 @@ function formatJsonForOutput(config, filterKey) { ).join(""); } -// TODO: Migrate to config/Configuration.js when available -async function readConfig() { - const {default: fs} = await import("graceful-fs"); - const {promisify} = await import("node:util"); - const readFile = promisify(fs.readFile); - let config; - try { - const fileContent = await readFile(ui5RcFilePath); - config = JSON.parse(fileContent); - } catch (err) { - if (err.code === "ENOENT") { - // "File or directory does not exist" - config = {}; - } else { - throw err; - } - } - return config; -} - -// TODO: Migrate to config/Configuration.js when available -async function saveConfig(config) { - const {default: fs} = await import("graceful-fs"); - const {promisify} = await import("node:util"); - const writeFile = promisify(fs.writeFile); - - return writeFile(ui5RcFilePath, JSON.stringify(config)); -} - export default configCommand; From 085535fe0369a8e95878933978228716be847b00 Mon Sep 17 00:00:00 2001 From: Yavor Ivanov Date: Tue, 18 Apr 2023 15:34:44 +0300 Subject: [PATCH 08/15] snapshotEndpointUrl -> mavenSnapshotEndpointUrl --- lib/cli/commands/config.js | 2 +- test/lib/cli/commands/config.js | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/cli/commands/config.js b/lib/cli/commands/config.js index 2e38fb71..9bdce824 100644 --- a/lib/cli/commands/config.js +++ b/lib/cli/commands/config.js @@ -5,7 +5,7 @@ import baseMiddleware from "../middlewares/base.js"; // Internal module that is not exposed in package.json. Workaround in order to make it available for the CLI. import Configuration from "../../../node_modules/@ui5/project/lib/config/Configuration.js"; -const ALLOWED_KEYS = ["snapshotEndpointUrl"]; +const ALLOWED_KEYS = ["mavenSnapshotEndpointUrl"]; const ui5RcFilePath = path.resolve(path.join(os.homedir(), ".ui5rc")); const configCommand = { diff --git a/test/lib/cli/commands/config.js b/test/lib/cli/commands/config.js index a981eb38..af75aa14 100644 --- a/test/lib/cli/commands/config.js +++ b/test/lib/cli/commands/config.js @@ -36,7 +36,7 @@ test.beforeEach(async (t) => { t.context.promisifyStub .withArgs(fs.readFile) - .returns(() => JSON.stringify({snapshotEndpointUrl: "__url__"})); + .returns(() => JSON.stringify({mavenSnapshotEndpointUrl: "__url__"})); t.context.promisifyStub.withArgs(fs.writeFile).returns(() => {}); @@ -65,7 +65,7 @@ test.serial("ui5 config get", async (t) => { const {config, argv, consoleLog} = t.context; argv["_"] = ["get"]; - argv["key"] = "snapshotEndpointUrl"; + argv["key"] = "mavenSnapshotEndpointUrl"; await config.handler(argv); t.true( @@ -79,7 +79,7 @@ test.serial("ui5 config set", async (t) => { const {config, argv, consoleLog} = t.context; argv["_"] = ["set"]; - argv["key"] = "snapshotEndpointUrl"; + argv["key"] = "mavenSnapshotEndpointUrl"; argv["value"] = "https://_snapshot_endpoint_"; await config.handler(argv); @@ -94,7 +94,7 @@ test.serial("ui5 config set empty value should delete", async (t) => { const {config, argv, consoleLog} = t.context; argv["_"] = ["set"]; - argv["key"] = "snapshotEndpointUrl"; + argv["key"] = "mavenSnapshotEndpointUrl"; argv["value"] = ""; await config.handler(argv); @@ -114,7 +114,7 @@ test.serial("ui5 config invalid key", async (t) => { await t.throwsAsync(config.handler(argv), { message: - "The provided key is not part of the .ui5rc allowed options: snapshotEndpointUrl", + "The provided key is not part of the .ui5rc allowed options: mavenSnapshotEndpointUrl", }); }); From a13f3e59402a269beaa8de1a6b4277316761802e Mon Sep 17 00:00:00 2001 From: Yavor Ivanov Date: Wed, 19 Apr 2023 13:43:19 +0300 Subject: [PATCH 09/15] Remove graceful-fs --- package.json | 3 +-- test/lib/cli/commands/config.js | 13 +------------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 5360def5..645a32cb 100644 --- a/package.json +++ b/package.json @@ -128,8 +128,7 @@ "pretty-hrtime": "^1.0.3", "semver": "^7.5.0", "update-notifier": "^6.0.2", - "yargs": "^17.7.1", - "graceful-fs": "^4.2.11" + "yargs": "^17.7.1" }, "devDependencies": { "@istanbuljs/esm-loader-hook": "^0.2.0", diff --git a/test/lib/cli/commands/config.js b/test/lib/cli/commands/config.js index af75aa14..c0b89aec 100644 --- a/test/lib/cli/commands/config.js +++ b/test/lib/cli/commands/config.js @@ -32,19 +32,8 @@ test.beforeEach(async (t) => { t.context.builder = sinon.stub().resolves(); t.context.consoleLog = sinon.stub(console, "log"); - t.context.promisifyStub = sinon.stub(); - t.context.promisifyStub - .withArgs(fs.readFile) - .returns(() => JSON.stringify({mavenSnapshotEndpointUrl: "__url__"})); - - t.context.promisifyStub.withArgs(fs.writeFile).returns(() => {}); - - t.context.config = await esmock.p("../../../../lib/cli/commands/config.js", { - "node:util": { - "promisify": t.context.promisifyStub - }, - }); + t.context.config = await esmock.p("../../../../lib/cli/commands/config.js"); }); test.afterEach.always((t) => { From c599a0ecc63ff5c85e998db857d468afb8b51d97 Mon Sep 17 00:00:00 2001 From: Yavor Ivanov Date: Wed, 19 Apr 2023 13:48:57 +0300 Subject: [PATCH 10/15] Fix lint issues --- test/lib/cli/commands/config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/lib/cli/commands/config.js b/test/lib/cli/commands/config.js index c0b89aec..2bd7c87a 100644 --- a/test/lib/cli/commands/config.js +++ b/test/lib/cli/commands/config.js @@ -1,7 +1,6 @@ import test from "ava"; import sinon from "sinon"; import esmock from "esmock"; -const {default: fs} = await import("graceful-fs"); function getDefaultArgv() { // This has been taken from the actual argv object yargs provides From c5d78c7e3ee7481ea5db5c17a47463181c3e55aa Mon Sep 17 00:00:00 2001 From: Yavor Ivanov Date: Wed, 19 Apr 2023 16:19:39 +0300 Subject: [PATCH 11/15] consume @ui5-project->Configuration properly --- lib/cli/commands/config.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/cli/commands/config.js b/lib/cli/commands/config.js index 9bdce824..34f81d1a 100644 --- a/lib/cli/commands/config.js +++ b/lib/cli/commands/config.js @@ -2,8 +2,7 @@ import path from "node:path"; import os from "node:os"; import chalk from "chalk"; import baseMiddleware from "../middlewares/base.js"; -// Internal module that is not exposed in package.json. Workaround in order to make it available for the CLI. -import Configuration from "../../../node_modules/@ui5/project/lib/config/Configuration.js"; +import Configuration from "@ui5/project/config/Configuration"; const ALLOWED_KEYS = ["mavenSnapshotEndpointUrl"]; const ui5RcFilePath = path.resolve(path.join(os.homedir(), ".ui5rc")); From 1785172bc5bf3ba7a7632146a968a863d565355b Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Fri, 21 Apr 2023 15:19:32 +0200 Subject: [PATCH 12/15] cli/base: Update error messages * No help is printed on yargs errors, so do not refer to it (also see https://github.com/SAP/ui5-cli/pull/256) * Only mention 'ui5 --help' --- lib/cli/base.js | 4 ++-- test/lib/cli/base.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/cli/base.js b/lib/cli/base.js index ab8fbc4e..83f0f991 100644 --- a/lib/cli/base.js +++ b/lib/cli/base.js @@ -4,7 +4,7 @@ import ConsoleWriter from "@ui5/logger/writers/Console"; export default function(cli) { cli.usage("Usage: ui5 [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", @@ -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); }); diff --git a/test/lib/cli/base.js b/test/lib/cli/base.js index 85954511..ae0d0650 100644 --- a/test/lib/cli/base.js +++ b/test/lib/cli/base.js @@ -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"); }); From 22cd468d2932d6c798a7687924e8de45d9f5e5c2 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Fri, 21 Apr 2023 17:13:01 +0200 Subject: [PATCH 13/15] cli/config: Update texts, fix edge cases, fix tests * Fix 'ui5 set ' without value or empty string to unset * Log everything to stderr, except machine parsable output like configuration values returned by 'ui5 list' and 'ui5 get ' * Do not mention location of ui5rc in config commands. In the future we might have multiple locations and @ui5/project/config/Configuration would take care of deciding where to read what from. The CLI should not have that knowledge * Move imports and variable definitions to handler in order to only execute them if the command is actually 'ui5 config' * Tests now mock Configuration in order to not modify the actual configuration of the user running the tests --- lib/cli/commands/config.js | 80 +++++++------ test/lib/cli/commands/config.js | 202 ++++++++++++++++++++++++++++---- 2 files changed, 223 insertions(+), 59 deletions(-) diff --git a/lib/cli/commands/config.js b/lib/cli/commands/config.js index 34f81d1a..d1846960 100644 --- a/lib/cli/commands/config.js +++ b/lib/cli/commands/config.js @@ -1,76 +1,88 @@ -import path from "node:path"; -import os from "node:os"; import chalk from "chalk"; +import process from "node:process"; import baseMiddleware from "../middlewares/base.js"; -import Configuration from "@ui5/project/config/Configuration"; - -const ALLOWED_KEYS = ["mavenSnapshotEndpointUrl"]; -const ui5RcFilePath = path.resolve(path.join(os.homedir(), ".ui5rc")); const configCommand = { command: "config", - describe: "Configures the `.ui5rc` file.", + describe: "Get and set UI5 Tooling configuration options", middlewares: [baseMiddleware], handler: handleConfig }; configCommand.builder = function(cli) { return cli - .command("set ", "Sets a property in `.ui5rc`", { + .demandCommand(1, "Command required. Available commands are 'set', 'get' and 'list'") + .command("set [value]", + "Set the value for a given configuration key. Provide no unset the configuration", { + handler: handleConfig, + builder: noop, + middlewares: [baseMiddleware], + }) + .command("get ", "Get the value for a given configuration key", { handler: handleConfig, builder: noop, middlewares: [baseMiddleware], }) - .command("get ", "Gets a property from `.ui5rc`", { + .command("list", "Display the current configuration", { handler: handleConfig, builder: noop, middlewares: [baseMiddleware], }) - .command("list", "List settings stored in `.ui5rc`", { - 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]; - if (key && !ALLOWED_KEYS.includes(key)) { - throw new Error(`The provided key is not part of the .ui5rc allowed options: ${ALLOWED_KEYS.join(", ")}`); + 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 (commandArgs.includes("list")) { - console.log(`Listing properties from ${chalk.dim(ui5RcFilePath)}: -${formatJsonForOutput(config.toJSON())}`); - } else if (commandArgs.includes("get")) { - console.log(`Getting property ${chalk.bold(key)} from ${chalk.dim(ui5RcFilePath)}: -${formatJsonForOutput(config.toJSON(), key)}`); - } else if (commandArgs.includes("set")) { - const jsonConfig = config.toJSON(); - if (value) { - jsonConfig[key] = value; - } else { - delete jsonConfig[key]; + 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 = ""; } - - console.log(`Set property ${chalk.bold(key)} into ${chalk.dim(ui5RcFilePath)}: + 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) - .map( - (key) => ` ${key} = ${config[key]}\n` - ).join(""); + .filter((key) => config[key] !== undefined) // Don't print undefined config values + .map((key) => { + return ` ${key} = ${config[key]}\n`; + }).join(""); } export default configCommand; diff --git a/test/lib/cli/commands/config.js b/test/lib/cli/commands/config.js index 2bd7c87a..3b1578f6 100644 --- a/test/lib/cli/commands/config.js +++ b/test/lib/cli/commands/config.js @@ -1,6 +1,7 @@ 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 @@ -30,9 +31,18 @@ test.beforeEach(async (t) => { t.context.argv = getDefaultArgv(); t.context.builder = sinon.stub().resolves(); - t.context.consoleLog = sinon.stub(console, "log"); + t.context.stdoutWriteStub = sinon.stub(process.stdout, "write"); + t.context.stderrWriteStub = sinon.stub(process.stderr, "write"); + t.context.configToJson = sinon.stub().returns({}); - t.context.config = await esmock.p("../../../../lib/cli/commands/config.js"); + 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) => { @@ -41,68 +51,210 @@ test.afterEach.always((t) => { }); test.serial("ui5 config list", async (t) => { - const {config, argv, consoleLog} = t.context; + 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.true(consoleLog.getCall(0).args[0].startsWith("Listing properties from "), "Lists properties via console.log"); + 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, consoleLog} = t.context; + 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.true( - consoleLog.getCall(0) - .args[0].startsWith("Getting property "), - "Getting key from config file" - ); + t.is(stdoutWriteStub.firstCall.firstArg, "\n", "Logged no value to console"); }); test.serial("ui5 config set", async (t) => { - const {config, argv, consoleLog} = t.context; + 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.true( - consoleLog.getCall(0) - .args[0].startsWith("Set property "), - "Setting value into file" - ); + 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 value should delete", 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 empty value should delete", async (t) => { - const {config, argv, consoleLog} = t.context; + const {config, argv, stderrWriteStub, stdoutWriteStub, Configuration} = t.context; argv["_"] = ["set"]; argv["key"] = "mavenSnapshotEndpointUrl"; - argv["value"] = ""; + argv["value"] = ""; // Simulating empty value provided to Yargs using quotes "" await config.handler(argv); - t.true( - consoleLog.getCall(0) - .args[0].startsWith("Set property "), - "Setting value into file" - ); + 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", 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", 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["_"] = ["set"]; + argv["_"] = ["get"]; argv["key"] = "_invalid_key_"; argv["value"] = "https://_snapshot_endpoint_"; await t.throwsAsync(config.handler(argv), { message: - "The provided key is not part of the .ui5rc allowed options: mavenSnapshotEndpointUrl", + "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'", + }); +}); From 19122b51e85ee619c39c6c2421e46a4dd9c4da0a Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Mon, 24 Apr 2023 15:01:19 +0200 Subject: [PATCH 14/15] cli/config: Update 'set' command description --- lib/cli/commands/config.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/cli/commands/config.js b/lib/cli/commands/config.js index d1846960..b96243b3 100644 --- a/lib/cli/commands/config.js +++ b/lib/cli/commands/config.js @@ -12,12 +12,12 @@ const configCommand = { configCommand.builder = function(cli) { return cli .demandCommand(1, "Command required. Available commands are 'set', 'get' and 'list'") - .command("set [value]", - "Set the value for a given configuration key. Provide no unset the configuration", { - handler: handleConfig, - builder: noop, - middlewares: [baseMiddleware], - }) + .command("set [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 ", "Get the value for a given configuration key", { handler: handleConfig, builder: noop, From 04fdf8d1acf1155f737c892f13131be0b71ea0b3 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Tue, 25 Apr 2023 10:01:03 +0200 Subject: [PATCH 15/15] [INTERNAL] Apply suggestions from UA review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Günter Klatt <57760635+KlattG@users.noreply.github.com> --- lib/cli/commands/config.js | 2 +- test/lib/cli/commands/config.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/cli/commands/config.js b/lib/cli/commands/config.js index b96243b3..bfdc810a 100644 --- a/lib/cli/commands/config.js +++ b/lib/cli/commands/config.js @@ -11,7 +11,7 @@ const configCommand = { configCommand.builder = function(cli) { return cli - .demandCommand(1, "Command required. Available commands are 'set', 'get' and 'list'") + .demandCommand(1, "Command required. Available commands are 'set', 'get', and 'list'") .command("set [value]", "Set the value for a given configuration key. " + "Clear an existing configuration by omitting the value", { handler: handleConfig, diff --git a/test/lib/cli/commands/config.js b/test/lib/cli/commands/config.js index 3b1578f6..85ee69ec 100644 --- a/test/lib/cli/commands/config.js +++ b/test/lib/cli/commands/config.js @@ -138,7 +138,7 @@ test.serial("ui5 config set", async (t) => { }, "Configuration#toFile got called with expected argument"); }); -test.serial("ui5 config set without value should delete", async (t) => { +test.serial("ui5 config set without a value should delete the configuration", async (t) => { const {config, argv, stderrWriteStub, stdoutWriteStub, Configuration} = t.context; argv["_"] = ["set"]; @@ -157,7 +157,7 @@ test.serial("ui5 config set without value should delete", async (t) => { }, "Configuration#toFile got called with expected argument"); }); -test.serial("ui5 config set empty value should delete", async (t) => { +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"]; @@ -176,7 +176,7 @@ test.serial("ui5 config set empty value should delete", async (t) => { }, "Configuration#toFile got called with expected argument"); }); -test.serial("ui5 config set null should update", async (t) => { +test.serial("ui5 config set null should update the configuration", async (t) => { const {config, argv, stderrWriteStub, stdoutWriteStub, Configuration} = t.context; argv["_"] = ["set"]; @@ -199,7 +199,7 @@ test.serial("ui5 config set null should update", async (t) => { }, "Configuration#toFile got called with expected argument"); }); -test.serial("ui5 config set false should update", async (t) => { +test.serial("ui5 config set false should update the configuration", async (t) => { const {config, argv, stderrWriteStub, stdoutWriteStub, Configuration} = t.context; argv["_"] = ["set"];