From 2749e8ba069c4a5d7f3faa1299aace9f7e68d31e Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Fri, 23 Oct 2026 14:17:43 +0300 Subject: [PATCH 1/6] feat: Extend versioninfo object to support framework props --- .../lib/processors/versionInfoGenerator.js | 4 ++++ .../resources/sap-ui-version.json | 4 ++++ .../dest/resources/sap-ui-version.json | 4 ++++ .../dest/resources/sap-ui-version.json | 4 ++++ .../lib/processors/versionInfoGenerator.js | 19 +++++++++++++++++++ .../test/lib/tasks/generateVersionInfo.js | 8 ++++++++ 6 files changed, 43 insertions(+) diff --git a/packages/builder/lib/processors/versionInfoGenerator.js b/packages/builder/lib/processors/versionInfoGenerator.js index 21ff0d36c88..8e46520d94c 100644 --- a/packages/builder/lib/processors/versionInfoGenerator.js +++ b/packages/builder/lib/processors/versionInfoGenerator.js @@ -379,6 +379,9 @@ const processLibraryInfo = async (libraryInfo) => { * } * ``` * + * @param {object} [parameters.options.framework] Framework info to embed as the top-level + * `framework` object, e.g. `{name: "OpenUI5", version: "1.136.0"}`. Resolved by the + * caller (generateVersionInfo task). Defaults to `{name: "", version: ""}` when omitted. * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving with an array containing the versionInfo resource */ @@ -463,6 +466,7 @@ export default async function({options}) { buildTimestamp: buildTimestamp, scmRevision: "", // TODO: insert current application scm revision here // gav: "", // TODO: insert current application id + version here + framework: options.framework ?? {name: "", version: ""}, libraries, components }; diff --git a/packages/builder/test/expected/build/application.j/dest-resources-json/resources/sap-ui-version.json b/packages/builder/test/expected/build/application.j/dest-resources-json/resources/sap-ui-version.json index 551fc7d927c..151d6d5be04 100644 --- a/packages/builder/test/expected/build/application.j/dest-resources-json/resources/sap-ui-version.json +++ b/packages/builder/test/expected/build/application.j/dest-resources-json/resources/sap-ui-version.json @@ -3,5 +3,9 @@ "version": "1.0.0", "buildTimestamp": "202008120917", "scmRevision": "", + "framework": { + "name": "", + "version": "" + }, "libraries": [] } \ No newline at end of file diff --git a/packages/builder/test/expected/build/application.m/dest/resources/sap-ui-version.json b/packages/builder/test/expected/build/application.m/dest/resources/sap-ui-version.json index a109feea58d..a965e2b6034 100644 --- a/packages/builder/test/expected/build/application.m/dest/resources/sap-ui-version.json +++ b/packages/builder/test/expected/build/application.m/dest/resources/sap-ui-version.json @@ -3,5 +3,9 @@ "version": "1.0.0", "buildTimestamp": "WILL_BE_IGNORED_BY_TESTS", "scmRevision": "", + "framework": { + "name": "", + "version": "" + }, "libraries": [] } diff --git a/packages/builder/test/expected/build/application.o/dest/resources/sap-ui-version.json b/packages/builder/test/expected/build/application.o/dest/resources/sap-ui-version.json index b6212fb25e3..38949352ec3 100644 --- a/packages/builder/test/expected/build/application.o/dest/resources/sap-ui-version.json +++ b/packages/builder/test/expected/build/application.o/dest/resources/sap-ui-version.json @@ -3,5 +3,9 @@ "version": "1.0.0", "buildTimestamp": "WILL_BE_IGNORED_BY_TESTS", "scmRevision": "", + "framework": { + "name": "", + "version": "" + }, "libraries": [] } diff --git a/packages/builder/test/lib/processors/versionInfoGenerator.js b/packages/builder/test/lib/processors/versionInfoGenerator.js index 5a69ac6c72d..e4b6813ce79 100644 --- a/packages/builder/test/lib/processors/versionInfoGenerator.js +++ b/packages/builder/test/lib/processors/versionInfoGenerator.js @@ -37,6 +37,13 @@ const assertVersionInfoContent = (t, oExpectedVersionInfo, sActualContent) => { delete lib.buildTimestamp; // removing to allow deep comparison }); + // The "framework" object is always emitted. When the expectation does not assert it + // explicitly, verify it defaults to empty strings and remove it to allow deep comparison. + if (!("framework" in oExpectedVersionInfo)) { + t.deepEqual(currentVersionInfo.framework, {name: "", version: ""}, + "framework defaults to empty strings when no framework option is passed"); + delete currentVersionInfo.framework; + } t.deepEqual(currentVersionInfo, oExpectedVersionInfo, "Correct content"); }; @@ -59,6 +66,18 @@ test.serial("versionInfoGenerator empty libraryInfos parameter", async (t) => { }); +test.serial("versionInfoGenerator passes through framework option", async (t) => { + const {versionInfoGenerator} = t.context; + const versionInfos = await versionInfoGenerator({options: { + rootProjectName: "myname", rootProjectVersion: "1.33.7", libraryInfos: [], + framework: {name: "OpenUI5", version: "1.136.0"}}}); + + const result = JSON.parse(await versionInfos[0].getString()); + t.deepEqual(result.framework, {name: "OpenUI5", version: "1.136.0"}, + "framework object is emitted from the provided option"); +}); + + test.serial("versionInfoGenerator simple library infos", async (t) => { const {versionInfoGenerator} = t.context; const options = { diff --git a/packages/builder/test/lib/tasks/generateVersionInfo.js b/packages/builder/test/lib/tasks/generateVersionInfo.js index bedb63baf19..bd9bac0a598 100644 --- a/packages/builder/test/lib/tasks/generateVersionInfo.js +++ b/packages/builder/test/lib/tasks/generateVersionInfo.js @@ -91,6 +91,14 @@ async function assertCreatedVersionInfo(t, oExpectedVersionInfo, oOptions) { return libraryA.name.localeCompare(libraryB.name); }); + // The "framework" object is always emitted. When the expectation does not assert it + // explicitly, verify it defaults to empty strings and remove it to allow deep comparison. + if (!("framework" in oExpectedVersionInfo)) { + t.deepEqual(currentVersionInfo.framework, {name: "", version: ""}, + "framework defaults to empty strings when no framework is resolved"); + delete currentVersionInfo.framework; + } + t.deepEqual(currentVersionInfo, oExpectedVersionInfo, "Correct content"); } From 2f783429161a32e15119aeb67dbaeddff422fab2 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Fri, 23 Oct 2026 14:26:40 +0300 Subject: [PATCH 2/6] feat: Resolve explicit ui5.yaml framework config in generateVersionInfo task --- .../builder/lib/tasks/generateVersionInfo.js | 8 ++ .../test/lib/tasks/generateVersionInfo.js | 79 ++++++++++++++++++- 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/packages/builder/lib/tasks/generateVersionInfo.js b/packages/builder/lib/tasks/generateVersionInfo.js index 8cb2f8f6cfb..f9b4be6aec8 100644 --- a/packages/builder/lib/tasks/generateVersionInfo.js +++ b/packages/builder/lib/tasks/generateVersionInfo.js @@ -47,10 +47,18 @@ export default async ({workspace, dependencies, options: {rootProject, pattern}} }); const libraryInfos = await Promise.all(libraryInfosPromises); + // Resolve framework from explicit ui5.yaml configuration + const frameworkName = rootProject.getFrameworkName?.(); + let framework; + if (frameworkName === "OpenUI5" || frameworkName === "SAPUI5") { + framework = {name: frameworkName, version: rootProject.getFrameworkVersion()}; + } + const [versionInfoResource] = await versionInfoGenerator({ options: { rootProjectName: rootProject.getName(), rootProjectVersion: rootProject.getVersion(), + framework, libraryInfos } }); diff --git a/packages/builder/test/lib/tasks/generateVersionInfo.js b/packages/builder/test/lib/tasks/generateVersionInfo.js index bd9bac0a598..283302ffcac 100644 --- a/packages/builder/test/lib/tasks/generateVersionInfo.js +++ b/packages/builder/test/lib/tasks/generateVersionInfo.js @@ -28,7 +28,9 @@ const createProjectMetadata = (names, version, type) => { getName: () => key, getNamespace: () => names.join("/"), getVersion: () => version || "3.0.0-" + key, - getType: () => type || "library" + getType: () => type || "library", + getFrameworkName: () => undefined, + getFrameworkVersion: () => undefined }; }; @@ -1192,3 +1194,78 @@ test.serial("integration: Library of type 'module'", async (t) => { t.is(infoLogStub.callCount, 0); }); + +test.serial("task: explicit OpenUI5 framework config is forwarded", async (t) => { + const workspace = createWorkspace(); + const rootProject = { + getName: () => "myname", + getVersion: () => "1.33.7", + getFrameworkName: () => "OpenUI5", + getFrameworkVersion: () => "1.136.0" + }; + const oOptions = { + workspace, + dependencies: resourceFactory.createAdapter({virBasePath: "/resources/"}), + options: { + pattern: "/resources/**/.library", + rootProject + } + }; + await assertCreatedVersionInfo(t, { + "name": "myname", + "scmRevision": "", + "version": "1.33.7", + "framework": {"name": "OpenUI5", "version": "1.136.0"}, + "libraries": [] + }, oOptions); +}); + +test.serial("task: explicit SAPUI5 framework config is forwarded", async (t) => { + const workspace = createWorkspace(); + const rootProject = { + getName: () => "myname", + getVersion: () => "1.33.7", + getFrameworkName: () => "SAPUI5", + getFrameworkVersion: () => "1.136.0" + }; + const oOptions = { + workspace, + dependencies: resourceFactory.createAdapter({virBasePath: "/resources/"}), + options: { + pattern: "/resources/**/.library", + rootProject + } + }; + await assertCreatedVersionInfo(t, { + "name": "myname", + "scmRevision": "", + "version": "1.33.7", + "framework": {"name": "SAPUI5", "version": "1.136.0"}, + "libraries": [] + }, oOptions); +}); + +test.serial("task: unrecognized framework name yields empty framework", async (t) => { + const workspace = createWorkspace(); + const rootProject = { + getName: () => "myname", + getVersion: () => "1.33.7", + getFrameworkName: () => "CustomFramework", + getFrameworkVersion: () => "1.0.0" + }; + const oOptions = { + workspace, + dependencies: resourceFactory.createAdapter({virBasePath: "/resources/"}), + options: { + pattern: "/resources/**/.library", + rootProject + } + }; + // framework not in expected → assertCreatedVersionInfo verifies it defaults to {name:"",version:""} + await assertCreatedVersionInfo(t, { + "name": "myname", + "scmRevision": "", + "version": "1.33.7", + "libraries": [] + }, oOptions); +}); From ac0e166fd6ae7c6f1de1f10da1765f8a0ff283c1 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Fri, 23 Oct 2026 14:43:55 +0300 Subject: [PATCH 3/6] feat: Scan dependencies for framework signal in generateVersionInfo task --- .../builder/lib/tasks/generateVersionInfo.js | 26 +++++ .../test/lib/tasks/generateVersionInfo.js | 98 +++++++++++++++++++ 2 files changed, 124 insertions(+) diff --git a/packages/builder/lib/tasks/generateVersionInfo.js b/packages/builder/lib/tasks/generateVersionInfo.js index f9b4be6aec8..f38cfee10af 100644 --- a/packages/builder/lib/tasks/generateVersionInfo.js +++ b/packages/builder/lib/tasks/generateVersionInfo.js @@ -54,6 +54,32 @@ export default async ({workspace, dependencies, options: {rootProject, pattern}} framework = {name: frameworkName, version: rootProject.getFrameworkVersion()}; } + // Fallback: scan dependency npm IDs for framework signal + if (!framework) { + let openui5Detected = false; + let openui5CoreVersion = ""; + for (const res of resources) { + const id = res.getProject().getId?.(); + // If at least one SAPUI5 library is present, use "SAPUI5", + // but leave version empty if no SAPUI5 dist version is known + if (id?.startsWith("@sapui5/")) { + framework = {name: "SAPUI5", version: ""}; + break; + } + // If only OpenUI5 libraries are present, use "OpenUI5" and version + // from "sap.ui.core" project + if (id?.startsWith("@openui5/")) { + openui5Detected = true; + if (res.getProject().getName() === "sap.ui.core") { + openui5CoreVersion = res.getProject().getVersion(); + } + } + } + if (!framework && openui5Detected) { + framework = {name: "OpenUI5", version: openui5CoreVersion}; + } + } + const [versionInfoResource] = await versionInfoGenerator({ options: { rootProjectName: rootProject.getName(), diff --git a/packages/builder/test/lib/tasks/generateVersionInfo.js b/packages/builder/test/lib/tasks/generateVersionInfo.js index 283302ffcac..730da35c742 100644 --- a/packages/builder/test/lib/tasks/generateVersionInfo.js +++ b/packages/builder/test/lib/tasks/generateVersionInfo.js @@ -29,6 +29,7 @@ const createProjectMetadata = (names, version, type) => { getNamespace: () => names.join("/"), getVersion: () => version || "3.0.0-" + key, getType: () => type || "library", + getId: () => key, getFrameworkName: () => undefined, getFrameworkVersion: () => undefined }; @@ -1269,3 +1270,100 @@ test.serial("task: unrecognized framework name yields empty framework", async (t "libraries": [] }, oOptions); }); + +function createScopedDepProject(names, {id, version = "1.136.0"} = {}) { + const key = names.join("."); + return { + getName: () => key, + getNamespace: () => names.join("/"), + getVersion: () => version, + getType: () => "library", + getId: () => id ?? key, + getFrameworkName: () => undefined, + getFrameworkVersion: () => undefined + }; +} + +test.serial("task: scans @openui5/sap.ui.core dep and resolves OpenUI5 with core version", async (t) => { + const workspace = createWorkspace(); + const dep = resourceFactory.createAdapter({ + virBasePath: "/", + project: createScopedDepProject(["sap", "ui", "core"], {id: "@openui5/sap.ui.core", version: "1.136.0"}) + }); + await dep.write(resourceFactory.createResource({ + path: "/resources/sap/ui/core/.library", + string: `sap.ui.core` + })); + const rootProject = { + getName: () => "myapp", getVersion: () => "1.0.0", + getFrameworkName: () => undefined, getFrameworkVersion: () => undefined + }; + const oOptions = { + workspace, + dependencies: dep, + options: {pattern: "/resources/**/.library", rootProject} + }; + await assertCreatedVersionInfo(t, { + "name": "myapp", + "scmRevision": "", + "version": "1.0.0", + "framework": {"name": "OpenUI5", "version": "1.136.0"}, + "libraries": [{"name": "sap.ui.core", "scmRevision": "", "version": "1.136.0"}] + }, oOptions); +}); + +test.serial("task: scans @sapui5/ dep and resolves SAPUI5 with empty version (Scenario 5)", async (t) => { + const workspace = createWorkspace(); + const dep = resourceFactory.createAdapter({ + virBasePath: "/", + project: createScopedDepProject(["sap", "m"], {id: "@sapui5/sap.m", version: "1.136.0"}) + }); + await dep.write(resourceFactory.createResource({ + path: "/resources/sap/m/.library", + string: `sap.m` + })); + const rootProject = { + getName: () => "myapp", getVersion: () => "1.0.0", + getFrameworkName: () => undefined, getFrameworkVersion: () => undefined + }; + const oOptions = { + workspace, + dependencies: dep, + options: {pattern: "/resources/**/.library", rootProject} + }; + await assertCreatedVersionInfo(t, { + "name": "myapp", + "scmRevision": "", + "version": "1.0.0", + "framework": {"name": "SAPUI5", "version": ""}, + "libraries": [{"name": "sap.m", "scmRevision": "", "version": "1.136.0"}] + }, oOptions); +}); + +test.serial("task: explicit ui5.yaml config wins over @sapui5/ scan result", async (t) => { + const workspace = createWorkspace(); + const dep = resourceFactory.createAdapter({ + virBasePath: "/", + project: createScopedDepProject(["sap", "m"], {id: "@sapui5/sap.m", version: "1.136.0"}) + }); + await dep.write(resourceFactory.createResource({ + path: "/resources/sap/m/.library", + string: `sap.m` + })); + const rootProject = { + getName: () => "myapp", getVersion: () => "1.0.0", + getFrameworkName: () => "OpenUI5", getFrameworkVersion: () => "1.138.0" + }; + const oOptions = { + workspace, + dependencies: dep, + options: {pattern: "/resources/**/.library", rootProject} + }; + await assertCreatedVersionInfo(t, { + "name": "myapp", + "scmRevision": "", + "version": "1.0.0", + "framework": {"name": "OpenUI5", "version": "1.138.0"}, + "libraries": [{"name": "sap.m", "scmRevision": "", "version": "1.136.0"}] + }, oOptions); +}); From 4b58bb2457426ac8a111a2f73619f5014c8598ee Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Tue, 27 Oct 2026 10:22:58 +0200 Subject: [PATCH 4/6] test: Address new structure in server tests --- packages/server/test/lib/server/main.js | 1 + packages/server/test/lib/server/middleware/versionInfo.js | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/server/test/lib/server/main.js b/packages/server/test/lib/server/main.js index 54c9049be62..0633d6e2d0d 100644 --- a/packages/server/test/lib/server/main.js +++ b/packages/server/test/lib/server/main.js @@ -208,6 +208,7 @@ test("Get sap-ui-version.json from versionInfo middleware (/resources/sap-ui-ver "version": "1.0.0", "buildTimestamp": buildTimestamp, "scmRevision": "", + "framework": {"name": "", "version": ""}, "libraries": [ { name: "library.a", diff --git a/packages/server/test/lib/server/middleware/versionInfo.js b/packages/server/test/lib/server/middleware/versionInfo.js index e3ca031ee95..9adb0b304e9 100644 --- a/packages/server/test/lib/server/middleware/versionInfo.js +++ b/packages/server/test/lib/server/middleware/versionInfo.js @@ -349,6 +349,7 @@ test.serial("integration: Library with dependencies and subcomponent complex sce "name": "myname", "scmRevision": "", "version": "1.33.7", + "framework": {"name": "", "version": ""}, "libraries": [{ "name": "lib.a", "scmRevision": "", From ef6378d4184cc5a517633cdefa97cbd5b8549de6 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Wed, 28 Oct 2026 12:07:04 +0200 Subject: [PATCH 5/6] refactor: Extract framework detection logic as a standalone method --- .../lib/processors/versionInfoGenerator.js | 9 ++- .../builder/lib/tasks/generateVersionInfo.js | 72 ++++++++++--------- 2 files changed, 46 insertions(+), 35 deletions(-) diff --git a/packages/builder/lib/processors/versionInfoGenerator.js b/packages/builder/lib/processors/versionInfoGenerator.js index 8e46520d94c..dc4c9df88cb 100644 --- a/packages/builder/lib/processors/versionInfoGenerator.js +++ b/packages/builder/lib/processors/versionInfoGenerator.js @@ -379,9 +379,12 @@ const processLibraryInfo = async (libraryInfo) => { * } * ``` * - * @param {object} [parameters.options.framework] Framework info to embed as the top-level - * `framework` object, e.g. `{name: "OpenUI5", version: "1.136.0"}`. Resolved by the - * caller (generateVersionInfo task). Defaults to `{name: "", version: ""}` when omitted. + * @param {object} [parameters.options.framework] Framework info embedded as the top-level + * `framework` object in sap-ui-version.json. Defaults to `{name: "", version: ""}` + * when omitted. + * @param {string} parameters.options.framework.name Framework name, e.g. "OpenUI5" or "SAPUI5" + * @param {string} parameters.options.framework.version Framework version, e.g. "1.136.0"; + * may be an empty string if the version is unknown * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving with an array containing the versionInfo resource */ diff --git a/packages/builder/lib/tasks/generateVersionInfo.js b/packages/builder/lib/tasks/generateVersionInfo.js index f38cfee10af..564845cd039 100644 --- a/packages/builder/lib/tasks/generateVersionInfo.js +++ b/packages/builder/lib/tasks/generateVersionInfo.js @@ -7,6 +7,45 @@ const MANIFEST_JSON = "manifest.json"; * @module @ui5/builder/tasks/generateVersionInfo */ +/** + * Determines the UI5 framework (name and version) for the version info. + * + * Resolution order: + * 1. Explicit framework configuration from the root project's ui5.yaml + * 2. Dependency npm-scope scan (@sapui5/ → SAPUI5, @openui5/ → OpenUI5) + * 3. undefined if no framework signal is found + * + * @private + * @param {object} rootProject The root project instance + * @param {module:@ui5/fs/Resource[]} resources Filtered library dependency resources + * @returns {{name: string, version: string}|undefined} Framework object or undefined + */ +function resolveFramework(rootProject, resources) { + const frameworkName = rootProject.getFrameworkName?.(); + if (frameworkName === "OpenUI5" || frameworkName === "SAPUI5") { + return {name: frameworkName, version: rootProject.getFrameworkVersion()}; + } + + let openui5Detected = false; + let openui5CoreVersion = ""; + for (const res of resources) { + const id = res.getProject().getId?.(); + if (id?.startsWith("@sapui5/")) { + return {name: "SAPUI5", version: ""}; + } + if (id?.startsWith("@openui5/")) { + openui5Detected = true; + if (res.getProject().getName() === "sap.ui.core") { + openui5CoreVersion = res.getProject().getVersion(); + } + } + } + if (openui5Detected) { + return {name: "OpenUI5", version: openui5CoreVersion}; + } + return undefined; +} + /** * Task to create sap-ui-version.json * @@ -47,38 +86,7 @@ export default async ({workspace, dependencies, options: {rootProject, pattern}} }); const libraryInfos = await Promise.all(libraryInfosPromises); - // Resolve framework from explicit ui5.yaml configuration - const frameworkName = rootProject.getFrameworkName?.(); - let framework; - if (frameworkName === "OpenUI5" || frameworkName === "SAPUI5") { - framework = {name: frameworkName, version: rootProject.getFrameworkVersion()}; - } - - // Fallback: scan dependency npm IDs for framework signal - if (!framework) { - let openui5Detected = false; - let openui5CoreVersion = ""; - for (const res of resources) { - const id = res.getProject().getId?.(); - // If at least one SAPUI5 library is present, use "SAPUI5", - // but leave version empty if no SAPUI5 dist version is known - if (id?.startsWith("@sapui5/")) { - framework = {name: "SAPUI5", version: ""}; - break; - } - // If only OpenUI5 libraries are present, use "OpenUI5" and version - // from "sap.ui.core" project - if (id?.startsWith("@openui5/")) { - openui5Detected = true; - if (res.getProject().getName() === "sap.ui.core") { - openui5CoreVersion = res.getProject().getVersion(); - } - } - } - if (!framework && openui5Detected) { - framework = {name: "OpenUI5", version: openui5CoreVersion}; - } - } + const framework = resolveFramework(rootProject, resources); const [versionInfoResource] = await versionInfoGenerator({ options: { From 81ac82435d47260f8154d5ae1cce4e55e7f18201 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Thu, 29 Oct 2026 14:41:37 +0200 Subject: [PATCH 6/6] fix: Consider version info middleware & always fill the whole framework object --- .../lib/processors/versionInfoGenerator.js | 58 ++++++++-- .../builder/lib/tasks/generateVersionInfo.js | 47 +------- .../lib/processors/versionInfoGenerator.js | 70 +++++++++++- .../test/lib/tasks/generateVersionInfo.js | 2 +- packages/server/lib/middleware/versionInfo.js | 5 +- .../test/lib/server/middleware/versionInfo.js | 103 +++++++++++++++++- 6 files changed, 228 insertions(+), 57 deletions(-) diff --git a/packages/builder/lib/processors/versionInfoGenerator.js b/packages/builder/lib/processors/versionInfoGenerator.js index dc4c9df88cb..64f8b2a68ca 100644 --- a/packages/builder/lib/processors/versionInfoGenerator.js +++ b/packages/builder/lib/processors/versionInfoGenerator.js @@ -350,12 +350,57 @@ const processLibraryInfo = async (libraryInfo) => { * @typedef {object} LibraryInfo * @property {string} name The library name, e.g. "lib.x" * @property {string} version The library version, e.g. "1.0.0" + * @property {string} [id] The npm package id of the library project, e.g. "@openui5/sap.ui.core". + * Used by {@link resolveFramework} to detect the UI5 framework via npm-scope scanning. * @property {@ui5/fs/Resource} libraryManifest library manifest resource, * e.g. resource with path "lib/x/manifest.json" * @property {@ui5/fs/Resource[]} embeddedManifests list of embedded manifest resources, * e.g. resource with path "lib/x/sub/manifest.json" */ +/** + * Resolves the UI5 framework name and version from raw project inputs. + * + * Resolution order: + * 1. Explicit config: frameworkName is "OpenUI5" or "SAPUI5" + * → returns it with frameworkVersion coalesced to "". + * 2. Dependency scan: examines libraryInfos[].id for @sapui5/ or + * @openui5/ npm scopes; version comes from the sap.ui.core entry. + * 3. Fallback: {name: "", version: ""}. + * + * The returned object always has both name and version as strings. + * + * @public + * @param {string} [frameworkName] Framework name from the root project's ui5.yaml + * @param {string} [frameworkVersion] Framework version from the root project's ui5.yaml + * @param {module:@ui5/builder/processors/versionInfoGenerator~LibraryInfo[]} libraryInfos + * Library infos; each entry's optional id field is used for the npm-scope scan + * @returns {{name: string, version: string}} Resolved framework object + */ +export function resolveFramework(frameworkName, frameworkVersion, libraryInfos) { + if (frameworkName === "OpenUI5" || frameworkName === "SAPUI5") { + return {name: frameworkName, version: frameworkVersion || ""}; + } + let openui5Detected = false; + let openui5CoreVersion = ""; + for (const libraryInfo of libraryInfos) { + const {id} = libraryInfo; + if (id?.startsWith("@sapui5/")) { + return {name: "SAPUI5", version: ""}; + } + if (id?.startsWith("@openui5/")) { + openui5Detected = true; + if (libraryInfo.name === "sap.ui.core") { + openui5CoreVersion = libraryInfo.version; + } + } + } + if (openui5Detected) { + return {name: "OpenUI5", version: openui5CoreVersion}; + } + return {name: "", version: ""}; +} + /** * Creates sap-ui-version.json. * @@ -379,12 +424,11 @@ const processLibraryInfo = async (libraryInfo) => { * } * ``` * - * @param {object} [parameters.options.framework] Framework info embedded as the top-level - * `framework` object in sap-ui-version.json. Defaults to `{name: "", version: ""}` - * when omitted. - * @param {string} parameters.options.framework.name Framework name, e.g. "OpenUI5" or "SAPUI5" - * @param {string} parameters.options.framework.version Framework version, e.g. "1.136.0"; - * may be an empty string if the version is unknown + * @param {string} [parameters.options.frameworkName] Framework name from the root project's ui5.yaml, + * e.g. "OpenUI5" or "SAPUI5". Used for framework resolution. + * @param {string} [parameters.options.frameworkVersion] Framework version from the root project's ui5.yaml, + * e.g. "1.136.0". Used only when frameworkName is recognized; + * defaults to "" when omitted. * @returns {Promise<@ui5/fs/Resource[]>} Promise resolving with an array containing the versionInfo resource */ @@ -469,7 +513,7 @@ export default async function({options}) { buildTimestamp: buildTimestamp, scmRevision: "", // TODO: insert current application scm revision here // gav: "", // TODO: insert current application id + version here - framework: options.framework ?? {name: "", version: ""}, + framework: resolveFramework(options.frameworkName, options.frameworkVersion, options.libraryInfos), libraries, components }; diff --git a/packages/builder/lib/tasks/generateVersionInfo.js b/packages/builder/lib/tasks/generateVersionInfo.js index 564845cd039..076f7408c46 100644 --- a/packages/builder/lib/tasks/generateVersionInfo.js +++ b/packages/builder/lib/tasks/generateVersionInfo.js @@ -7,45 +7,6 @@ const MANIFEST_JSON = "manifest.json"; * @module @ui5/builder/tasks/generateVersionInfo */ -/** - * Determines the UI5 framework (name and version) for the version info. - * - * Resolution order: - * 1. Explicit framework configuration from the root project's ui5.yaml - * 2. Dependency npm-scope scan (@sapui5/ → SAPUI5, @openui5/ → OpenUI5) - * 3. undefined if no framework signal is found - * - * @private - * @param {object} rootProject The root project instance - * @param {module:@ui5/fs/Resource[]} resources Filtered library dependency resources - * @returns {{name: string, version: string}|undefined} Framework object or undefined - */ -function resolveFramework(rootProject, resources) { - const frameworkName = rootProject.getFrameworkName?.(); - if (frameworkName === "OpenUI5" || frameworkName === "SAPUI5") { - return {name: frameworkName, version: rootProject.getFrameworkVersion()}; - } - - let openui5Detected = false; - let openui5CoreVersion = ""; - for (const res of resources) { - const id = res.getProject().getId?.(); - if (id?.startsWith("@sapui5/")) { - return {name: "SAPUI5", version: ""}; - } - if (id?.startsWith("@openui5/")) { - openui5Detected = true; - if (res.getProject().getName() === "sap.ui.core") { - openui5CoreVersion = res.getProject().getVersion(); - } - } - } - if (openui5Detected) { - return {name: "OpenUI5", version: openui5CoreVersion}; - } - return undefined; -} - /** * Task to create sap-ui-version.json * @@ -80,19 +41,19 @@ export default async ({workspace, dependencies, options: {rootProject, pattern}} libraryManifest, embeddedManifests, name: dotLibResource.getProject().getName(), - version: dotLibResource.getProject().getVersion() + version: dotLibResource.getProject().getVersion(), + id: dotLibResource.getProject().getId?.() }; }); }); const libraryInfos = await Promise.all(libraryInfosPromises); - const framework = resolveFramework(rootProject, resources); - const [versionInfoResource] = await versionInfoGenerator({ options: { rootProjectName: rootProject.getName(), rootProjectVersion: rootProject.getVersion(), - framework, + frameworkName: rootProject.getFrameworkName?.(), + frameworkVersion: rootProject.getFrameworkVersion?.(), libraryInfos } }); diff --git a/packages/builder/test/lib/processors/versionInfoGenerator.js b/packages/builder/test/lib/processors/versionInfoGenerator.js index e4b6813ce79..3b0b541ec1b 100644 --- a/packages/builder/test/lib/processors/versionInfoGenerator.js +++ b/packages/builder/test/lib/processors/versionInfoGenerator.js @@ -1,6 +1,7 @@ import test from "ava"; import sinon from "sinon"; import esmock from "esmock"; +import {resolveFramework} from "../../../lib/processors/versionInfoGenerator.js"; test.beforeEach(async (t) => { t.context.warnLogStub = sinon.stub(); @@ -66,15 +67,76 @@ test.serial("versionInfoGenerator empty libraryInfos parameter", async (t) => { }); -test.serial("versionInfoGenerator passes through framework option", async (t) => { +test.serial("versionInfoGenerator resolves framework from explicit frameworkName/frameworkVersion", async (t) => { const {versionInfoGenerator} = t.context; const versionInfos = await versionInfoGenerator({options: { rootProjectName: "myname", rootProjectVersion: "1.33.7", libraryInfos: [], - framework: {name: "OpenUI5", version: "1.136.0"}}}); + frameworkName: "OpenUI5", frameworkVersion: "1.136.0"}}); const result = JSON.parse(await versionInfos[0].getString()); - t.deepEqual(result.framework, {name: "OpenUI5", version: "1.136.0"}, - "framework object is emitted from the provided option"); + t.deepEqual(result.framework, {name: "OpenUI5", version: "1.136.0"}); +}); + +// --- resolveFramework unit tests (pure function, no esmock needed) --- + +test("resolveFramework: explicit OpenUI5 returns name and version", (t) => { + t.deepEqual(resolveFramework("OpenUI5", "1.136.0", []), {name: "OpenUI5", version: "1.136.0"}); +}); + +test("resolveFramework: explicit SAPUI5 returns name and version", (t) => { + t.deepEqual(resolveFramework("SAPUI5", "1.136.0", []), {name: "SAPUI5", version: "1.136.0"}); +}); + +test("resolveFramework: name-only config coalesces missing version to empty string (fixes #3)", (t) => { + t.deepEqual(resolveFramework("SAPUI5", undefined, []), {name: "SAPUI5", version: ""}); + t.deepEqual(resolveFramework("OpenUI5", undefined, []), {name: "OpenUI5", version: ""}); +}); + +test("resolveFramework: explicit config wins over dependency scan", (t) => { + const libs = [{id: "@sapui5/sap.m", name: "sap.m", version: "1.136.0"}]; + t.deepEqual(resolveFramework("OpenUI5", "1.138.0", libs), {name: "OpenUI5", version: "1.138.0"}); +}); + +test("resolveFramework: unrecognized frameworkName falls through to scan and then fallback", (t) => { + t.deepEqual(resolveFramework("CustomFramework", "1.0.0", []), {name: "", version: ""}); +}); + +test("resolveFramework: scans @openui5/ dep and resolves OpenUI5 with sap.ui.core version", (t) => { + const libs = [ + {id: "@openui5/sap.m", name: "sap.m", version: "1.136.0"}, + {id: "@openui5/sap.ui.core", name: "sap.ui.core", version: "1.136.0"} + ]; + t.deepEqual(resolveFramework(undefined, undefined, libs), {name: "OpenUI5", version: "1.136.0"}); +}); + +test("resolveFramework: scans @openui5/ dep without sap.ui.core yields empty version", (t) => { + const libs = [{id: "@openui5/sap.m", name: "sap.m", version: "1.136.0"}]; + t.deepEqual(resolveFramework(undefined, undefined, libs), {name: "OpenUI5", version: ""}); +}); + +test("resolveFramework: scans @sapui5/ dep and resolves SAPUI5 with empty version", (t) => { + const libs = [{id: "@sapui5/sap.m", name: "sap.m", version: "1.136.0"}]; + t.deepEqual(resolveFramework(undefined, undefined, libs), {name: "SAPUI5", version: ""}); +}); + +test("resolveFramework: @sapui5/ dep short-circuits before @openui5/ accumulation", (t) => { + const libs = [ + {id: "@openui5/sap.ui.core", name: "sap.ui.core", version: "1.136.0"}, + {id: "@sapui5/sap.m", name: "sap.m", version: "1.136.0"} + ]; + t.deepEqual(resolveFramework(undefined, undefined, libs), {name: "SAPUI5", version: ""}); +}); + +test("resolveFramework: libs without id are ignored in scan (no-id fallback)", (t) => { + const libs = [ + {name: "sap.m", version: "1.136.0"}, + {id: undefined, name: "sap.ui.core", version: "1.136.0"} + ]; + t.deepEqual(resolveFramework(undefined, undefined, libs), {name: "", version: ""}); +}); + +test("resolveFramework: empty libraryInfos returns fallback", (t) => { + t.deepEqual(resolveFramework(undefined, undefined, []), {name: "", version: ""}); }); diff --git a/packages/builder/test/lib/tasks/generateVersionInfo.js b/packages/builder/test/lib/tasks/generateVersionInfo.js index 730da35c742..2c2adcfcd82 100644 --- a/packages/builder/test/lib/tasks/generateVersionInfo.js +++ b/packages/builder/test/lib/tasks/generateVersionInfo.js @@ -1312,7 +1312,7 @@ test.serial("task: scans @openui5/sap.ui.core dep and resolves OpenUI5 with core }, oOptions); }); -test.serial("task: scans @sapui5/ dep and resolves SAPUI5 with empty version (Scenario 5)", async (t) => { +test.serial("task: scans @sapui5/ dep and resolves SAPUI5 with empty version", async (t) => { const workspace = createWorkspace(); const dep = resourceFactory.createAdapter({ virBasePath: "/", diff --git a/packages/server/lib/middleware/versionInfo.js b/packages/server/lib/middleware/versionInfo.js index ac48ae70755..4fc12d94d1e 100644 --- a/packages/server/lib/middleware/versionInfo.js +++ b/packages/server/lib/middleware/versionInfo.js @@ -40,7 +40,8 @@ function createMiddleware({resources, middlewareUtil}) { libraryManifest, embeddedManifests, name: dotLibResource.getProject().getName(), - version: dotLibResource.getProject().getVersion() + version: dotLibResource.getProject().getVersion(), + id: dotLibResource.getProject().getId?.() }; }); const rootProject = middlewareUtil.getProject(); @@ -49,6 +50,8 @@ function createMiddleware({resources, middlewareUtil}) { options: { rootProjectName: rootProject.getName(), rootProjectVersion: rootProject.getVersion(), + frameworkName: rootProject.getFrameworkName?.(), + frameworkVersion: rootProject.getFrameworkVersion?.(), libraryInfos } }); diff --git a/packages/server/test/lib/server/middleware/versionInfo.js b/packages/server/test/lib/server/middleware/versionInfo.js index 9adb0b304e9..80e2d4b776c 100644 --- a/packages/server/test/lib/server/middleware/versionInfo.js +++ b/packages/server/test/lib/server/middleware/versionInfo.js @@ -32,7 +32,9 @@ const createProjectMetadata = (names, version, type) => { getName: () => key, getNamespace: () => names.join("/"), getVersion: () => version || "3.0.0-" + key, - getType: () => type || "library" + getType: () => type || "library", + getFrameworkName: () => undefined, + getFrameworkVersion: () => undefined }; }; @@ -272,6 +274,10 @@ test.serial("test all inner API calls within middleware", async (t) => { const versionInfoGeneratorOptions = versionInfoGeneratorStub.getCall(0).args[0].options; t.is(versionInfoGeneratorOptions.rootProjectName, "myname"); t.is(versionInfoGeneratorOptions.rootProjectVersion, "1.33.7"); + t.is(versionInfoGeneratorOptions.frameworkName, undefined, + "frameworkName forwarded from root project (undefined when not configured)"); + t.is(versionInfoGeneratorOptions.frameworkVersion, undefined, + "frameworkVersion forwarded from root project (undefined when not configured)"); t.is(versionInfoGeneratorOptions.libraryInfos.length, 3); t.is(versionInfoGeneratorOptions.libraryInfos[0].name, "lib.a"); t.is(await versionInfoGeneratorOptions.libraryInfos[0].libraryManifest.getString(), "mocked manifest.json 1"); @@ -444,3 +450,98 @@ test.serial("integration: Library with dependencies and subcomponent complex sce await middleware(undefined, res, next); }); + +test.serial("integration: middleware resolves framework for configured OpenUI5 project", async (t) => { + const workspace = createWorkspace(); + await createDotLibrary(workspace, resourceFactory, ["test", "lib"]); + + const coreProject = { + getName: () => "sap.ui.core", + getNamespace: () => "sap/ui/core", + getVersion: () => "1.136.0", + getType: () => "library", + getFrameworkName: () => undefined, + getFrameworkVersion: () => undefined, + getId: () => "@openui5/sap.ui.core" + }; + const dep = resourceFactory.createAdapter({virBasePath: "/", project: coreProject}); + coreProject.getSourceReader = () => dep; + await createDotLibrary(dep, resourceFactory, ["sap", "ui", "core"], "1.136.0"); + await createManifestResource(dep, resourceFactory, ["sap", "ui", "core"], []); + + const resources = { + dependencies: resourceFactory.createReaderCollection({ + name: "dependencies", + readers: [dep] + }) + }; + + const middlewareUtil = { + getProject: () => ({ + getName: () => "myapp", + getVersion: () => "1.0.0", + getFrameworkName: () => "OpenUI5", + getFrameworkVersion: () => "1.136.0" + }) + }; + + const versionInfoMiddleware = await t.context.createVersionInfoMiddleware(); + const middleware = versionInfoMiddleware({resources, middlewareUtil}); + + const expectedVersionInfo = { + "name": "myapp", + "scmRevision": "", + "version": "1.0.0", + "framework": {"name": "OpenUI5", "version": "1.136.0"}, + "libraries": [{"name": "sap.ui.core", "scmRevision": "", "version": "1.136.0"}] + }; + + const res = { + writeHead: function() {}, + end: async function(versionInfoContent) { + await assertCreatedVersionInfo(t, expectedVersionInfo, versionInfoContent); + } + }; + await middleware(undefined, res, function(err) { + throw err; + }); +}); + +test.serial("integration: middleware emits empty string version for name-only framework config", async (t) => { + const workspace = createWorkspace(); + await createDotLibrary(workspace, resourceFactory, ["test", "lib"]); + + const resources = { + dependencies: resourceFactory.createReaderCollection({name: "dependencies", readers: []}) + }; + + const middlewareUtil = { + getProject: () => ({ + getName: () => "myapp", + getVersion: () => "1.0.0", + getFrameworkName: () => "SAPUI5", + getFrameworkVersion: () => undefined + }) + }; + + const versionInfoMiddleware = await t.context.createVersionInfoMiddleware(); + const middleware = versionInfoMiddleware({resources, middlewareUtil}); + + const expectedVersionInfo = { + "name": "myapp", + "scmRevision": "", + "version": "1.0.0", + "framework": {"name": "SAPUI5", "version": ""}, + "libraries": [] + }; + + const res = { + writeHead: function() {}, + end: async function(versionInfoContent) { + await assertCreatedVersionInfo(t, expectedVersionInfo, versionInfoContent); + } + }; + await middleware(undefined, res, function(err) { + throw err; + }); +});