Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/project/lib/build/helpers/MonitoredTaskUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const TRACKED_TASK_UTIL_METHODS = {
getEnv: "env",
getTime: "time",
isRootProject: "isRootProject",
getResolvedFrameworkVersion: "resolvedFrameworkVersion",
};

// Tracked methods whose first argument is the input name (the environment variable name, the time
Expand Down
13 changes: 13 additions & 0 deletions packages/project/lib/build/helpers/ProjectBuildContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ class ProjectBuildContext {
return undefined;
}
rawValue = quantizeTime(name, this.getBuildTime());
} else if (type === "resolvedFrameworkVersion") {
rawValue = this.getResolvedFrameworkVersion();
} else if (type.startsWith("project.")) {
const method = type.slice("project.".length);
const project = this.getProject(name);
Expand Down Expand Up @@ -206,6 +208,17 @@ class ProjectBuildContext {
return this._project;
}

/**
* Returns the effective framework version resolved for this graph during enrichment
* (reflects --framework-version, or a workspace link), or the root
* project's configured version when no resolution occurred.
*
* @returns {string|undefined} Effective framework version, e.g. "1.138.0"
*/
getResolvedFrameworkVersion() {
return this._buildContext.getGraph().getResolvedFrameworkVersion();
}

/**
* Retrieves a list of direct dependencies of a given project from the dependency graph
*
Expand Down
20 changes: 18 additions & 2 deletions packages/project/lib/build/helpers/TaskUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,22 @@ class TaskUtil {
return this._projectBuildContext.getBuildTime();
}

/**
* Returns the effective framework version resolved for the build's graph during enrichment
* (reflects --framework-version, or a workspace link), or the root
* project's configured version when no resolution occurred.
*
* </br></br>
* This method is only available to custom task extensions defining
* <b>Specification Version 5.0 and above</b>.
*
* @returns {string|undefined} Effective framework version, e.g. "1.138.0"
* @public
*/
getResolvedFrameworkVersion() {
return this._projectBuildContext.getResolvedFrameworkVersion();
}

/**
* Check whether the project currently being built is the root project.
*
Expand Down Expand Up @@ -434,7 +450,7 @@ class TaskUtil {
}

if (specVersion.gte("5.0")) {
bindFunctions(this, baseInterface, ["getEnv", "getTime", "getBuildTime"]);
bindFunctions(this, baseInterface, ["getEnv", "getTime", "getBuildTime", "getResolvedFrameworkVersion"]);
}
return baseInterface;
}
Expand Down Expand Up @@ -482,7 +498,7 @@ class TaskUtil {
}

if (specVersion.gte("5.0")) {
bindFunctions(this, baseInterface, ["getEnv", "getTime", "getBuildTime"]);
bindFunctions(this, baseInterface, ["getEnv", "getTime", "getBuildTime", "getResolvedFrameworkVersion"]);
}
return baseInterface;
}
Expand Down
27 changes: 27 additions & 0 deletions packages/project/lib/graph/ProjectGraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class ProjectGraph {
this._sealed = false;
this._hasUnresolvedOptionalDependencies = false; // Performance optimization flag
this._taskRepository = null;
this._resolvedFrameworkVersion = null;
}

/**
Expand Down Expand Up @@ -675,6 +676,32 @@ class ProjectGraph {
this._taskRepository = taskRepository;
}

/**
* Set the framework version resolved for this graph during enrichment.
*
* @public
* @param {string} version Effective framework version
*/
setResolvedFrameworkVersion(version) {
this._resolvedFrameworkVersion = version;
}

/**
* Get the effective framework version for this graph, as resolved during enrichment.
*
* Returns the value set by {@link #setResolvedFrameworkVersion}. When enrichment has not run
* (e.g. the project has no framework configuration), the stored value is <code>null</code> and
* this falls back to the root project's configured <code>framework.version</code>.
*
* @public
* @returns {string|undefined} Effective framework version, e.g. "1.138.0", or <code>""</code>
* when the framework is active but has no resolvable distribution version (SAPUI5 + workspace),
* or the configured version as a fallback when enrichment did not run.
*/
getResolvedFrameworkVersion() {
return this._resolvedFrameworkVersion ?? this.getRoot().getFrameworkVersion();
}

async _getTaskRepository() {
if (!this._taskRepository) {
try {
Expand Down
53 changes: 44 additions & 9 deletions packages/project/lib/graph/helpers/ui5Framework.js
Original file line number Diff line number Diff line change
Expand Up @@ -399,15 +399,6 @@ export default {

const {libraryMetadata} = await resolver.install(referencedLibraries);

if (version) {
process.emit("ui5.project-framework-resolved", {
framework: {
name: frameworkName,
version,
},
});
}

if (log.isLevelEnabled("verbose")) {
const timeDiff = process.hrtime(startTime);
const {default: prettyHrtime} = await import("pretty-hrtime");
Expand Down Expand Up @@ -435,6 +426,50 @@ export default {
log.verbose("Joining framework graph into project graph...");
projectGraph.join(frameworkGraph);
await utils.declareFrameworkDependenciesInGraph(projectGraph);

// A workspace only overrides the resolved version when it actually contributed framework
// libraries; an empty providedLibraryMetadata means ui5-workspace.yaml resolved only
// non-framework libraries, so the configured/overridden `version` still applies.
const workspaceContributedFramework = Boolean(workspace) &&

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really an accurate information? It only states that there are libraries in the workspace resolution, but it doesn't mean that they will be used by our graph (e.g. when they are available, but the project doesn't need them).

Object.keys(providedLibraryMetadata).length > 0;

// Without a workspace contribution, `version` is the effective framework version resolved
// above (it already reflects --framework-version and the ui5.yaml config). With a workspace
// contribution that config is ignored and the type is derived from the libraries the
// workspace actually provided (not the potentially stale ui5.yaml framework name): any
// @sapui5/ library means SAPUI5, which has no reliable distribution version and is left
// empty; otherwise OpenUI5 reports the sap.ui.core version.
let resolvedFrameworkVersion;
if (workspaceContributedFramework) {
const hasSapui5Library = Object.values(providedLibraryMetadata)
.some((metadata) => metadata.id?.startsWith("@sapui5/"));
if (hasSapui5Library) {
resolvedFrameworkVersion = "";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does having workspace defined here always correspond to the usage of framework libraries from it?
If not, this line would be reached when a ui5-workspace.yaml only contains resolutions for non-framework libraries, which IMHO is not correct.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! TBH, I didn't consider external projects for that scenario and now this makes more sense. I have pushed a fix for that with 4f0bd4a

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether the updated logic is working as intended in all scenarios.
When the framework is "SAPUI5", but no @sapui5/ projects are in the workspace (e.g. only openui5 is resolved via workspace), the "SAPUI5" version will be set to the version of "sap.ui.core". In case of a -SNAPSHOT version, this is usually correct, but I didn't we decide that we still rather want to omit the SAPUI5 version in such a case, as we don't certainly know it, and do not want to fallback to the OpenUI5 version? I also might remember this incorrectly, so please correct me when I'm wrong here.

} else {
// OpenUI5: sap.ui.core's version IS the framework version, and is correct whether
// sap.ui.core was overridden by the workspace or installed from the dist.
// Falls back to `version` if sap.ui.core is absent.
resolvedFrameworkVersion = projectGraph.getProject("sap.ui.core")?.getVersion() ?? version ?? "";
}
} else {
resolvedFrameworkVersion = version;
}
projectGraph.setResolvedFrameworkVersion(resolvedFrameworkVersion);

// Emit after workspace-aware resolution so the version reflects the actual resolved
// version (e.g. the linked sap.ui.core for OpenUI5 workspace) rather than the
// configured ui5.yaml version that may differ.
// Always emit when the workspace contributed framework libraries (framework name is known
// even if version is ""), and when no such contribution but a concrete version was resolved.
if (workspaceContributedFramework || resolvedFrameworkVersion) {
process.emit("ui5.project-framework-resolved", {
framework: {
name: frameworkName,
version: resolvedFrameworkVersion,
},
});
}

return projectGraph;
},

Expand Down
9 changes: 9 additions & 0 deletions packages/project/test/lib/build/helpers/MonitoredTaskUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ test.beforeEach((t) => {
getEnv: sinon.stub().callsFake((name) => (name === "SET" ? "on" : undefined)),
getTime: sinon.stub().callsFake((granularity) => (granularity === "year" ? "2026" : "2026-09-25")),
isRootProject: sinon.stub().returns(true),
getResolvedFrameworkVersion: sinon.stub().returns("1.138.0"),
getDependencies: sinon.stub().returns(["dep.a", "dep.b"]),
getProject: sinon.stub().callsFake((name) => {
if (name === undefined || name === "sap.ui.core") {
Expand Down Expand Up @@ -95,6 +96,14 @@ test("records isRootProject reads", (t) => {
]);
});

test("records getResolvedFrameworkVersion reads", (t) => {
const monitored = new MonitoredTaskUtil(t.context.taskUtil);
t.is(monitored.getResolvedFrameworkVersion(), "1.138.0");
t.deepEqual(monitored.getInputRecording(), [
{type: "resolvedFrameworkVersion", name: "", value: "1.138.0"},
]);
});

test("records getDependencies reads, resolving the default project name", (t) => {
const monitored = new MonitoredTaskUtil(t.context.taskUtil);

Expand Down
35 changes: 35 additions & 0 deletions packages/project/test/lib/build/helpers/ProjectBuildContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,19 @@ test("getProject: No name provided", (t) => {
t.is(getProjectStub.callCount, 0, "ProjectGraph#getProject has not been called");
});

test("getResolvedFrameworkVersion delegates to graph", (t) => {
const buildContext = createBuildContextStub({
getGraph: () => ({
getResolvedFrameworkVersion: () => "1.138.0"
})
});
const project = {getName: () => "project", getType: () => "type"};
const projectBuildContext = new ProjectBuildContext(buildContext, project);

t.is(projectBuildContext.getResolvedFrameworkVersion(), "1.138.0",
"Returns the graph's effective framework version");
});

test("getDependencies", (t) => {
const project = {
getName: () => "project",
Expand Down Expand Up @@ -466,6 +479,28 @@ test("resolveInputValue: isRootProject normalizes the boolean", (t) => {
t.is(projectBuildContext.resolveInputValue("isRootProject", ""), "true");
});

test("resolveInputValue: resolvedFrameworkVersion normalizes the version string", (t) => {
const buildContext = createBuildContextStub({
getGraph: () => ({getResolvedFrameworkVersion: () => "1.138.0"})
});
const project = {getName: () => "project", getType: () => "type"};
const projectBuildContext = new ProjectBuildContext(buildContext, project);

t.is(projectBuildContext.resolveInputValue("resolvedFrameworkVersion", ""), "1.138.0",
"Returns the resolved framework version as-is (string passthrough)");
});

test("resolveInputValue: resolvedFrameworkVersion normalizes empty string (SAPUI5 workspace)", (t) => {
const buildContext = createBuildContextStub({
getGraph: () => ({getResolvedFrameworkVersion: () => ""})
});
const project = {getName: () => "project", getType: () => "type"};
const projectBuildContext = new ProjectBuildContext(buildContext, project);

t.is(projectBuildContext.resolveInputValue("resolvedFrameworkVersion", ""), "",
"Returns empty string for SAPUI5 workspace (no distribution version)");
});

test("resolveInputValue: getDependencies normalizes the array via the graph", (t) => {
const getDependencies = sinon.stub().returns(["dep.a", "dep.b"]);
const buildContext = createBuildContextStub({
Expand Down
37 changes: 37 additions & 0 deletions packages/project/test/lib/build/helpers/TaskUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,28 @@ test("getBuildTime returns the build run's raw timestamp", (t) => {
t.is(taskUtil.getBuildTime(), buildTime, "returns the build context's Date instance");
});

test("getResolvedFrameworkVersion delegates to projectBuildContext", (t) => {
const taskUtil = new TaskUtil({
projectBuildContext: {
getResolvedFrameworkVersion: () => "1.138.0"
}
});

t.is(taskUtil.getResolvedFrameworkVersion(), "1.138.0",
"Returns the value from ProjectBuildContext.getResolvedFrameworkVersion()");
});

test("getResolvedFrameworkVersion returns undefined when unset (fallback passes through)", (t) => {
const taskUtil = new TaskUtil({
projectBuildContext: {
getResolvedFrameworkVersion: () => undefined
}
});

t.is(taskUtil.getResolvedFrameworkVersion(), undefined,
"Undefined propagates so the processor uses its own fallback");
});

test("registerCleanupTask", (t) => {
const registerCleanupTaskStub = sinon.stub();
const taskUtil = new TaskUtil({
Expand Down Expand Up @@ -490,6 +512,8 @@ test("getInterface: specVersion 3.0", (t) => {
t.is(interfacedTaskUtil.getEnv, undefined, "getEnv is not provided below specVersion 5.0");
t.is(interfacedTaskUtil.getTime, undefined, "getTime is not provided below specVersion 5.0");
t.is(interfacedTaskUtil.getBuildTime, undefined, "getBuildTime is not provided below specVersion 5.0");
t.is(interfacedTaskUtil.getResolvedFrameworkVersion, undefined,
"getResolvedFrameworkVersion is not provided below specVersion 5.0");

// getProject
const interfacedProject = interfacedTaskUtil.getProject("pony");
Expand Down Expand Up @@ -557,6 +581,7 @@ test("getInterface: specVersion 5.0 exposes getBuildTime", (t) => {
}),
getDependencies: sinon.stub().returns([]),
getBuildTime: () => buildTime,
getResolvedFrameworkVersion: () => "1.138.0",
}
});

Expand All @@ -575,12 +600,17 @@ test("getInterface: specVersion 5.0 exposes getBuildTime", (t) => {
"getEnv",
"getTime",
"getBuildTime",
"getResolvedFrameworkVersion",
], "getBuildTime is added at specVersion 5.0");

t.is(typeof interfacedTaskUtil.getEnv, "function", "function getEnv is provided");
t.is(typeof interfacedTaskUtil.getTime, "function", "function getTime is provided");
t.is(typeof interfacedTaskUtil.getBuildTime, "function", "function getBuildTime is provided");
t.is(interfacedTaskUtil.getBuildTime(), buildTime, "getBuildTime returns the build run's Date");
t.is(typeof interfacedTaskUtil.getResolvedFrameworkVersion, "function",
"function getResolvedFrameworkVersion is provided");
t.is(interfacedTaskUtil.getResolvedFrameworkVersion(), "1.138.0",
"getResolvedFrameworkVersion returns the resolved framework version");
});

test("getReadOnlyInterface: specVersion 5.0 exposes getBuildTime", (t) => {
Expand All @@ -592,6 +622,7 @@ test("getReadOnlyInterface: specVersion 5.0 exposes getBuildTime", (t) => {
}),
getDependencies: sinon.stub().returns([]),
getBuildTime: () => buildTime,
getResolvedFrameworkVersion: () => "1.138.0",
}
});

Expand All @@ -601,9 +632,15 @@ test("getReadOnlyInterface: specVersion 5.0 exposes getBuildTime", (t) => {
t.is(typeof interfacedTaskUtil.getTime, "function", "function getTime is provided");
t.is(typeof interfacedTaskUtil.getBuildTime, "function", "function getBuildTime is provided");
t.is(interfacedTaskUtil.getBuildTime(), buildTime, "getBuildTime returns the build run's Date");
t.is(typeof interfacedTaskUtil.getResolvedFrameworkVersion, "function",
"function getResolvedFrameworkVersion is provided");
t.is(interfacedTaskUtil.getResolvedFrameworkVersion(), "1.138.0",
"getResolvedFrameworkVersion returns the resolved framework version");

const readOnlyBelow5 = taskUtil.getReadOnlyInterface(getSpecificationVersion("3.0"));
t.is(readOnlyBelow5.getEnv, undefined, "getEnv is not provided below specVersion 5.0");
t.is(readOnlyBelow5.getTime, undefined, "getTime is not provided below specVersion 5.0");
t.is(readOnlyBelow5.getBuildTime, undefined, "getBuildTime is not provided below specVersion 5.0");
t.is(readOnlyBelow5.getResolvedFrameworkVersion, undefined,
"getResolvedFrameworkVersion is not provided below specVersion 5.0");
});
34 changes: 34 additions & 0 deletions packages/project/test/lib/graph/ProjectGraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -1934,3 +1934,37 @@ test("Seal/isSealed", async (t) => {
const extension = graph.getExtension("extension.b");
t.is(extension, undefined, "extension.b should not be added");
});

test("getResolvedFrameworkVersion: returns value set by setResolvedFrameworkVersion", async (t) => {
const {ProjectGraph} = t.context;
const rootProject = await createProject("root");
const graph = new ProjectGraph({rootProjectName: "root"});
await graph.addProject(rootProject);

graph.setResolvedFrameworkVersion("1.138.0");
t.is(graph.getResolvedFrameworkVersion(), "1.138.0");
});

test("getResolvedFrameworkVersion: falls back to root project getFrameworkVersion() when unset", async (t) => {
const {ProjectGraph} = t.context;
const rootProject = await createProject("root");
// Stub getFrameworkVersion on the project instance
rootProject.getFrameworkVersion = () => "1.136.0";
const graph = new ProjectGraph({rootProjectName: "root"});
await graph.addProject(rootProject);

t.is(graph.getResolvedFrameworkVersion(), "1.136.0",
"Should fall back to the root project's configured version");
});

test("getResolvedFrameworkVersion: setResolvedFrameworkVersion overrides configured root version", async (t) => {
const {ProjectGraph} = t.context;
const rootProject = await createProject("root");
rootProject.getFrameworkVersion = () => "1.136.0";
const graph = new ProjectGraph({rootProjectName: "root"});
await graph.addProject(rootProject);

graph.setResolvedFrameworkVersion("1.138.0");
t.is(graph.getResolvedFrameworkVersion(), "1.138.0",
"Set value must take precedence over configured root version");
});
Loading
Loading