From 30ed97c05261bdea760c069f14fe1f79605a0957 Mon Sep 17 00:00:00 2001 From: Max Reichmann Date: Fri, 2 Oct 2026 14:47:16 +0200 Subject: [PATCH 1/5] feat: Offer configPath property to workspace configs --- .../lib/validation/schema/ui5-workspace.json | 23 +++++++++++++++++-- packages/project/test/lib/graph/Workspace.js | 13 +++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/project/lib/validation/schema/ui5-workspace.json b/packages/project/lib/validation/schema/ui5-workspace.json index d4cd46c1946..f7a4bf76826 100644 --- a/packages/project/lib/validation/schema/ui5-workspace.json +++ b/packages/project/lib/validation/schema/ui5-workspace.json @@ -9,7 +9,7 @@ "additionalProperties": false, "properties": { "specVersion": { - "enum": ["workspace/1.0"], + "enum": ["workspace/1.1", "workspace/1.0"], "errorMessage": "Unsupported \"specVersion\"\nYour UI5 CLI installation might be outdated.\nSupported specification versions: \"workspace/1.0\"\nFor details, see: https://ui5.github.io/cli/stable/pages/Workspace/#workspace-specification-versions" }, "metadata": { @@ -19,6 +19,23 @@ "$ref": "#/definitions/dependencyManagement" } }, + "if": { + "properties": {"specVersion": {"const": "workspace/1.0"}} + }, + "then": { + "properties": { + "dependencyManagement": { + "properties": { + "resolutions": { + "items": { + "not": {"required": ["configPath"]}, + "errorMessage": "\"configPath\" is only supported for specVersion \"workspace/1.1\" and higher" + } + } + } + } + } + }, "definitions": { "metadata": { "type": "object", @@ -42,13 +59,15 @@ "properties": { "resolutions": { "type": "array", - "items": { "type": "object", "additionalProperties": false, "properties": { "path": { "type": "string" + }, + "configPath": { + "type": "string" } } } diff --git a/packages/project/test/lib/graph/Workspace.js b/packages/project/test/lib/graph/Workspace.js index 6e5b586e138..d7cc2e9399b 100644 --- a/packages/project/test/lib/graph/Workspace.js +++ b/packages/project/test/lib/graph/Workspace.js @@ -473,3 +473,16 @@ test("Missing parameters", (t) => { message: "Could not create Workspace: Missing or empty parameter 'configuration'" }, "Threw with expected error message"); }); + +test("Basic validation of workspace with specified configPath for dependency", (t) => { + // TODO: add test which passes the following yaml example: + // specVersion: workspace/1.1 + // metadata: + // name: default + // dependencyManagement: + // resolutions: + // - path: ../my-reuse-library + // configPath: ui5-mock.yaml + + +}); From d006aa8cece52c698ba5c0f1b4ade427ea07617f Mon Sep 17 00:00:00 2001 From: Max Reichmann Date: Thu, 8 Oct 2026 15:08:45 +0200 Subject: [PATCH 2/5] feat(project): Add logic for configPath parameter + Add tests --- packages/project/lib/graph/Workspace.js | 10 ++- .../library.d/custom-config-path.yaml | 10 +++ packages/project/test/lib/graph/Workspace.js | 74 ++++++++++++++++--- 3 files changed, 79 insertions(+), 15 deletions(-) create mode 100644 packages/project/test/fixtures/library.d/custom-config-path.yaml diff --git a/packages/project/lib/graph/Workspace.js b/packages/project/lib/graph/Workspace.js index d39629a1491..57c4c233f92 100644 --- a/packages/project/lib/graph/Workspace.js +++ b/packages/project/lib/graph/Workspace.js @@ -144,7 +144,7 @@ class Workspace { `Missing property 'path' in dependency resolution configuration of workspace ${this.getName()}`); } return await this._getModulesFromPath( - this.#cwd, resolutionConfig.path); + this.#cwd, resolutionConfig.path, resolutionConfig.configPath); })); // Flatten array since package-workspaces might have resolved to multiple modules for a single resolution @@ -175,7 +175,7 @@ class Workspace { }; } - async _getModulesFromPath(cwd, relPath, failOnMissingFiles = true) { + async _getModulesFromPath(cwd, relPath, configPath, failOnMissingFiles = true) { const nodePath = path.join(cwd, relPath); if (this.#visitedNodePaths.has(nodePath)) { log.verbose(`Module located at ${nodePath} has already been visited`); @@ -235,7 +235,8 @@ class Workspace { const resolvedModules = new Map(); await Promise.all(searchPaths.map(async (pkgPath) => { - const modules = await this._getModulesFromPath(nodePath, pkgPath, staticPatterns.includes(pkgPath)); + const modules = + await this._getModulesFromPath(nodePath, pkgPath, configPath, staticPatterns.includes(pkgPath)); modules.forEach((module) => { const id = module.getId(); if (!resolvedModules.get(id)) { @@ -248,7 +249,8 @@ class Workspace { return [new Module({ id: pkg.name, version: pkg.version, - modulePath: nodePath + modulePath: nodePath, + configPath: configPath, })]; } } diff --git a/packages/project/test/fixtures/library.d/custom-config-path.yaml b/packages/project/test/fixtures/library.d/custom-config-path.yaml new file mode 100644 index 00000000000..ca7c0921253 --- /dev/null +++ b/packages/project/test/fixtures/library.d/custom-config-path.yaml @@ -0,0 +1,10 @@ +specVersion: "2.3" +type: library +metadata: + name: library.d + copyright: A custom config (called by configPath paramater) +resources: + configuration: + paths: + src: main/src + test: main/test diff --git a/packages/project/test/lib/graph/Workspace.js b/packages/project/test/lib/graph/Workspace.js index d7cc2e9399b..c78ac320a78 100644 --- a/packages/project/test/lib/graph/Workspace.js +++ b/packages/project/test/lib/graph/Workspace.js @@ -14,9 +14,9 @@ const collectionBLibraryA = path.join(__dirname, "..", "..", "fixtures", "collec const collectionBLibraryB = path.join(__dirname, "..", "..", "fixtures", "collection.b", "library.b"); const collectionBLibraryC = path.join(__dirname, "..", "..", "fixtures", "collection.b", "library.c"); -function createWorkspaceConfig({dependencyManagement}) { +function createWorkspaceConfig({dependencyManagement, specVersion = "workspace/1.0"}) { return { - specVersion: "workspace/1.0", + specVersion, metadata: { name: "workspace-name" }, @@ -474,15 +474,67 @@ test("Missing parameters", (t) => { }, "Threw with expected error message"); }); -test("Basic validation of workspace with specified configPath for dependency", (t) => { - // TODO: add test which passes the following yaml example: - // specVersion: workspace/1.1 - // metadata: - // name: default - // dependencyManagement: - // resolutions: - // - path: ../my-reuse-library - // configPath: ui5-mock.yaml +test("workspace/1.1: Basic validation with specified configPath for dependency", async (t) => { + const workspace = new t.context.Workspace({ + cwd: __dirname, + configuration: createWorkspaceConfig({ + specVersion: "workspace/1.1", + dependencyManagement: { + resolutions: [{ + path: "../../fixtures/library.d", + configPath: "custom-config-path.yaml" // Custom configPath + }, { + path: "../../fixtures/library.e", + // no custom yaml -> resorts to default: ui5.yaml + }] + } + }) + }); + + const {projectNameMap} = await workspace._getResolvedModules(); + t.deepEqual(Array.from(projectNameMap.keys()).sort(), ["library.d", "library.e"], "Correct project name keys"); + t.is(projectNameMap.get("library.d")._configPath, "custom-config-path.yaml"); + t.is(projectNameMap.get("library.e")._configPath, "ui5.yaml"); +}); + +test("workspace/1.1: Custom configPath for dependency but specVersion is workspace/1.0", async (t) => { + const workspace = new t.context.Workspace({ + cwd: __dirname, + configuration: createWorkspaceConfig({ + specVersion: "workspace/1.0", + dependencyManagement: { + resolutions: [{ + path: "../../fixtures/library.d", + configPath: "custom-config-path.yaml" // Custom configPath + }, { + path: "../../fixtures/library.e", + }] + } + }) + }); + + const err = await t.throwsAsync(async () => await workspace._getResolvedModules()); + t.is(err.message.includes("\"configPath\" is only supported for specVersion \"workspace/1.1\" and higher"), true); +}); +test("workspace/1.1: Custom configPath for dependency does not exist", async (t) => { + const workspace = new t.context.Workspace({ + cwd: __dirname, + configuration: createWorkspaceConfig({ + specVersion: "workspace/1.1", + dependencyManagement: { + resolutions: [{ + path: "../../fixtures/library.d", + configPath: "not-existing-config-path.yaml" // Custom configPath + }, { + path: "../../fixtures/library.e", + // no custom yaml -> resorts to default: ui5.yaml + }] + } + }) + }); + const err = await t.throwsAsync(async () => await workspace._getResolvedModules()); + t.is(err.message.includes("Failed to read configuration for module library.d: " + + "Could not find configuration file in module at path 'not-existing-config-path.yaml'"), true); }); From 628498b0f9e9d6b685a9e7e7125c9bb6ceb0b815 Mon Sep 17 00:00:00 2001 From: Max Reichmann Date: Thu, 8 Oct 2026 16:08:25 +0200 Subject: [PATCH 3/5] docs: Adjust Workspace.md page with configPath property --- .../documentation/docs/pages/Workspace.md | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/internal/documentation/docs/pages/Workspace.md b/internal/documentation/docs/pages/Workspace.md index 730e9420284..a588a33f721 100644 --- a/internal/documentation/docs/pages/Workspace.md +++ b/internal/documentation/docs/pages/Workspace.md @@ -5,7 +5,7 @@ ::: details ```yaml title="ui5-workspace.yaml" -specVersion: workspace/1.0 +specVersion: workspace/1.1 metadata: name: default dependencyManagement: @@ -23,14 +23,14 @@ Workspaces are typically configured in a `ui5-workspace.yaml` file, located next ::: details Example ```yaml title="ui5-workspace.yaml" -specVersion: workspace/1.0 +specVersion: workspace/1.1 metadata: name: default dependencyManagement: resolutions: - path: ../heavy.library --- -specVersion: workspace/1.0 +specVersion: workspace/1.1 metadata: name: extended dependencyManagement: @@ -52,14 +52,14 @@ This concept has been discussed in an RFC: [RFC 0006 Local Dependency Resolution A UI5 Workspace configuration must define a specification version (`specVersion`) compatible with its configuration. For more information, see [Workspace Specification Versions](#workspace-specification-versions). ```yaml -specVersion: "workspace/1.0" +specVersion: "workspace/1.1" ``` ### Metadata ::: details Example ```yaml -specVersion: workspace/1.0 +specVersion: workspace/1.1 metadata: name: dolphin ``` @@ -91,7 +91,7 @@ UI5 Workspace configurations allow to influence the dependency resolution when w ::: details ```yaml -specVersion: workspace/1.0 +specVersion: workspace/1.1 metadata: name: dolphin dependencyManagement: @@ -119,11 +119,24 @@ Paths must be written in POSIX (i.e. using only forward slashes `/` as path segm Note that this configuration only affects the resolution of dependencies which have already been found during the regular dependency resolution process of a project. For example, if a workspace resolution path resolves to a project that would otherwise not be part of the dependency tree of the current root project, it will not be added to the dependency tree. Also, transitive dependencies of resolved projects are not being followed. +#### configPath + +*Available since `workspace/1.1`* + +```yaml +dependencyManagement: + resolutions: + - path: ../test.library + configPath: custom-config.yaml +``` + +By default, UI5 CLI looks for a `ui5.yaml` at the root of the resolved module. The optional `configPath` property allows pointing to a different configuration file within the module directory. The path must be written in POSIX notation and must be **relative to the resolution `path`**. + ## Workspace Specification Versions A workspace configuration must define a Specification Version by setting the `specVersion` property. UI5 CLI uses this information to detect whether the currently installed version is compatible with a workspace's configuration. ```yaml -specVersion: "workspace/1.0" +specVersion: "workspace/1.1" [...] ``` @@ -142,8 +155,15 @@ Unless otherwise noted in the table below, UI5 CLI modules are backward-compatib Version | UI5 CLI Release --- | --- +**`workspace/1.1`** | v5.0.0+ and v4.1.0+ **`workspace/1.0`** | v3.0.0+ +### Specification Version `workspace/1.1` + +Added [`configPath`](#configpath) property to resolution entries. + +Specification Version `workspace/1.1` configurations are compatible with [UI5 CLI](https://github.com/SAP/ui5-cli) v5.0.0 and above, as well as v4.1.0 and above. + ### Specification Version `workspace/1.0` Initial version. From d7c329f87864b0589e6ab60cf1f446df6b848e5e Mon Sep 17 00:00:00 2001 From: Max Reichmann Date: Fri, 9 Oct 2026 12:02:58 +0200 Subject: [PATCH 4/5] refactor: Improve error handling when `path` is absolute + Forbid absolute paths for `configPath` property + Allow relative paths for `configPath` which are out-of-root (resolution dir) --- packages/project/lib/graph/Module.js | 2 +- packages/project/lib/graph/Workspace.js | 10 +++ .../library.e/ui5-library.d-config.yaml | 11 +++ packages/project/test/lib/graph/Workspace.js | 77 +++++++++++++++++-- 4 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 packages/project/test/fixtures/library.e/ui5-library.d-config.yaml diff --git a/packages/project/lib/graph/Module.js b/packages/project/lib/graph/Module.js index 5130ef2acf0..c7b9b5c5e08 100644 --- a/packages/project/lib/graph/Module.js +++ b/packages/project/lib/graph/Module.js @@ -69,7 +69,7 @@ class Module { this._id = id; this._version = version; this._modulePath = modulePath; - this._configPath = configPath || DEFAULT_CONFIG_PATH; + this._configPath = configPath ? path.resolve(modulePath, configPath) : DEFAULT_CONFIG_PATH; this._dependencies = Object.create(null); if (!Array.isArray(configuration)) { diff --git a/packages/project/lib/graph/Workspace.js b/packages/project/lib/graph/Workspace.js index 57c4c233f92..8add41dcc3e 100644 --- a/packages/project/lib/graph/Workspace.js +++ b/packages/project/lib/graph/Workspace.js @@ -143,6 +143,16 @@ class Workspace { throw new Error( `Missing property 'path' in dependency resolution configuration of workspace ${this.getName()}`); } + if (path.isAbsolute(resolutionConfig.path)) { + throw new Error( + `Absolute resolution path (${resolutionConfig.path}) is not supported in dependency resolution ` + + `configuration of workspace ${this.getName()}`); + } + if (resolutionConfig.configPath && path.isAbsolute(resolutionConfig.configPath)) { + throw new Error( + `Absolute configPath (${resolutionConfig.configPath}) is not supported in dependency resolution ` + + `configuration of workspace ${this.getName()}`); + } return await this._getModulesFromPath( this.#cwd, resolutionConfig.path, resolutionConfig.configPath); })); diff --git a/packages/project/test/fixtures/library.e/ui5-library.d-config.yaml b/packages/project/test/fixtures/library.e/ui5-library.d-config.yaml new file mode 100644 index 00000000000..a9f1cdca018 --- /dev/null +++ b/packages/project/test/fixtures/library.e/ui5-library.d-config.yaml @@ -0,0 +1,11 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d + copyright: This is a config for another library (outside of this dir) +resources: + configuration: + paths: + src: main/src + test: main/test diff --git a/packages/project/test/lib/graph/Workspace.js b/packages/project/test/lib/graph/Workspace.js index c78ac320a78..0d3dac931b6 100644 --- a/packages/project/test/lib/graph/Workspace.js +++ b/packages/project/test/lib/graph/Workspace.js @@ -474,6 +474,23 @@ test("Missing parameters", (t) => { }, "Threw with expected error message"); }); +test("Absolute resolution path", async (t) => { + const workspace = new t.context.Workspace({ + cwd: __dirname, + configuration: createWorkspaceConfig({ + dependencyManagement: { + resolutions: [{ + path: path.resolve(__dirname, "../../fixtures/library.d") + }] + } + }) + }); + + const err = await t.throwsAsync(async () => await workspace._getResolvedModules()); + t.is(err.message.includes(`Absolute resolution path (${path.resolve(__dirname, "../../fixtures/library.d")}) `+ + `is not supported in dependency resolution configuration of workspace ${workspace.getName()}`), true); +}); + test("workspace/1.1: Basic validation with specified configPath for dependency", async (t) => { const workspace = new t.context.Workspace({ cwd: __dirname, @@ -493,7 +510,10 @@ test("workspace/1.1: Basic validation with specified configPath for dependency", const {projectNameMap} = await workspace._getResolvedModules(); t.deepEqual(Array.from(projectNameMap.keys()).sort(), ["library.d", "library.e"], "Correct project name keys"); - t.is(projectNameMap.get("library.d")._configPath, "custom-config-path.yaml"); + // Custom config: + t.is(projectNameMap.get("library.d")._configPath, + path.resolve(__dirname, "../../fixtures/library.d/custom-config-path.yaml")); + // Default config: t.is(projectNameMap.get("library.e")._configPath, "ui5.yaml"); }); @@ -517,6 +537,25 @@ test("workspace/1.1: Custom configPath for dependency but specVersion is workspa t.is(err.message.includes("\"configPath\" is only supported for specVersion \"workspace/1.1\" and higher"), true); }); +test("workspace/1.1: Absolute custom configPath", async (t) => { + const workspace = new t.context.Workspace({ + cwd: __dirname, + configuration: createWorkspaceConfig({ + specVersion: "workspace/1.1", + dependencyManagement: { + resolutions: [{ + path: "../../fixtures/library.d", + configPath: path.resolve(__dirname, "ui5.yaml") // Absolute configPath + }] + } + }) + }); + + const err = await t.throwsAsync(async () => await workspace._getResolvedModules()); + t.is(err.message.includes(`Absolute configPath (${path.resolve(__dirname, "ui5.yaml")}) ` + + `is not supported in dependency resolution configuration of workspace ${workspace.getName()}`), true); +}); + test("workspace/1.1: Custom configPath for dependency does not exist", async (t) => { const workspace = new t.context.Workspace({ cwd: __dirname, @@ -525,16 +564,40 @@ test("workspace/1.1: Custom configPath for dependency does not exist", async (t) dependencyManagement: { resolutions: [{ path: "../../fixtures/library.d", - configPath: "not-existing-config-path.yaml" // Custom configPath - }, { - path: "../../fixtures/library.e", - // no custom yaml -> resorts to default: ui5.yaml + configPath: "not-existing-config-path.yaml" // Custom not-existing configPath }] } }) }); const err = await t.throwsAsync(async () => await workspace._getResolvedModules()); - t.is(err.message.includes("Failed to read configuration for module library.d: " + - "Could not find configuration file in module at path 'not-existing-config-path.yaml'"), true); + t.is(err.message.includes(`Failed to read configuration for module library.d at `+ + `'${path.resolve(__dirname, "../../fixtures/library.d/not-existing-config-path.yaml")}'. ` + + `Error: ENOENT: no such file or directory, `+ + `open '${path.resolve(__dirname, "../../fixtures/library.d/not-existing-config-path.yaml")}'`), true); +}); + +test("workspace/1.1: Custom relative configPath which is outside of the module dir", async (t) => { + // This test covers a scenario where the custom relative configPath + // points to a location outside of the module directory. + // Config for library.d is located in library.e: + + const workspace = new t.context.Workspace({ + cwd: __dirname, + configuration: createWorkspaceConfig({ + specVersion: "workspace/1.1", + dependencyManagement: { + resolutions: [{ + path: "../../fixtures/library.d", + configPath: "../library.e/ui5-library.d-config.yaml" // Out-of-root path + }] + } + }) + }); + + const {projectNameMap} = await workspace._getResolvedModules(); + t.deepEqual(Array.from(projectNameMap.keys()).sort(), ["library.d"], "Correct project name key"); + // Returns absolute path of other library config: + t.is(projectNameMap.get("library.d")._configPath, + path.resolve(__dirname, "../../fixtures/library.e/ui5-library.d-config.yaml")); }); From 553b6db5a52d9fff9e5082d69b46619007f6c727 Mon Sep 17 00:00:00 2001 From: Max Reichmann Date: Fri, 9 Oct 2026 18:20:51 +0200 Subject: [PATCH 5/5] refactor: Adjust workspace config to fix tests --- .../lib/validation/schema/ui5-workspace.json | 8 ++- .../lib/validation/schema/ui5-workspace.js | 57 ++++++++++++++++++- 2 files changed, 60 insertions(+), 5 deletions(-) diff --git a/packages/project/lib/validation/schema/ui5-workspace.json b/packages/project/lib/validation/schema/ui5-workspace.json index f7a4bf76826..7b2916daa3b 100644 --- a/packages/project/lib/validation/schema/ui5-workspace.json +++ b/packages/project/lib/validation/schema/ui5-workspace.json @@ -28,8 +28,12 @@ "properties": { "resolutions": { "items": { - "not": {"required": ["configPath"]}, - "errorMessage": "\"configPath\" is only supported for specVersion \"workspace/1.1\" and higher" + "if": {"type": "object"}, + "then": { + "not": {"required": ["configPath"]}, + "errorMessage": "\"configPath\" is only supported for specVersion \"workspace/1.1\" and higher" + }, + "else": {} } } } diff --git a/packages/project/test/lib/validation/schema/ui5-workspace.js b/packages/project/test/lib/validation/schema/ui5-workspace.js index 232f5965991..8279a0d93b3 100644 --- a/packages/project/test/lib/validation/schema/ui5-workspace.js +++ b/packages/project/test/lib/validation/schema/ui5-workspace.js @@ -89,7 +89,7 @@ For details, see: https://ui5.github.io/cli/stable/pages/Workspace/#workspace-sp message: "must be equal to one of the allowed values", params: { - allowedValues: ["workspace/1.0"], + allowedValues: ["workspace/1.1", "workspace/1.0"], }, }, ], @@ -290,7 +290,7 @@ For details, see: https://ui5.github.io/cli/stable/pages/Workspace/#workspace-sp message: "must be equal to one of the allowed values", params: { - allowedValues: ["workspace/1.0"], + allowedValues: ["workspace/1.1", "workspace/1.0"], }, }, ], @@ -351,7 +351,7 @@ For details, see: https://ui5.github.io/cli/stable/pages/Workspace/#workspace-sp message: "must be equal to one of the allowed values", params: { - allowedValues: ["workspace/1.0"], + allowedValues: ["workspace/1.1", "workspace/1.0"], }, }, ], @@ -456,3 +456,54 @@ test("Invalid dependencyManagement", async (t) => { ] ); }); + +test("Invalid dependencyManagement: configPath not allowed for workspace/1.0", async (t) => { + await assertValidation( + t, + { + specVersion: "workspace/1.0", + metadata: { + name: "test-spec-name", + }, + dependencyManagement: { + resolutions: [ + { + path: "../my.library", + configPath: "config/ui5.yaml" + }], + }, + }, + [ + { + instancePath: "/dependencyManagement/resolutions/0", + keyword: "errorMessage", + message: "\"configPath\" is only supported for specVersion \"workspace/1.1\" and higher", + params: { + errors: [ + { + instancePath: "/dependencyManagement/resolutions/0", + keyword: "not", + message: "must NOT be valid", + params: {}, + }, + ], + }, + }, + ] + ); +}); + +test("Valid dependencyManagement: configPath allowed for workspace/1.1", async (t) => { + await assertValidation( + t, + { + specVersion: "workspace/1.1", + metadata: { + name: "test-spec-name", + }, + dependencyManagement: { + resolutions: [{path: "../my.library", configPath: "config/ui5.yaml"}], + }, + } + ); +});