diff --git a/internal/documentation/docs/pages/Server.md b/internal/documentation/docs/pages/Server.md index f3dc2ed6865..64fd885fe4a 100644 --- a/internal/documentation/docs/pages/Server.md +++ b/internal/documentation/docs/pages/Server.md @@ -25,6 +25,12 @@ Please be aware of the following risks when using the server: ## Standard Middleware +::: info Removed Middleware +The `serveThemes` middleware has been removed in UI5 CLI v5. Theme compilation is now handled by the `buildThemes` build task during the incremental build, 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. +::: + All available standard middleware are listed below in the order of their execution. A project can also add custom middleware to the server by using the [Custom Server Middleware Extensibility](./extensibility/CustomServerMiddleware.md). @@ -37,7 +43,6 @@ A project can also add custom middleware to the server by using the [Custom Serv | `discovery` | See chapter [discovery](#discovery) | | `serveResources` | See chapter [serveResources](#serveresources) | | `testRunner` | See chapter [testRunner](#testrunner) | -| `serveThemes` | See chapter [serveThemes](#servethemes) | | `versionInfo` | See chapter [versionInfo](#versioninfo) | | `nonReadRequests` | See chapter [nonReadRequests](#nonreadrequests) | | `serveIndex` | See chapter [serveIndex](#serveindex) | @@ -73,11 +78,6 @@ The following file content transformations are executed: ### testRunner Serves a static version of the UI5 QUnit TestRunner at `/test-resources/sap/ui/qunit/testrunner.html`. -### serveThemes -Compiles CSS files for themes on-the-fly from the source `*.less` files. - -Changes made to these `*.less` files while the server is running will automatically lead to the re-compilation of the relevant CSS files when requested again. - ### 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 b4de575af10..68a27c2fdff 100644 --- a/internal/documentation/docs/updates/migrate-v5.md +++ b/internal/documentation/docs/updates/migrate-v5.md @@ -198,6 +198,27 @@ Delete the custom `test/Test.qunit.html` file from your test directory. This fil Depending on your project setup, you might need to update additional paths in configuration files or test runners to reflect the new structure. The test suite is now served under the standard `/test-resources/` path with the component's full namespace (e.g. `/test-resources/sap/ui/demo/todo/testsuite.qunit.html`). +## Removal of Standard Server Middleware + +The following middleware has been removed from the [standard middlewares list](../pages/Server.md#standard-middleware): + +* `serveThemes` — Theme compilation (LESS to CSS) is now handled by the `buildThemes` build task during the incremental build, rather than on-demand during runtime. The resulting CSS files are served via the `serveResources` middleware. This change improves performance through build-time compilation and caching while maintaining the same functionality. + +**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. + +**What Changed:** +- Theme CSS files (`library.css`, `library-RTL.css`, etc.) are now **pre-built** during the incremental build +- Files are served via `serveResources` instead of being compiled on-demand +- The same CSS files are available at the same URLs as before + +**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` | + ## Learn More - [Project: Type `component`](../pages/Project#component) diff --git a/packages/server/lib/middleware/MiddlewareManager.js b/packages/server/lib/middleware/MiddlewareManager.js index 10348b82154..397fc6422b6 100644 --- a/packages/server/lib/middleware/MiddlewareManager.js +++ b/packages/server/lib/middleware/MiddlewareManager.js @@ -2,6 +2,16 @@ import middlewareRepository from "./middlewareRepository.js"; import MiddlewareUtil from "./MiddlewareUtil.js"; 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 @@ -25,7 +35,7 @@ class MiddlewareManager { sendSAPTargetCSP: false, serveCSPReports: false }}) { - if (!graph || !rootProject || !sources || !resources || !resources.all || + if (!graph || !rootProject || !resources || !resources.all || !resources.rootProject || !resources.dependencies) { throw new Error("[MiddlewareManager]: One or more mandatory parameters not provided"); } @@ -96,7 +106,7 @@ class MiddlewareManager { } if (beforeMiddleware || afterMiddleware) { - const refMiddlewareName = beforeMiddleware || afterMiddleware; + let refMiddlewareName = beforeMiddleware || afterMiddleware; let refMiddlewareIdx = this.middlewareExecutionOrder.indexOf(refMiddlewareName); if (refMiddlewareName === "connectUi5Proxy") { @@ -106,6 +116,26 @@ class MiddlewareManager { `has been removed in this version of UI5 CLI and can't be referenced anymore. ` + `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) { + const originalRef = refMiddlewareName; + // Replace with the appropriate fallback based on reference type + refMiddlewareName = afterMiddleware ? legacyMapping.before : legacyMapping.after; + + log.warn( + `Standard middleware "${originalRef}" has been removed. ` + + `Custom middleware "${middlewareName}" defined in project ` + + `"${this.middlewareUtil.getProject()}" references it and ` + + `is now placed ${afterMiddleware ? "after" : "before"} ` + + `"${refMiddlewareName}" instead. ` + + `For details, see the migration guide at ` + + `https://ui5.github.io/cli/next/updates/migrate-v5`); + } + + refMiddlewareIdx = this.middlewareExecutionOrder.indexOf(refMiddlewareName); + if (refMiddlewareIdx === -1) { throw new Error(`Could not find middleware ${refMiddlewareName}, referenced by custom ` + `middleware ${middlewareName}`); @@ -220,8 +250,6 @@ class MiddlewareManager { }); await this.addMiddleware("serveResources"); await this.addMiddleware("testRunner"); - // TODO: Allow to still reference 'serveThemes' middleware in custom middleware - // await this.addMiddleware("serveThemes"); await this.addMiddleware("versionInfo", { mountPath: "/resources/sap-ui-version.json" }); diff --git a/packages/server/lib/middleware/middlewareRepository.js b/packages/server/lib/middleware/middlewareRepository.js index a9e0d25a1b1..863380de18e 100644 --- a/packages/server/lib/middleware/middlewareRepository.js +++ b/packages/server/lib/middleware/middlewareRepository.js @@ -6,7 +6,6 @@ const middlewareInfos = { serveIndex: {path: "./serveIndex.js"}, discovery: {path: "./discovery.js"}, versionInfo: {path: "./versionInfo.js"}, - serveThemes: {path: "./serveThemes.js"}, testRunner: {path: "./testRunner.js"}, nonReadRequests: {path: "./nonReadRequests.js"} }; diff --git a/packages/server/lib/middleware/serveThemes.js b/packages/server/lib/middleware/serveThemes.js deleted file mode 100644 index 57771224805..00000000000 --- a/packages/server/lib/middleware/serveThemes.js +++ /dev/null @@ -1,123 +0,0 @@ -import {ThemeBuilder} from "@ui5/builder/processors/themeBuilder"; -import fsInterface from "@ui5/fs/fsInterface"; -import {basename, dirname} from "node:path/posix"; -import etag from "etag"; -import fresh from "fresh"; -import parseurl from "parseurl"; - -function isFresh(req, res) { - return fresh(req.headers, { - "etag": res.getHeader("ETag") - }); -} - -// List of experimental css variables resources that should activate the "cssVariables" build -const cssVariablesThemeResources = [ - "css_variables.source.less", - "css_variables.css", - "library_skeleton.css", - "library_skeleton-RTL.css" -]; - -// List of resources that should be handled by the middleware -const themeResources = [ - "library.css", - "library-RTL.css", - "library-parameters.json", - ...cssVariablesThemeResources -]; - -/** - * Creates and returns the middleware to build themes. - * - * The theme is built in realtime. If a less file was modified, the theme build is triggered to rebuild the theme. - * - * @module @ui5/server/middleware/serveThemes - * @param {object} parameters Parameters - * @param {@ui5/server/internal/MiddlewareManager.middlewareResources} parameters.resources Parameters - * @param {object} parameters.middlewareUtil Specification version dependent interface to a - * [MiddlewareUtil]{@link @ui5/server/middleware/MiddlewareUtil} instance - * @returns {Function} Returns a server middleware closure. - */ -function createMiddleware({resources, middlewareUtil}) { - const builder = new ThemeBuilder({ - fs: fsInterface(resources.all) - }); - const buildOptions = Object.create(null); - - const currentRequests = Object.create(null); - - async function buildTheme(pathname) { - const filename = basename(pathname); - - if (cssVariablesThemeResources.includes(filename) && !buildOptions.cssVariables) { - // Activate CSS Variables build the first time a relevant resource is requested - buildOptions.cssVariables = true; - // Clear the cache to ensure that the build is executed again with "cssVariables: true" - builder.clearCache(); - } - - const sourceLessPath = dirname(pathname) + "/library.source.less"; - const sourceLessResource = await resources.all.byPath(sourceLessPath); - if (!sourceLessResource) { // Not found - return; - } - - const createdResources = await builder.build([sourceLessResource], buildOptions); - - // Pick requested file resource - const resource = createdResources.find((res) => basename(res.getPath()) === filename); - if (!resource) { - throw new Error(`Theme Build did not return requested file "${pathname}"`); - } - - return resource; - } - - async function sendResponse(req, res, resource) { - const resourcePath = resource.getPath(); - const {contentType} = middlewareUtil.getMimeInfo(resourcePath); - res.setHeader("Content-Type", contentType); - - const content = await resource.getBuffer(); - - res.setHeader("ETag", etag(content)); - - if (isFresh(req, res)) { - // client has a fresh copy of the resource - res.statusCode = 304; - res.end(); - return; - } - - res.end(content); - } - - return async function theme(req, res, next) { - try { - const pathname = parseurl(req).pathname; - const filename = basename(pathname); - if (!themeResources.includes(filename)) { - next(); - return; - } - - if (!currentRequests[pathname]) { - currentRequests[pathname] = buildTheme(pathname); - } - - const resource = await currentRequests[pathname]; - if (!resource) { - next(); - } else { - await sendResponse(req, res, resource); - } - - delete currentRequests[pathname]; - } catch (err) { - next(err); - } - }; -} - -export default createMiddleware; diff --git a/packages/server/test/lib/server/main.js b/packages/server/test/lib/server/main.js index 6a8abe32f4d..acba0d1b91a 100644 --- a/packages/server/test/lib/server/main.js +++ b/packages/server/test/lib/server/main.js @@ -12,6 +12,12 @@ test.before(async (t) => { cwd: "./test/fixtures/application.a" }); + // Wrap graph.serve to enable CSS Variables for theme tests + const originalServe = graph.serve.bind(graph); + graph.serve = function(options) { + return originalServe({...options, cssVariables: true}); + }; + server = await serve(graph, { port: 3333 }); @@ -124,11 +130,19 @@ test("Get app_pages from discovery middleware (/discovery/app_pages)", async (t) throw new Error(res.error); } t.is(res.statusCode, 200, "Correct HTTP status code"); + // Note: With BuildServer/incremental build, additional resources are discovered + // including those under /resources/id1/ namespace t.deepEqual(res.body, { "app_pages": [ { "entry": "index.html" }, + { + "entry": "resources/id1/index.html" + }, + { + "entry": "resources/id1/versionTest.html" + }, { "entry": "versionTest.html" } @@ -250,114 +264,117 @@ test("Get sap-ui-version.json from versionInfo middleware (/resources/sap-ui-ver }, "Correct response"); }); -test("Get library.css from theme middleware (/resources/library/a/themes/base/library.css)", async (t) => { +test("Get library.css (built by buildThemes task) (/resources/library/a/themes/base/library.css)", async (t) => { const res = await request.get("/resources/library/a/themes/base/library.css"); if (res.error) { throw new Error(res.error); } t.is(res.statusCode, 200, "Correct HTTP status code"); t.regex(res.headers["content-type"], /css/, "Correct content type"); - t.is(res.text, `.library-a-foo { - color: #fafad2; - padding: 1px 2px 3px 4px; -} - + // CSS is minified by default in buildThemes task + t.is(res.text, `.library-a-foo{color:#fafad2;padding:1px 2px 3px 4px} /* Inline theming parameters */ #sap-ui-theme-library\\.a{background-image:url('data:text/plain;utf-8,%7B%22libraryAColor1%22%3A%22%23fafad2%22%7D')} `, "Correct response"); }); -test("Get library-RTL.css from theme middleware (/resources/library/a/themes/base/library-RTL.css)", async (t) => { - const res = await request.get("/resources/library/a/themes/base/library-RTL.css"); +test("Get library-RTL.css (built by buildThemes task) " + + "(/resources/library/a/themes/base/library-RTL.css)", async (t) => { + const res = await request.get( + "/resources/library/a/themes/base/library-RTL.css", + ); if (res.error) { throw new Error(res.error); } t.is(res.statusCode, 200, "Correct HTTP status code"); t.regex(res.headers["content-type"], /css/, "Correct content type"); - t.is(res.text, `.library-a-foo { - color: #fafad2; - padding: 1px 4px 3px 2px; -} - + // CSS is minified by default in buildThemes task + t.is( + res.text, + `.library-a-foo{color:#fafad2;padding:1px 4px 3px 2px} /* Inline theming parameters */ #sap-ui-theme-library\\.a{background-image:url('data:text/plain;utf-8,%7B%22libraryAColor1%22%3A%22%23fafad2%22%7D')} -`, "Correct response"); +`, + "Correct response", + ); }); -test("Get library-parameters.json from theme middleware (/resources/library/a/themes/base/library-parameters.json)", - async (t) => { - const res = await request.get("/resources/library/a/themes/base/library-parameters.json"); - if (res.error) { - throw new Error(res.error); - } - t.is(res.statusCode, 200, "Correct HTTP status code"); - t.regex(res.headers["content-type"], /json/, "Correct content type"); - t.deepEqual(res.body, { - libraryAColor1: "#fafad2" - }, "Correct response"); - }); +test("Get library-parameters.json (built by buildThemes task) " + + "(/resources/library/a/themes/base/library-parameters.json)", +async (t) => { + const res = await request.get( + "/resources/library/a/themes/base/library-parameters.json"); + if (res.error) { + throw new Error(res.error); + } + t.is(res.statusCode, 200, "Correct HTTP status code"); + t.regex(res.headers["content-type"], /json/, "Correct content type"); + t.deepEqual(res.body, { + libraryAColor1: "#fafad2" + }, "Correct response"); +}); -test("Get css_variables.source.less from theme middleware (/resources/library/a/themes/base/css_variables.source.less)", - async (t) => { - const res = await request.get("/resources/library/a/themes/base/css_variables.source.less"); - if (res.error) { - throw new Error(res.error); - } - t.is(res.statusCode, 200, "Correct HTTP status code"); - t.regex(res.headers["content-type"], /less/, "Correct content type"); - t.is(res.text, `@libraryAColor1: #fafad2; +test("Get css_variables.source.less (built by buildThemes task) " + + "(/resources/library/a/themes/base/css_variables.source.less)", +async (t) => { + const res = await request.get( + "/resources/library/a/themes/base/css_variables.source.less"); + if (res.error) { + throw new Error(res.error); + } + t.is(res.statusCode, 200, "Correct HTTP status code"); + t.regex(res.headers["content-type"], /less/, "Correct content type"); + t.is(res.text, `@libraryAColor1: #fafad2; :root { --libraryAColor1: @libraryAColor1; } `, "Correct response"); - }); +}); -test("Get css_variables.css from theme middleware (/resources/library/a/themes/base/css_variables.css)", async (t) => { - const res = await request.get("/resources/library/a/themes/base/css_variables.css"); +test("Get css_variables.css (built by buildThemes task) " + + "(/resources/library/a/themes/base/css_variables.css)", async (t) => { + const res = await request.get( + "/resources/library/a/themes/base/css_variables.css"); if (res.error) { throw new Error(res.error); } t.is(res.statusCode, 200, "Correct HTTP status code"); t.regex(res.headers["content-type"], /css/, "Correct content type"); - t.is(res.text, `:root { - --libraryAColor1: #fafad2; -} - + // CSS is minified by default in buildThemes task + t.is(res.text, `:root{--libraryAColor1:#fafad2} /* Inline theming parameters */ #sap-ui-theme-library\\.a{background-image:url('data:text/plain;utf-8,%7B%22libraryAColor1%22%3A%22%23fafad2%22%7D')} `, "Correct response"); }); -test("Get library_skeleton.css from theme middleware (/resources/library/a/themes/base/library_skeleton.css)", - async (t) => { - const res = await request.get("/resources/library/a/themes/base/library_skeleton.css"); - if (res.error) { - throw new Error(res.error); - } - t.is(res.statusCode, 200, "Correct HTTP status code"); - t.regex(res.headers["content-type"], /css/, "Correct content type"); - t.is(res.text, `.library-a-foo { - color: var(--libraryAColor1); - padding: 1px 2px 3px 4px; -} -`, "Correct response"); - }); +test("Get library_skeleton.css (built by buildThemes task) " + + "(/resources/library/a/themes/base/library_skeleton.css)", +async (t) => { + const res = await request.get( + "/resources/library/a/themes/base/library_skeleton.css"); + if (res.error) { + throw new Error(res.error); + } + t.is(res.statusCode, 200, "Correct HTTP status code"); + t.regex(res.headers["content-type"], /css/, "Correct content type"); + // CSS is minified by default in buildThemes task + t.is(res.text, `.library-a-foo{color:var(--libraryAColor1);padding:1px 2px 3px 4px}`, "Correct response"); +}); -test("Get library_skeleton-RTL.css from theme middleware (/resources/library/a/themes/base/library_skeleton-RTL.css)", - async (t) => { - const res = await request.get("/resources/library/a/themes/base/library_skeleton-RTL.css"); - if (res.error) { - throw new Error(res.error); - } - t.is(res.statusCode, 200, "Correct HTTP status code"); - t.regex(res.headers["content-type"], /css/, "Correct content type"); - t.is(res.text, `.library-a-foo { - color: var(--libraryAColor1); - padding: 1px 4px 3px 2px; -} -`, "Correct response"); - }); +test("Get library_skeleton-RTL.css (built by buildThemes task) " + + "(/resources/library/a/themes/base/library_skeleton-RTL.css)", +async (t) => { + const res = await request.get( + "/resources/library/a/themes/base/library_skeleton-RTL.css"); + if (res.error) { + throw new Error(res.error); + } + t.is(res.statusCode, 200, "Correct HTTP status code"); + t.regex(res.headers["content-type"], /css/, "Correct content type"); + // CSS is minified by default in buildThemes task + t.is(res.text, `.library-a-foo{color:var(--libraryAColor1);padding:1px 4px 3px 2px}`, "Correct response"); +}); test("Stop server", async (t) => { const port = 3350; diff --git a/packages/server/test/lib/server/middleware/MiddlewareManager.js b/packages/server/test/lib/server/middleware/MiddlewareManager.js index fb297df3f02..a1fcf036821 100644 --- a/packages/server/test/lib/server/middleware/MiddlewareManager.js +++ b/packages/server/test/lib/server/middleware/MiddlewareManager.js @@ -203,6 +203,125 @@ test("addMiddleware: Add middleware with beforeMiddleware=connectUi5Proxy", asyn "Trying to bind to a non-existing standard middleware"); }); +test("addMiddleware: Add middleware with afterMiddleware referencing removed middleware", async (t) => { + const {sinon} = t.context; + const warnSpy = sinon.spy(); + const StubbedMiddlewareManager = await esmock("../../../../lib/middleware/MiddlewareManager.js", { + "@ui5/logger": {getLogger: sinon.stub().returns({warn: warnSpy})} + }); + const middlewareManager = new StubbedMiddlewareManager({ + graph: {}, + rootProject: "root project", + resources: { + all: "I", + rootProject: "like", + dependencies: "ponies" + } + }); + + await middlewareManager.addStandardMiddleware(); + + await middlewareManager.addMiddleware("customMiddleware", { + customMiddleware: () => {}, + afterMiddleware: "serveThemes", + mountPath: "/pony" + }); + + // Custom middleware should be placed after testRunner (predecessor of serveThemes) + const testRunnerIdx = middlewareManager.middlewareExecutionOrder.indexOf("testRunner"); + const customIdx = middlewareManager.middlewareExecutionOrder.indexOf("customMiddleware"); + t.is(customIdx, testRunnerIdx + 1, + "Custom middleware is placed right after testRunner (predecessor of removed serveThemes)"); + + t.is(warnSpy.callCount, 1, "Warning was logged"); + t.true(warnSpy.getCall(0).args[0].includes("serveThemes"), + "Warning message mentions removed middleware"); + t.true(warnSpy.getCall(0).args[0].includes("testRunner"), + "Warning message mentions fallback target"); + t.true(warnSpy.getCall(0).args[0].includes("migrate-v5"), + "Warning message includes migration guide URL"); +}); + +test("addMiddleware: Add middleware with beforeMiddleware referencing removed middleware", async (t) => { + const {sinon} = t.context; + const warnSpy = sinon.spy(); + const StubbedMiddlewareManager = await esmock("../../../../lib/middleware/MiddlewareManager.js", { + "@ui5/logger": {getLogger: sinon.stub().returns({warn: warnSpy})} + }); + const middlewareManager = new StubbedMiddlewareManager({ + graph: {}, + rootProject: "root project", + resources: { + all: "I", + rootProject: "like", + dependencies: "ponies" + } + }); + + await middlewareManager.addStandardMiddleware(); + + await middlewareManager.addMiddleware("customMiddleware", { + customMiddleware: () => {}, + beforeMiddleware: "serveThemes", + mountPath: "/pony" + }); + + // Custom middleware should be placed before versionInfo (successor of serveThemes) + const versionInfoIdx = middlewareManager.middlewareExecutionOrder.indexOf("versionInfo"); + const customIdx = middlewareManager.middlewareExecutionOrder.indexOf("customMiddleware"); + t.is(customIdx, versionInfoIdx - 1, + "Custom middleware is placed right before versionInfo (successor of removed serveThemes)"); + + t.is(warnSpy.callCount, 1, "Warning was logged"); + t.true(warnSpy.getCall(0).args[0].includes("serveThemes"), + "Warning message mentions removed middleware"); + t.true(warnSpy.getCall(0).args[0].includes("versionInfo"), + "Warning message mentions fallback target"); + t.true(warnSpy.getCall(0).args[0].includes("migrate-v5"), + "Warning message includes migration guide URL"); +}); + +test("addMiddleware: Multiple custom middlewares referencing removed middleware", async (t) => { + const {sinon} = t.context; + const warnSpy = sinon.spy(); + const StubbedMiddlewareManager = await esmock("../../../../lib/middleware/MiddlewareManager.js", { + "@ui5/logger": {getLogger: sinon.stub().returns({warn: warnSpy})} + }); + const middlewareManager = new StubbedMiddlewareManager({ + graph: {}, + rootProject: "root project", + resources: { + all: "I", + rootProject: "like", + dependencies: "ponies" + } + }); + + await middlewareManager.addStandardMiddleware(); + + await middlewareManager.addMiddleware("customMiddleware1", { + customMiddleware: () => {}, + afterMiddleware: "serveThemes" + }); + await middlewareManager.addMiddleware("customMiddleware2", { + customMiddleware: () => {}, + afterMiddleware: "serveThemes" + }); + + // Both should be placed after testRunner (predecessor of serveThemes) + // Since both reference the same position, they end up in reverse insertion order + const testRunnerIdx = middlewareManager.middlewareExecutionOrder.indexOf("testRunner"); + const custom1Idx = middlewareManager.middlewareExecutionOrder.indexOf("customMiddleware1"); + const custom2Idx = middlewareManager.middlewareExecutionOrder.indexOf("customMiddleware2"); + + t.is(custom2Idx, testRunnerIdx + 1, + "Second custom middleware is placed right after testRunner (inserted last)"); + t.is(custom1Idx, testRunnerIdx + 2, + "First custom middleware is placed after the second one (was pushed forward)"); + + t.is(warnSpy.callCount, 2, "Warning was logged for both middlewares"); +}); + test("addMiddleware: Add middleware with afterMiddleware parameter", async (t) => { const middlewareManager = new MiddlewareManager({ graph: {}, @@ -330,7 +449,7 @@ test("addStandardMiddleware: Adds standard middleware in correct order", async ( const addMiddlewareStub = sinon.stub(middlewareManager, "addMiddleware").resolves(); await middlewareManager.addStandardMiddleware(); - t.is(addMiddlewareStub.callCount, 10, "Expected count of middleware got added"); + t.is(addMiddlewareStub.callCount, 9, "Expected count of middleware got added"); const addedMiddlewareNames = []; for (let i = 0; i < addMiddlewareStub.callCount; i++) { addedMiddlewareNames.push(addMiddlewareStub.getCall(i).args[0]); @@ -342,7 +461,6 @@ test("addStandardMiddleware: Adds standard middleware in correct order", async ( "discovery", "serveResources", "testRunner", - "serveThemes", "versionInfo", "nonReadRequests", "serveIndex" diff --git a/packages/server/test/lib/server/middleware/serveThemes.js b/packages/server/test/lib/server/middleware/serveThemes.js deleted file mode 100644 index deb58536f60..00000000000 --- a/packages/server/test/lib/server/middleware/serveThemes.js +++ /dev/null @@ -1,478 +0,0 @@ -import test from "ava"; - -import sinon from "sinon"; -import esmock from "esmock"; - -import {ThemeBuilder} from "@ui5/builder/processors/themeBuilder"; -import MiddlewareUtil from "../../../../lib/middleware/MiddlewareUtil.js"; - -const failOnNext = function(t, reject) { - return function(err) { - if (err) { - t.fail("Unexpected error passed to next function: " + err); - } else { - t.fail("Unexpected call of next function"); - } - reject(); - }; -}; - -const createResources = function() { - return { - // Input - "library.source.less": {}, - - // Default result - "library.css": { - getBuffer: sinon.stub().resolves("/* library.css */"), - getPath: sinon.stub().returns("/resources/sap/ui/test/themes/base/library.css") - }, - "library-RTL.css": { - getBuffer: sinon.stub().resolves("/* library-RTL.css */"), - getPath: sinon.stub().returns("/resources/sap/ui/test/themes/base/library-RTL.css") - }, - "library-parameters.json": { - getBuffer: sinon.stub().resolves("/* library-parameters.json */"), - getPath: sinon.stub().returns("/resources/sap/ui/test/themes/base/library-parameters.json") - }, - - // CSS Variables result - "css_variables.source.less": { - getBuffer: sinon.stub().resolves(`/* css_variables.source.less */`), - getPath: sinon.stub().returns("/resources/sap/ui/test/themes/base/css_variables.source.less") - }, - "css_variables.css": { - getBuffer: sinon.stub().resolves(`/* css_variables.css */`), - getPath: sinon.stub().returns("/resources/sap/ui/test/themes/base/css_variables.css") - }, - "library_skeleton.css": { - getBuffer: sinon.stub().resolves(`/* library_skeleton.css */`), - getPath: sinon.stub().returns("/resources/sap/ui/test/themes/base/library_skeleton.css") - }, - "library_skeleton-RTL.css": { - getBuffer: sinon.stub().resolves(`/* library_skeleton-RTL.css */`), - getPath: sinon.stub().returns("/resources/sap/ui/test/themes/base/library_skeleton-RTL.css") - } - }; -}; - -const stubThemeBuild = function(resources) { - const build = sinon.stub(ThemeBuilder.prototype, "build"); - build.rejects(new Error("File not found!")); - build.withArgs([resources["library.source.less"]], {}).resolves([ - resources["library.css"], - resources["library-RTL.css"], - resources["library-parameters.json"] - ]); - build.withArgs([resources["library.source.less"]], {cssVariables: true}).resolves([ - resources["library.css"], - resources["library-RTL.css"], - resources["library-parameters.json"], - resources["css_variables.source.less"], - resources["css_variables.css"], - resources["library_skeleton.css"], - resources["library_skeleton-RTL.css"] - ]); - return build; -}; - -const verifyThemeRequest = function(t, filename) { - const resources = createResources(); - - stubThemeBuild(resources); - - const {middleware, byPath} = t.context; - byPath.withArgs("/resources/sap/ui/test/themes/base/library.source.less") - .resolves(resources["library.source.less"]); - - const req = { - url: "/resources/sap/ui/test/themes/base/" + filename, - headers: {} - }; - - return new Promise((resolve, reject) => { - const res = { - setHeader: sinon.stub(), - getHeader: sinon.stub(), - end: function(responseText) { - t.is(responseText, `/* ${filename} */`); - if (filename.endsWith(".css")) { - t.deepEqual(res.setHeader.getCall(0).args, ["Content-Type", "text/css; charset=UTF-8"]); - } else if (filename.endsWith(".less")) { - t.deepEqual(res.setHeader.getCall(0).args, ["Content-Type", "text/less; charset=UTF-8"]); - } else if (filename.endsWith(".json")) { - t.deepEqual(res.setHeader.getCall(0).args, ["Content-Type", "application/json; charset=UTF-8"]); - } else { - t.fail("Invalid file extension provided to 'verifyThemeRequest'"); - } - resolve(); - } - }; - - middleware(req, res, failOnNext(t, reject)); - }); -}; - -test.beforeEach(async (t) => { - t.context.etag = sinon.stub(); - t.context.fresh = sinon.stub(); - - t.context.serveThemes = await esmock("../../../../lib/middleware/serveThemes.js", { - "etag": t.context.etag, - "fresh": t.context.fresh - }); - - const resources = { - all: { - byPath: sinon.stub() - } - }; - t.context.byPath = resources.all.byPath; - - t.context.middleware = t.context.serveThemes({ - middlewareUtil: new MiddlewareUtil({graph: "graph", project: "project"}), - resources - }); -}); - -test.afterEach.always((t) => { - sinon.restore(); -}); - -test.serial("Serving library.css", (t) => { - return verifyThemeRequest(t, "library.css"); -}); - -test.serial("Serving library-RTL.css", (t) => { - return verifyThemeRequest(t, "library-RTL.css"); -}); - -test.serial("Serving library-parameters.json", (t) => { - return verifyThemeRequest(t, "library-parameters.json"); -}); - -test.serial("Serving css_variables.source.less", (t) => { - return verifyThemeRequest(t, "css_variables.source.less"); -}); - -test.serial("Serving css_variables.css", (t) => { - return verifyThemeRequest(t, "css_variables.css"); -}); - -test.serial("Serving library_skeleton.css", (t) => { - return verifyThemeRequest(t, "library_skeleton.css"); -}); - -test.serial("Serving library_skeleton-RTL.css", (t) => { - return verifyThemeRequest(t, "library_skeleton-RTL.css"); -}); - -test.serial("Clear cache to rebuild themes when CSS Variables file is requested", (t) => { - const resources = createResources(); - - const build = stubThemeBuild(resources); - const clearCache = sinon.stub(ThemeBuilder.prototype, "clearCache"); - - const {middleware, byPath} = t.context; - byPath.withArgs("/resources/sap/ui/test/themes/base/library.source.less") - .resolves(resources["library.source.less"]); - - return new Promise((resolve, reject) => { - function firstRequest() { - const req = { - url: "/resources/sap/ui/test/themes/base/library.css", - headers: {} - }; - - const res = { - setHeader: sinon.stub(), - getHeader: sinon.stub(), - end: function() { - t.deepEqual(build.getCall(0).args, [[resources["library.source.less"]], {}], - "Build should be called without options"); - - t.false(clearCache.called, "Clear cache should not be called"); - - // Trigger next request - secondRequest(); - } - }; - - middleware(req, res, failOnNext(t)); - } - - function secondRequest() { - const req = { - url: "/resources/sap/ui/test/themes/base/css_variables.css", - headers: {} - }; - - const res = { - setHeader: sinon.stub(), - getHeader: sinon.stub(), - end: function() { - t.deepEqual(build.getCall(1).args, [[resources["library.source.less"]], {cssVariables: true}], - "Build should be called with cssVariables option"); - - t.true(clearCache.called, "Clear cache should be called"); - - resolve(); - } - }; - - middleware(req, res, failOnNext(t, reject)); - } - - firstRequest(); - }); -}); - -test.serial("Clear cache only once after enabling CSS Variables", (t) => { - const resources = createResources(); - - const build = stubThemeBuild(resources); - const clearCache = sinon.stub(ThemeBuilder.prototype, "clearCache"); - - const {middleware, byPath} = t.context; - byPath.withArgs("/resources/sap/ui/test/themes/base/library.source.less") - .resolves(resources["library.source.less"]); - - return new Promise((resolve, reject) => { - function firstRequest() { - const req = { - url: "/resources/sap/ui/test/themes/base/css_variables.css", - headers: {} - }; - - const res = { - setHeader: sinon.stub(), - getHeader: sinon.stub(), - end: function() { - t.deepEqual(build.getCall(0).args, [[resources["library.source.less"]], {cssVariables: true}], - "Build should be called with cssVariables option"); - - t.true(clearCache.calledOnce, "Clear cache should be called once"); - - // Trigger next request - secondRequest(); - } - }; - - middleware(req, res, failOnNext(t, reject)); - } - - function secondRequest() { - const req = { - url: "/resources/sap/ui/test/themes/base/library_skeleton.css", - headers: {} - }; - - const res = { - setHeader: sinon.stub(), - getHeader: sinon.stub(), - end: function() { - t.deepEqual(build.getCall(1).args, [[resources["library.source.less"]], {cssVariables: true}], - "Build should be called with cssVariables option"); - - t.true(clearCache.calledOnce, "Clear cache should still only be called once"); - - resolve(); - } - }; - - middleware(req, res, failOnNext(t, reject)); - } - - firstRequest(); - }); -}); - -test.serial("Do not handle non-theme requests", (t) => { - const {middleware} = t.context; - - const req = { - url: "/resources/sap/ui/test/test.js" - }; - - const res = {}; - - return new Promise((resolve) => { - middleware(req, res, function() { - t.pass("Next middleware is called for non-theme requests"); - resolve(); - }); - }); -}); - -test.serial("Do not handle requests without an existing library.source.less file", (t) => { - const resources = createResources(); - - stubThemeBuild(resources); - - const {middleware, byPath} = t.context; - byPath.withArgs("/resources/sap/ui/test/themes/base/library.source.less").resolves(null); - - const req = { - url: "/resources/sap/ui/test/themes/base/library.css", - headers: {} - }; - - const res = {}; - - return new Promise((resolve) => { - middleware(req, res, function() { - t.pass("Next middleware is called when no library.source.less file is found"); - resolve(); - }); - }); -}); - -test.serial("Only send 304 response in case the client has cached the response already", (t) => { - const {middleware, byPath, etag, fresh} = t.context; - - const ETag = `"fake-etag"`; - - etag.returns(ETag); - - fresh.callsFake(function(reqHeaders, resHeaders) { - t.deepEqual(reqHeaders, { - "If-None-Match": ETag - }); - t.deepEqual(resHeaders, { - "etag": ETag - }); - return true; - }); - - const resources = createResources(); - - stubThemeBuild(resources); - - byPath.withArgs("/resources/sap/ui/test/themes/base/library.source.less") - .resolves(resources["library.source.less"]); - - const req = { - url: "/resources/sap/ui/test/themes/base/library.css", - headers: { - "If-None-Match": ETag - } - }; - - return new Promise((resolve, reject) => { - const res = { - setHeader: sinon.stub(), - getHeader: sinon.stub().withArgs("ETag").returns(ETag), - end: function(responseText) { - t.is(responseText, undefined); - t.is(res.statusCode, 304); - t.deepEqual(res.setHeader.getCall(1).args, ["ETag", ETag]); - resolve(); - } - }; - - middleware(req, res, failOnNext(t, reject)); - }); -}); - -// This could only happen when the theme build processor does not return an expected resource -test.serial("Error handling: Request resource that ThemeBuild doesn't return", (t) => { - const resources = createResources(); - - // Adopt path of library.css so that it can't be found from the theme build results - resources["library.css"].getPath.returns("/foo.js"); - - stubThemeBuild(resources); - - const {middleware, byPath} = t.context; - byPath.withArgs("/resources/sap/ui/test/themes/base/library.source.less") - .resolves(resources["library.source.less"]); - - const req = { - url: "/resources/sap/ui/test/themes/base/library.css", - headers: {} - }; - - const res = {}; - - return new Promise((resolve, reject) => { - middleware(req, res, function(err) { - t.is(err.message, - `Theme Build did not return requested file "/resources/sap/ui/test/themes/base/library.css"`); - resolve(); - }); - }); -}); - -test.serial("Error handling: Unexpected exception within middleware should call next with error", (t) => { - const error = new Error("Unexpected Error"); - - const {middleware, byPath} = t.context; - byPath.rejects(error); - - const req = { - url: "/resources/sap/ui/test/themes/base/library.css", - headers: {} - }; - - const res = {}; - - return new Promise((resolve, reject) => { - middleware(req, res, function(err) { - t.is(err, error); - resolve(); - }); - }); -}); - -test.serial("Multiple parallel requests to the same path should only result in one theme build", async (t) => { - const resources = createResources(); - - const build = stubThemeBuild(resources); - - const {middleware, byPath} = t.context; - byPath.withArgs("/resources/sap/ui/test/themes/base/library.source.less") - .resolves(resources["library.source.less"]); - byPath.withArgs("/resources/sap/ui/test2/themes/base/library.source.less") - .resolves(resources["library.source.less"]); - - function request(url) { - return new Promise((resolve, reject) => { - const req = { - url, - headers: {} - }; - - const res = { - setHeader: sinon.stub(), - getHeader: sinon.stub(), - end: resolve - }; - - middleware(req, res, reject); - }); - } - - await Promise.all([ - request("/resources/sap/ui/test/themes/base/library.css"), - request("/resources/sap/ui/test/themes/base/library.css"), - request("/resources/sap/ui/test/themes/base/library.css"), - - request("/resources/sap/ui/test2/themes/base/library.css"), - request("/resources/sap/ui/test2/themes/base/library.css") - ]); - // Should only build once per url - t.is(build.callCount, 2, "Build should be called 2 times"); - - - // After all requests have finished, the build should be started again when another request comes in - await Promise.all([ - request("/resources/sap/ui/test/themes/base/library.css"), - request("/resources/sap/ui/test/themes/base/library.css"), - request("/resources/sap/ui/test/themes/base/library.css"), - - request("/resources/sap/ui/test2/themes/base/library.css"), - request("/resources/sap/ui/test2/themes/base/library.css") - ]); - // Should only build once per url - t.is(build.callCount, 4, "Build should be called 4 times"); -});