diff --git a/lib/graph/Module.js b/lib/graph/Module.js index 9609a49d4..d0de6e5ae 100644 --- a/lib/graph/Module.js +++ b/lib/graph/Module.js @@ -27,6 +27,16 @@ function clone(obj) { * @alias @ui5/project/graph/Module */ class Module { + /** + * Regular expression to identify environment variables in strings. + * Environment variables have to be prefixed with "env:", e.g. "env:Path". + * + * @private + * @static + * @readonly + */ + static _ENVIRONMENT_VARIABLE_REGEX = /env:\S+/g; + /** * @param {object} parameters Module parameters * @param {string} parameters.id Unique ID for the module @@ -339,6 +349,25 @@ class Module { return configs; } + try { + for (const config of configs) { + if (config.customConfiguration !== undefined) { + this._normalizeConfigValue(config, "customConfiguration"); + } + if (config.builder?.customTasks !== undefined) { + this._normalizeConfigValue(config.builder, "customTasks"); + } + if (config.server?.customMiddleware !== undefined) { + this._normalizeConfigValue(config.server, "customMiddleware"); + } + } + } catch (err) { + throw new Error( + "Failed to parse configuration for project " + + `${this.getId()} at '${configPath}'\nError: ${err.message}` + ); + } + // Validate found configurations with schema // Validation is done again in the Specification class. But here we can reference the YAML file // which adds helpful information like the line number @@ -406,6 +435,45 @@ class Module { return config; } + /** + * Normalizes the config value at object[key]. If the config value is an object / array, + * this method will descend depth-first on its properties / elements. If the config value is a string + * and contains any environment variables, they will be filled in at this point. + * + * @private + * @param {(object|any[])} object An object or array + * @param {(string|number)} key A key of the object or an index of the array + */ + _normalizeConfigValue(object, key) { + let value = object[key]; + switch (typeof value) { + case "string": { + value = value.replace(Module._ENVIRONMENT_VARIABLE_REGEX, (substring) => { + const envVarName = substring.slice(4); + if (process.env[envVarName] === undefined) { + throw new Error(`The environment variable '${envVarName}' is not defined`); + } + return process.env[envVarName]; + }); + object[key] = value; + break; + } + case "object": { + if (value === null) break; + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + this._normalizeConfigValue(value, i); + } + } else { + for (const key of Object.keys(value)) { + this._normalizeConfigValue(value, key); + } + } + break; + } + } + } + /** * Resource Access */ diff --git a/test/fixtures/application.a/ui5-test-env.yaml b/test/fixtures/application.a/ui5-test-env.yaml new file mode 100644 index 000000000..51ac6924d --- /dev/null +++ b/test/fixtures/application.a/ui5-test-env.yaml @@ -0,0 +1,25 @@ +--- +specVersion: "3.0" +type: application +metadata: + name: application.a +customConfiguration: + stringWithEnvVar: env:testEnvVarForString + objectWithEnvVar: + someKey: env:testEnvVarForObject + arrayWithEnvVar: + - a + - env:testEnvVarForArray + - c +builder: + customTasks: + - name: foo + afterTask: replaceVersion + configuration: + builderWithEnvVar: env:testEnvVarForBuilder +server: + customMiddleware: + - name: bar + afterMiddleware: compression + configuration: + serverWithEnvVar: env:testEnvVarForServer diff --git a/test/lib/graph/Module.js b/test/lib/graph/Module.js index c87aac185..d9a5824dd 100644 --- a/test/lib/graph/Module.js +++ b/test/lib/graph/Module.js @@ -408,6 +408,77 @@ test("Legacy patches are applied", async (t) => { .map(testLegacyLibrary)); }); +test.serial("Environment variables in configuration", async (t) => { + const testEnvVars = [ + "testEnvVarForString", + "testEnvVarForObject", + "testEnvVarForArray", + "testEnvVarForBuilder", + "testEnvVarForServer", + ].map((testEnvVar, index) => { + const wrapper = { + name: testEnvVar, + oldValue: process.env[testEnvVar], + newValue: `testValue${index}`, + }; + process.env[testEnvVar] = wrapper.newValue; + return wrapper; + }); + try { + const ui5Module = new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configPath: "ui5-test-env.yaml", + }); + const {project} = await ui5Module.getSpecifications(); + t.deepEqual( + project.getCustomConfiguration(), + { + stringWithEnvVar: testEnvVars[0].newValue, + objectWithEnvVar: { + someKey: testEnvVars[1].newValue, + }, + arrayWithEnvVar: ["a", testEnvVars[2].newValue, "c"], + }, + "Environment variables in custom configuration are filled in" + ); + t.deepEqual( + project.getCustomTasks()?.[0]?.configuration, + { + builderWithEnvVar: testEnvVars[3].newValue, + }, + "Environment variable in builder custom tasks is filled in" + ); + t.deepEqual( + project.getCustomMiddleware()?.[0]?.configuration, + { + serverWithEnvVar: testEnvVars[4].newValue, + }, + "Environment variable in server custom middleware is filled in" + ); + } finally { + // Reset all env vars back to their value previous to testing + testEnvVars.forEach((wrapper) => { + if (wrapper.oldValue === undefined) { + delete process.env[wrapper.name]; + } else { + process.env[wrapper.name] = wrapper.oldValue; + } + }); + } +}); + +test.serial("Undefined environment variables in configuration", async (t) => { + const ui5Module = new Module({ + id: "application.a.id", + version: "1.0.0", + modulePath: applicationAPath, + configPath: "ui5-test-env.yaml", + }); + await t.throwsAsync(() => ui5Module.getSpecifications()); +}); + test("Invalid configuration in file", async (t) => { const ui5Module = new Module({ id: "application.a.id",