diff --git a/internal/documentation/docs/pages/Server.md b/internal/documentation/docs/pages/Server.md
index 3900de4ee72..5382121f102 100644
--- a/internal/documentation/docs/pages/Server.md
+++ b/internal/documentation/docs/pages/Server.md
@@ -28,7 +28,9 @@ Please be aware of the following risks when using the server:
::: info Removed Middleware
The `serveThemes` middleware has been removed in UI5 CLI v5. Theme compilation is now handled by the `buildThemes` build task, which pre-compiles all theme CSS files. The resulting CSS files (including `library.css`, `library-RTL.css`, `library-parameters.json`, and CSS Variables resources) are served via the `serveResources` middleware, providing the same functionality with better performance through build-time compilation and caching.
-Custom middleware previously referencing `serveThemes` via `beforeMiddleware` or `afterMiddleware` will continue to work with automatic remapping and a deprecation warning. See the [v5 migration guide](../updates/migrate-v5.md) for details.
+The `testRunner` middleware has also been removed in UI5 CLI v5. The QUnit TestRunner (`testrunner.html`, `testrunner.css`, `TestRunner.js`) is now provided by the UI5 framework (`sap.ui.core`) and is served via the `serveResources` middleware.
+
+Custom middleware previously referencing `serveThemes` or `testRunner` via `beforeMiddleware` or `afterMiddleware` will continue to work with automatic remapping and a deprecation warning. See the [v5 migration guide](../updates/migrate-v5.md) for details.
:::
All available standard middleware are listed below in the order of their execution.
@@ -43,7 +45,6 @@ A project can also add custom middleware to the server by using the [Custom Serv
| `liveReloadClient` | See chapter [liveReload](#livereload) |
| `discovery` | See chapter [discovery](#discovery) |
| `serveResources` | See chapter [serveResources](#serveresources) |
-| `testRunner` | See chapter [testRunner](#testrunner) |
| `versionInfo` | See chapter [versionInfo](#versioninfo) |
| `nonReadRequests` | See chapter [nonReadRequests](#nonreadrequests) |
| `serveIndex` | See chapter [serveIndex](#serveindex) |
@@ -105,9 +106,6 @@ The following file content transformations are executed:
- Escaping non-ASCII characters in `.properties` translation files based on a project's [configuration](./Configuration.md#encoding-of-properties-files)
- Enhancing the `manifest.json` with supported locales determined by available `.properties` [translation files](./Builder.md#generation-of-supported-locales)
-### testRunner
-Serves a static version of the UI5 QUnit TestRunner at `/test-resources/sap/ui/qunit/testrunner.html`.
-
### versionInfo
Generates and serves the version info file `/resources/sap-ui-version.json`, which is required for several framework functionalities.
diff --git a/internal/documentation/docs/updates/migrate-v5.md b/internal/documentation/docs/updates/migrate-v5.md
index 0b735e36608..21a97e1ff62 100644
--- a/internal/documentation/docs/updates/migrate-v5.md
+++ b/internal/documentation/docs/updates/migrate-v5.md
@@ -19,6 +19,7 @@ Or update your global install via: `npm i --global @ui5/cli@next`
- **@ui5/server: Live Reload is enabled by default for `ui5 serve`**
+- **@ui5/server: Standard middleware `serveThemes` and `testRunner` have been removed**
## Node.js and npm Version Support
@@ -268,21 +269,23 @@ If your project uses a custom middleware that provides live reload functionality
The following middleware has been removed from the [standard middlewares list](../pages/Server.md#standard-middleware):
* `serveThemes` — The `buildThemes` build task now handles theme compilation (LESS to CSS). Because server sessions now also perform builds, this task runs during a server start instead of on demand during runtime. The resulting CSS files are served by the `serveResources` middleware. This change improves performance through build-time compilation and caching while maintaining the same functionality.
+* `testRunner` — The UI5 QUnit TestRunner resources (`testrunner.html`, `testrunner.css`, `TestRunner.js`) are now provided by the UI5 framework (`sap.ui.core`) and served via the `serveResources` middleware. All supported UI5 releases ship the relevant testrunner resources, so the dedicated middleware is no longer needed.
**Backward Compatibility:**
-If your project or any custom middleware references a removed middleware via `beforeMiddleware` or `afterMiddleware`, UI5 CLI will automatically remap the reference to the nearest remaining middleware and log a deprecation warning. Your custom middleware will still be executed in the expected order.
+If your project or any custom middleware references a removed middleware via `beforeMiddleware` or `afterMiddleware`, UI5 CLI keeps a no-op placeholder in the middleware execution order at the original slot. The custom middleware is executed in the same position as before and a deprecation warning is logged.
**What Changed:**
- Theme CSS files (`library.css`, `library-RTL.css`, etc.) are now **pre-built**
-- Files are served via `serveResources` instead of being compiled on demand
-- The same CSS files are available at the same URLs as before
+- Theme files are served via `serveResources` instead of being compiled on demand
+- TestRunner resources are served via `serveResources` from the UI5 framework instead of being shipped with UI5 CLI
**Recommended Action:**
Update your `ui5.yaml` configuration to reference an existing middleware instead.
| Removed Middleware | Replacement Behavior | Recommended `afterMiddleware` |
| ------------------ | -------------------- | ----------------------------- |
-| `serveThemes` | CSS files pre-built by `buildThemes` task and served via `serveResources` | `testRunner` |
+| `serveThemes` | CSS files pre-built by `buildThemes` task and served via `serveResources` | `serveResources` |
+| `testRunner` | TestRunner resources served via `serveResources` from the UI5 framework | `serveResources` |
## Learn More
diff --git a/packages/server/REUSE.toml b/packages/server/REUSE.toml
index b4233a3d79d..43e6c6253ca 100644
--- a/packages/server/REUSE.toml
+++ b/packages/server/REUSE.toml
@@ -16,8 +16,3 @@ precedence = "aggregate"
SPDX-FileCopyrightText = ["2010 Sencha Inc.", "2011 LearnBoost", "2011 TJ Holowaychuk", "2014-2015 Douglas Christopher Wilson"]
SPDX-License-Identifier = "MIT"
-[[annotations]]
-path = "lib/middleware/testRunner/**"
-precedence = "aggregate"
-SPDX-FileCopyrightText = "2026 SAP SE or an SAP affiliate company and OpenUI5 contributors"
-SPDX-License-Identifier = "Apache-2.0"
diff --git a/packages/server/eslint.config.js b/packages/server/eslint.config.js
index 436d653b035..d4a5c181426 100644
--- a/packages/server/eslint.config.js
+++ b/packages/server/eslint.config.js
@@ -3,13 +3,6 @@ import eslintCommonConfig from "../../eslint.common.config.js";
export default [
...eslintCommonConfig, // Load common ESLint config
- {
- // Add project-specific ESLint config rules here
- // in order to override common config
- ignores: [
- "lib/middleware/testRunner/",
- ]
- },
{
// Live reload client script runs in the browser, not Node.js
files: ["lib/liveReload/client.js"],
diff --git a/packages/server/lib/middleware/MiddlewareManager.js b/packages/server/lib/middleware/MiddlewareManager.js
index 55f315f546e..f4c651a8f22 100644
--- a/packages/server/lib/middleware/MiddlewareManager.js
+++ b/packages/server/lib/middleware/MiddlewareManager.js
@@ -4,15 +4,6 @@ import {getLogger} from "@ui5/logger";
const hasOwn = Function.prototype.call.bind(Object.prototype.hasOwnProperty);
const log = getLogger("server:MiddlewareManager");
-/**
- * Mapping of removed standard middleware names to their predecessor and successor
- * in the original execution order. Used to remap custom middleware references
- * to the nearest remaining middleware when a removed middleware is referenced.
- */
-const LEGACY_MIDDLEWARE_MAPPING = {
- serveThemes: {before: "testRunner", after: "versionInfo"}
-};
-
/**
* @private
* @typedef {object} MiddlewareResources
@@ -49,6 +40,7 @@ class MiddlewareManager {
this.middleware = Object.create(null);
this.middlewareExecutionOrder = [];
+ this.legacyMiddlewarePlaceholders = new Set();
this.middlewareUtil = new MiddlewareUtil({graph, project: rootProject});
}
@@ -64,10 +56,15 @@ class MiddlewareManager {
await this.addStandardMiddleware();
await this.addCustomMiddleware();
- return this.middlewareExecutionOrder.map((name) => {
- const m = this.middleware[name];
- app.use(m.mountPath, m.middleware);
- });
+ // Strip legacy placeholders for removed standard middlewares — they only existed
+ // in middlewareExecutionOrder to preserve slots for custom beforeMiddleware /
+ // afterMiddleware references and are not backed by an actual middleware.
+ return this.middlewareExecutionOrder
+ .filter((name) => !this.legacyMiddlewarePlaceholders.has(name))
+ .map((name) => {
+ const m = this.middleware[name];
+ app.use(m.mountPath, m.middleware);
+ });
}
/**
@@ -86,7 +83,7 @@ class MiddlewareManager {
customMiddleware, wrapperCallback, mountPath = "/",
beforeMiddleware, afterMiddleware
} = {}) {
- if (this.middleware[middlewareName]) {
+ if (this.#isMiddlewareNameKnown(middlewareName)) {
throw new Error(`A middleware with the name ${middlewareName} has already been added`);
}
@@ -107,8 +104,7 @@ class MiddlewareManager {
}
if (beforeMiddleware || afterMiddleware) {
- let refMiddlewareName = beforeMiddleware || afterMiddleware;
- const originalRefMiddlewareName = refMiddlewareName; // Store original before any remapping
+ const refMiddlewareName = beforeMiddleware || afterMiddleware;
let refMiddlewareIdx = this.middlewareExecutionOrder.indexOf(refMiddlewareName);
if (refMiddlewareName === "connectUi5Proxy") {
@@ -119,33 +115,23 @@ class MiddlewareManager {
`Please see the migration guide at https://ui5.github.io/cli/updates/migrate-v3/`);
}
- // Handle legacy middleware with graceful fallback
- const legacyMapping = LEGACY_MIDDLEWARE_MAPPING[refMiddlewareName];
- if (legacyMapping) {
- // Replace with the appropriate fallback based on reference type
- refMiddlewareName = afterMiddleware ? legacyMapping.before : legacyMapping.after;
-
+ // Warn when a removed standard middleware is referenced. The reference still resolves
+ // because the removed middleware is kept as a placeholder in middlewareExecutionOrder
+ // (see #addLegacyMiddlewarePlaceholder), preserving the original execution slot.
+ if (this.legacyMiddlewarePlaceholders.has(refMiddlewareName)) {
log.warn(
- `Standard middleware "${originalRefMiddlewareName}" has been removed. ` +
+ `Standard middleware "${refMiddlewareName}" has been removed. ` +
`Custom middleware "${middlewareName}" defined in project ` +
- `"${this.middlewareUtil.getProject()}" references it and ` +
- `is now placed ${afterMiddleware ? "after" : "before"} ` +
- `"${refMiddlewareName}" instead. ` +
+ `"${this.middlewareUtil.getProject()}" still references it. ` +
+ `The custom middleware will be executed in the slot the removed middleware ` +
+ `originally occupied, but the reference should be updated. ` +
`For details, see the migration guide at ` +
`https://ui5.github.io/cli/next/updates/migrate-v5`);
}
- refMiddlewareIdx = this.middlewareExecutionOrder.indexOf(refMiddlewareName);
-
if (refMiddlewareIdx === -1) {
- // Provide clear error message, including remapping context if applicable
- const errorMsg = legacyMapping ?
- `Could not find fallback middleware "${refMiddlewareName}" ` +
- `(mapped from removed middleware "${originalRefMiddlewareName}"), ` +
- `referenced by custom middleware "${middlewareName}"` :
- `Could not find middleware ${refMiddlewareName}, referenced by custom ` +
- `middleware ${middlewareName}`;
- throw new Error(errorMsg);
+ throw new Error(`Could not find middleware ${refMiddlewareName}, referenced by custom ` +
+ `middleware ${middlewareName}`);
}
if (afterMiddleware) {
// Insert after index of referenced middleware
@@ -272,7 +258,8 @@ class MiddlewareManager {
});
}
});
- await this.addMiddleware("testRunner");
+ this.#addLegacyMiddlewarePlaceholder("testRunner");
+ this.#addLegacyMiddlewarePlaceholder("serveThemes");
await this.addMiddleware("versionInfo", {
mountPath: "/resources/sap-ui-version.json"
});
@@ -327,11 +314,11 @@ class MiddlewareManager {
}
let middlewareName = middlewareDef.name;
- if (this.middleware[middlewareName]) {
+ if (this.#isMiddlewareNameKnown(middlewareName)) {
// Middleware is already known
// => add a suffix to allow for multiple configurations of the same middleware
let suffixCounter = 0;
- while (this.middleware[middlewareName]) {
+ while (this.#isMiddlewareNameKnown(middlewareName)) {
suffixCounter++; // Start at 1
middlewareName = `${middlewareDef.name}--${suffixCounter}`;
}
@@ -363,6 +350,35 @@ class MiddlewareManager {
});
}
}
+
+ /**
+ * Inserts a no-op placeholder for a removed standard middleware into the execution order.
+ * Placeholders preserve the original slot so custom middlewares referencing the removed
+ * middleware via beforeMiddleware / afterMiddleware keep their intended position.
+ * They are stripped in applyMiddleware() before mounting on the express app.
+ *
+ * @private
+ * @param {string} middlewareName Name of the removed standard middleware
+ */
+ #addLegacyMiddlewarePlaceholder(middlewareName) {
+ this.legacyMiddlewarePlaceholders.add(middlewareName);
+ this.middlewareExecutionOrder.push(middlewareName);
+ }
+
+ /**
+ * Returns whether the given middleware name is already known — either registered
+ * in this.middleware or occupying a legacy placeholder slot.
+ * Placeholder names are treated as known to avoid collisions in the execution order:
+ * a custom middleware named e.g. "testRunner" would otherwise clash with the
+ * placeholder inserted to preserve its original slot.
+ *
+ * @private
+ * @param {string} middlewareName Middleware name to check
+ * @returns {boolean}
+ */
+ #isMiddlewareNameKnown(middlewareName) {
+ return !!(this.middleware[middlewareName] || this.legacyMiddlewarePlaceholders.has(middlewareName));
+ }
}
export default MiddlewareManager;
diff --git a/packages/server/lib/middleware/middlewareRepository.js b/packages/server/lib/middleware/middlewareRepository.js
index 613af7a738e..a4a28fac5d0 100644
--- a/packages/server/lib/middleware/middlewareRepository.js
+++ b/packages/server/lib/middleware/middlewareRepository.js
@@ -7,7 +7,6 @@ const middlewareInfos = {
serveIndex: {path: "./serveIndex.js"},
discovery: {path: "./discovery.js"},
versionInfo: {path: "./versionInfo.js"},
- testRunner: {path: "./testRunner.js"},
nonReadRequests: {path: "./nonReadRequests.js"}
};
diff --git a/packages/server/lib/middleware/testRunner.js b/packages/server/lib/middleware/testRunner.js
deleted file mode 100644
index 71bb4f4b79b..00000000000
--- a/packages/server/lib/middleware/testRunner.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import {promisify} from "node:util";
-import fs from "graceful-fs";
-const readFile = promisify(fs.readFile);
-import {fileURLToPath} from "node:url";
-import mime from "mime-types";
-import parseurl from "parseurl";
-import {getLogger} from "@ui5/logger";
-const log = getLogger("server:middleware:testRunner");
-
-const testRunnerResourceRegEx = /\/test-resources\/sap\/ui\/qunit\/(testrunner\.(html|css)|TestRunner.js)$/;
-const resourceCache = Object.create(null);
-
-function serveResource(res, resourcePath, resourceContent) {
- const type = mime.lookup(resourcePath) || "application/octet-stream";
- const charset = mime.charset(type);
- const contentType = type + (charset ? "; charset=" + charset : "");
-
- // resources served by this middleware do not change often
- res.setHeader("Cache-Control", "public, max-age=1800");
-
- res.setHeader("Content-Type", contentType);
- res.end(resourceContent);
-}
-
-/**
- * Creates and returns the middleware to serve a resource index.
- *
- * @module @ui5/server/middleware/testRunner
- * @param {object} parameters Parameters
- * @param {object} parameters.resources Contains the resource reader or collection to access project related files
- * @returns {Function} Returns a server middleware closure.
- */
-function createMiddleware({resources}) {
- return async function(req, res, next) {
- try {
- const pathname = parseurl(req).pathname;
- const parts = testRunnerResourceRegEx.exec(pathname);
- const resourceName = parts && parts[1];
-
- if (resourceName) { // either "testrunner.html", "testrunner.css" or "TestRunner.js" (case sensitive!)
- log.verbose(`Serving ${pathname}`);
- let pResource;
- if (!resourceCache[pathname]) {
- const filePath = fileURLToPath(new URL(`./testRunner/${resourceName}`, import.meta.url));
- pResource = readFile(filePath, {encoding: "utf8"});
- resourceCache[pathname] = pResource;
- } else {
- pResource = resourceCache[pathname];
- }
-
- const resourceContent = await pResource;
- serveResource(res, pathname, resourceContent);
- } else {
- next();
- }
- } catch (err) {
- next(err);
- }
- };
-}
-
-export default createMiddleware;
diff --git a/packages/server/lib/middleware/testRunner/TestRunner.js b/packages/server/lib/middleware/testRunner/TestRunner.js
deleted file mode 100644
index d7efe7ed8f9..00000000000
--- a/packages/server/lib/middleware/testRunner/TestRunner.js
+++ /dev/null
@@ -1,674 +0,0 @@
-(function(window) {
- "use strict";
-
- /*global CollectGarbage, Handlebars */
- /* eslint radix: 0 */
-
- /*
- * Simulate the JSUnit Testsuite to collect the available
- * test pages per Suite
- */
- window.jsUnitTestSuite = function() {};
- window.jsUnitTestSuite.prototype.getTestPages = function() {
- return this.aPages;
- };
- window.jsUnitTestSuite.prototype.addTestPage = function(sTestPage) {
- this.aPages = this.aPages || [];
- // in case of running in the root context the testsuites right now
- // generate an invalid URL because they assume that test-resources is
- // the context path - this section makes sure to remove the duplicate
- // test-resources segments in the path
- if (sTestPage.indexOf("/test-resources/test-resources") === 0 || sTestPage.indexOf("/test-resources/resources") === 0) {
- sTestPage = sTestPage.substr("/test-resources".length);
- }
- this.aPages.push(sTestPage);
- };
-
- function XHRQueue(iMaxParallelRequests, iWaitTime) {
- this.iLimit = iMaxParallelRequests === undefined ? Infinity : iMaxParallelRequests;
- this.iWaitTime = iWaitTime === undefined ? 0 : iWaitTime;
- this.aPendingTasks = [];
- this.oRunningTasks = new Set();
- this.iLastTaskExecution = -Infinity;
- }
-
- XHRQueue.prototype.ajax = function(sUrl, options) {
- var oTask = {
- url: sUrl,
- options: options
- };
- oTask.promise = new Promise(function(resolve, reject) {
- oTask.resolve = resolve;
- oTask.reject = reject;
- });
- this.aPendingTasks.push(oTask);
- this._processNext();
- return oTask.promise;
- };
-
- XHRQueue.prototype._processNext = function() {
- var iNow = Date.now();
- var iDelay = iNow - this.iLastTaskExecution;
- if ( iDelay < this.iWaitTime ) {
- setTimeout(function() {
- this._processNext();
- }.bind(this), iDelay);
- return;
- }
- if ( this.aPendingTasks.length > 0 && this.oRunningTasks.size < this.iLimit ) {
- var oTask = this.aPendingTasks.shift();
- this.oRunningTasks.add(oTask);
- this.iLastTaskExecution = iNow;
- Promise.resolve(jQuery.ajax(oTask.url, oTask.options))
- .then(oTask.resolve, oTask.reject)
- .finally(function() {
- this.oRunningTasks.delete(oTask);
- this._processNext();
- }.bind(this));
- }
- };
-
- var oXHRQueue = new XHRQueue(50, 2);
-
- /*
- * Template for test results
- */
- var sResultsTemplate = "{{#tests}}" +
- "
{{header}}
" + - "{{result.TestName}} ({{result.Failed}} ,{{result.Passed}} ,{{result.All}})
" + - " Rerun" + - "