diff --git a/lib/config/Configuration.js b/lib/config/Configuration.js new file mode 100644 index 000000000..622e7c9d7 --- /dev/null +++ b/lib/config/Configuration.js @@ -0,0 +1,93 @@ +import path from "node:path"; +import os from "node:os"; + +/** + * Provides basic configuration settings for @ui5/project/ui5Framework/* resolvers. + * Reads/writes configuration from/to ~/.ui5rc + * + * @public + * @class + * @alias @ui5/project/config/Configuration + */ +class Configuration { + #mavenSnapshotEndpointUrl; + + /** + * @param {object} configuration + * @param {string} [configuration.mavenSnapshotEndpointUrl] + */ + constructor({mavenSnapshotEndpointUrl}) { + this.#mavenSnapshotEndpointUrl = mavenSnapshotEndpointUrl; + } + + /** + * Maven Repository Snapshot URL. + * Used to download artifacts and packages from Maven's build-snapshots URL. + * + * @public + * @returns {string} + */ + getMavenSnapshotEndpointUrl() { + return this.#mavenSnapshotEndpointUrl; + } + + /** + * @public + * @returns {object} The configuration in a JSON format + */ + toJSON() { + return { + mavenSnapshotEndpointUrl: this.#mavenSnapshotEndpointUrl, + }; + } + + /** + * Creates Configuration from a JSON file + * + * @public + * @static + * @param {string} [filePath="~/.ui5rc"] Path to configuration JSON file + * @returns {Promise<@ui5/project/config/Configuration>} Configuration instance + */ + static async fromFile(filePath) { + filePath = 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 new Configuration(config); + } + + /** + * Saves Configuration to a JSON file + * + * @public + * @static + * @param {@ui5/project/config/Configuration} config Configuration to save + * @param {string} [filePath="~/.ui5rc"] Path to configuration JSON file + * @returns {Promise} + */ + static async toFile(config, filePath) { + filePath = 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.toJSON())); + } +} + +export default Configuration; diff --git a/lib/ui5Framework/Sapui5MavenSnapshotResolver.js b/lib/ui5Framework/Sapui5MavenSnapshotResolver.js index 0d22e23c4..04dab9137 100644 --- a/lib/ui5Framework/Sapui5MavenSnapshotResolver.js +++ b/lib/ui5Framework/Sapui5MavenSnapshotResolver.js @@ -227,7 +227,7 @@ class Sapui5MavenSnapshotResolver extends AbstractResolver { if (ok) { log.info(`Using Maven snapshot endpoint URL resolved from Maven configuration file: ${url}`); log.info(`Consider persisting this choice by executing the following command: ` + - `ui5 config set snapshotEndpointUrl ${url}`); + `ui5 config set mavenSnapshotEndpointUrl ${url}`); } else { log.verbose(`User rejected usage of the resolved URL`); url = null; diff --git a/lib/ui5Framework/maven/Installer.js b/lib/ui5Framework/maven/Installer.js index 53a0a614b..ff47ba55f 100644 --- a/lib/ui5Framework/maven/Installer.js +++ b/lib/ui5Framework/maven/Installer.js @@ -70,7 +70,7 @@ class Installer extends AbstractInstaller { reject(new Error( `Installer: Missing or empty Maven repository URL for snapshot consumption. ` + `Please configure the correct URL using the following command: ` + - `ui5 config set snapshotEndpointUrl https://registry.corp/vendor/build-snapshots/`)); + `ui5 config set mavenSnapshotEndpointUrl https://registry.corp/vendor/build-snapshots/`)); } resolve(new Registry({endpointUrl: snapshotEndpointUrl})); diff --git a/package.json b/package.json index 9b2a1cb00..a1f0ccdde 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ ], "type": "module", "exports": { + "./config/Configuration": "./lib/config/Configuration.js", "./specifications/Specification": "./lib/specifications/Specification.js", "./specifications/SpecificationVersion": "./lib/specifications/SpecificationVersion.js", "./ui5Framework/Sapui5MavenSnapshotResolver": "./lib/ui5Framework/Sapui5MavenSnapshotResolver.js", diff --git a/test/lib/config/Configuration.js b/test/lib/config/Configuration.js new file mode 100644 index 000000000..d6152220d --- /dev/null +++ b/test/lib/config/Configuration.js @@ -0,0 +1,122 @@ +import test from "ava"; +import sinonGlobal from "sinon"; +import esmock from "esmock"; + +test.beforeEach(async (t) => { + const sinon = t.context.sinon = sinonGlobal.createSandbox(); + + t.context.homedirStub = sinon.stub().returns("~"); + t.context.promisifyStub = sinon.stub(); + t.context.resolveStub = sinon.stub().callsFake((path) => path); + t.context.joinStub = sinon.stub().callsFake((...args) => args.join("/")); + t.context.Configuration = await esmock.p("../../../lib/config/Configuration.js", { + "node:path": { + resolve: t.context.resolveStub, + join: t.context.joinStub + }, + "node:util": { + "promisify": t.context.promisifyStub + }, + "node:os": { + "homedir": t.context.homedirStub + } + }); +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); + esmock.purge(t.context.Configuration); +}); + +test.serial("Build configuration with defaults", (t) => { + const {Configuration} = t.context; + + const config = new Configuration({}); + + t.deepEqual(config.toJSON(), { + mavenSnapshotEndpointUrl: undefined + }); +}); + + +test.serial("Overwrite defaults defaults", (t) => { + const {Configuration} = t.context; + + const params = { + mavenSnapshotEndpointUrl: "https://snapshot.url" + }; + + const config = new Configuration(params); + + t.deepEqual(config.toJSON(), params); +}); + +test.serial("Check getters", (t) => { + const {Configuration} = t.context; + + const params = { + mavenSnapshotEndpointUrl: "https://snapshot.url" + }; + + const config = new Configuration(params); + + t.is(config.getMavenSnapshotEndpointUrl(), params.mavenSnapshotEndpointUrl); +}); + + +test.serial("fromFile", async (t) => { + const fromFile = t.context.Configuration.fromFile; + const {promisifyStub, sinon} = t.context; + + const ui5rcContents = { + mavenSnapshotEndpointUrl: "https://snapshot.url" + }; + const responseStub = sinon.stub().resolves(JSON.stringify(ui5rcContents)); + promisifyStub.callsFake(() => responseStub); + + const config = await fromFile("/custom/path/.ui5rc"); + + t.deepEqual(config.toJSON(), ui5rcContents); +}); + +test.serial("fromFile: configuration file not found- fallback to default config", async (t) => { + const {promisifyStub, sinon, Configuration} = t.context; + const fromFile = Configuration.fromFile; + + const responseStub = sinon.stub().throws({code: "ENOENT"}); + promisifyStub.callsFake(() => responseStub); + + const config = await fromFile("/non-existing/path/.ui5rc"); + + t.is(config instanceof Configuration, true, "Created a default configuration"); + t.is(config.getMavenSnapshotEndpointUrl(), undefined, "Dafault settings"); +}); + +test.serial("fromFile: throws", async (t) => { + const fromFile = t.context.Configuration.fromFile; + const {promisifyStub, sinon} = t.context; + + const responseStub = sinon.stub().throws(new Error("Error")); + promisifyStub.callsFake(() => responseStub); + + await t.throwsAsync(fromFile("/non-existing/path/.ui5rc"), { + message: "Error" + }); +}); + +test.serial("toFile", async (t) => { + const {promisifyStub, sinon, Configuration} = t.context; + const toFile = Configuration.toFile; + + const writeStub = sinon.stub().resolves(); + promisifyStub.callsFake(() => writeStub); + + const config = new Configuration({mavenSnapshotEndpointUrl: "https://registry.corp/vendor/build-snapshots/"}); + await toFile(config, "/path/to/save/.ui5rc"); + + t.deepEqual( + writeStub.getCall(0).args, + ["/path/to/save/.ui5rc", JSON.stringify(config.toJSON())], + "Write config to path" + ); +}); diff --git a/test/lib/package-exports.js b/test/lib/package-exports.js index 9f91f1e10..c3c7284d6 100644 --- a/test/lib/package-exports.js +++ b/test/lib/package-exports.js @@ -13,11 +13,12 @@ test("export of package.json", (t) => { // Check number of definied exports test("check number of exports", (t) => { const packageJson = require("@ui5/project/package.json"); - t.is(Object.keys(packageJson.exports).length, 11); + t.is(Object.keys(packageJson.exports).length, 12); }); // Public API contract (exported modules) [ + "config/Configuration", "specifications/Specification", "specifications/SpecificationVersion", "ui5Framework/Openui5Resolver", diff --git a/test/lib/ui5framework/Sapui5MavenSnapshotResolver.js b/test/lib/ui5framework/Sapui5MavenSnapshotResolver.js index f14890426..ae11523bc 100644 --- a/test/lib/ui5framework/Sapui5MavenSnapshotResolver.js +++ b/test/lib/ui5framework/Sapui5MavenSnapshotResolver.js @@ -282,7 +282,7 @@ test.serial("_resolveSnapshotEndpointUrlFromMaven", async (t) => { "Using Maven snapshot endpoint URL resolved from Maven configuration file: /build-snapshots/"); t.is(loggerInfo.getCall(1).args[0], "Consider persisting this choice by executing the following command: " + - "ui5 config set snapshotEndpointUrl /build-snapshots/"); + "ui5 config set mavenSnapshotEndpointUrl /build-snapshots/"); }); test.serial("_resolveSnapshotEndpointUrlFromMaven fails", async (t) => {