Skip to content
Merged
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
36 changes: 36 additions & 0 deletions internal/documentation/docs/pages/Server.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,42 @@ Once started with `ui5 serve`, the server automatically monitors changes to the
Changes to configuration files or custom tasks require a server restart to take effect.
:::

## Integrating into an Existing Node.js Server

Beyond starting its own HTTP server via [`serve`](../api/module-@ui5_server.html#.serve), `@ui5/server` exposes a [`serveMiddleware`](../api/module-@ui5_server.html#.serveMiddleware) API for integrating UI5 Server functionality into an **existing** Express or Connect application. This is the supported entry point for tools that embed a UI5 Server — for example test runners or framework plugins — instead of running `ui5 serve` as a standalone process.

[`serveMiddleware`](../api/module-@ui5_server.html#.serveMiddleware)`(graph[, options][, error])` takes a [project graph](./Project.md) and resolves with:

- `middleware` — a single Connect/Express-compatible request handler; mount it on your own app with `app.use(middleware)`.
- `close()` — an async teardown function that releases the `BuildServer`'s source watcher and build-cache handle. Call it when shutting down.

Unlike `serve`, `serveMiddleware` does **not** bind a port, attach the live reload WebSocket server, or install the terminal HTML error handler — those remain the responsibility of the HTTP server you own. Error handling and the listener are yours to provide.

```js
import express from "express";
import {graphFromPackageDependencies} from "@ui5/project/graph";
import {serveMiddleware} from "@ui5/server";

const graph = await graphFromPackageDependencies({cwd: process.cwd()});
const {middleware, close} = await serveMiddleware(graph);

const app = express();
app.use(middleware);
const listener = app.listen(8080);

// On teardown, stop the listener and release the BuildServer's watcher and cache handle:
listener.close();
await close();
```

Connect works the same way — `connect().use(middleware)`.

`options` accepts the serving-related subset of [`serve`](../api/module-@ui5_server.html#.serve)'s options. See the [API Reference](../api/module-@ui5_server.html#.serveMiddleware) for the full signature.

::: warning Serve a graph only once
A project graph can be served only once. Do not call both `serveMiddleware` and `serve` for the same graph.
:::

## SSL Certificates
When starting the UI5 Server in HTTPS- or HTTP/2 mode, for example by using UI5 CLI parameter `--h2`, you will be prompted for the automatic generation of a local SSL certificate if necessary.

Expand Down
86 changes: 86 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions packages/project/lib/graph/ProjectGraph.js
Original file line number Diff line number Diff line change
Expand Up @@ -759,6 +759,27 @@ class ProjectGraph {
});
}

/**
* Creates a {@link @ui5/project/build/BuildServer} for this graph and returns it.
* Called by <code>@ui5/server</code> to back the request-serving middleware stack.
*
* Kept private because the API surface is not yet stable enough for general use,
* but must remain compatible with the server package's internal usage.
*
* @private
* @param {object} parameters Parameters
* @param {boolean} [parameters.initialBuildRootProject=false]
* @param {string[]} [parameters.initialBuildIncludedDependencies=[]]
* @param {string[]} [parameters.initialBuildExcludedDependencies=[]]
* @param {boolean} [parameters.selfContained=false]
* @param {boolean} [parameters.jsdoc=false]
* @param {boolean} [parameters.createBuildManifest=false]
* @param {string[]} [parameters.includedTasks=[]]
* @param {string[]} [parameters.excludedTasks=[]]
* @param {module:@ui5/project/build/cache/Cache} [parameters.cache=Default]
* @param {string} [parameters.ui5DataDir]
* @returns {Promise<@ui5/project/build/BuildServer>}
*/
async serve({
initialBuildRootProject = false,
initialBuildIncludedDependencies = [], initialBuildExcludedDependencies = [],
Expand Down
5 changes: 2 additions & 3 deletions packages/server/lib/serveMiddleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ import {buildRouter} from "./serve/stack.js";
* outside of request handling.
* @returns {Promise<object>} Promise resolving with an object containing the
* <code>middleware</code> (a connect/Express-compatible handler to be
* mounted via <code>app.use()</code>), the <code>buildServer</code>, and a
* <code>close</code> function releasing the BuildServer's watcher and cache.
* mounted via <code>app.use()</code>) and a <code>close</code> function
* releasing the BuildServer's watcher and cache.
*/
export default async function serveMiddleware(graph, {
sendSAPTargetCSP = false, simpleIndex = false, serveCSPReports = false, cache,
Expand All @@ -74,7 +74,6 @@ export default async function serveMiddleware(graph, {
let destroyed = false;
return {
middleware: router,
buildServer,
close: async function close() {
if (destroyed) {
return;
Expand Down
1 change: 1 addition & 0 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@
"@istanbuljs/esm-loader-hook": "^0.3.0",
"@ui5/project": "^5.0.0-alpha.7",
"ava": "^8.0.1",
"connect": "^3.7.0",
"cross-env": "^10.1.0",
"eslint": "^10.7.0",
"esmock": "^2.7.6",
Expand Down
65 changes: 39 additions & 26 deletions packages/server/test/lib/server/serveMiddleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,29 @@ import test from "ava";
import sinon from "sinon";
import esmock from "esmock";
import express from "express";
import connect from "connect";
import supertest from "supertest";
import {graphFromPackageDependencies} from "@ui5/project/graph";
import serveMiddleware from "../../../lib/serveMiddleware.js";
import {INJECT_SCRIPT_TAG} from "../../../lib/liveReload/constants.js";
import {isolatedUi5DataDir} from "../../utils/buildCacheIsolation.js";

// Integration: mount the returned middleware on a caller-owned express app and serve real
// requests through supertest, without binding a port or starting a UI5-owned HTTP server.
// Integration: mount the returned middleware on caller-owned HTTP frameworks and serve real
// requests through supertest, without binding a port or starting a UI5-owned HTTP server. The
// same middleware is mounted on both an Express app and a Connect app to prove it is a plain
// connect/Express-compatible handler and to satisfy the "multiple express and connect versions"
// acceptance criterion. A project graph can be served only once, so a single serveMiddleware
// result is shared across both hosts (mounting one handler on two apps is fine — it holds no
// per-app state).

let app;
let close;
let request;
// Host frameworks the embedding middleware is expected to work with. supertest accepts either
// app instance (both are request-listener functions), so the request assertions are identical.
const hosts = [
{name: "express", mount: (mw) => express().use(mw)},
{name: "connect", mount: (mw) => connect().use(mw)},
];
const requests = {};

test.before(async (t) => {
const graph = await graphFromPackageDependencies({
Expand All @@ -25,41 +36,43 @@ test.before(async (t) => {
});
close = result.close;

app = express();
app.use(result.middleware);
request = supertest(app);
for (const host of hosts) {
requests[host.name] = supertest(host.mount(result.middleware));
}
});

test.after.always(async () => {
await close();
await close?.();
});

async function get(path) {
async function get(request, path) {
const res = await request.get(path);
if (res.error) {
throw new Error(res.error);
throw res.error;
}
return res;
}

test("Serves index.html through a caller-owned express app", async (t) => {
const res = await get("/index.html");
t.is(res.statusCode, 200, "Correct HTTP status code");
t.regex(res.headers["content-type"], /html/, "Correct content type");
t.regex(res.text, /<title>Application A<\/title>/, "Correct response");
});
for (const host of hosts) {
test(`Serves index.html through a caller-owned ${host.name} app`, async (t) => {
const res = await get(requests[host.name], "/index.html");
t.is(res.statusCode, 200, "Correct HTTP status code");
t.regex(res.headers["content-type"], /html/, "Correct content type");
t.regex(res.text, /<title>Application A<\/title>/, "Correct response");
});

test("Serves the UI5 version info", async (t) => {
const res = await get("/resources/sap-ui-version.json");
t.is(res.statusCode, 200, "Correct HTTP status code");
t.regex(res.headers["content-type"], /json/, "Correct content type");
});
test(`Serves the UI5 version info through a caller-owned ${host.name} app`, async (t) => {
const res = await get(requests[host.name], "/resources/sap-ui-version.json");
t.is(res.statusCode, 200, "Correct HTTP status code");
t.regex(res.headers["content-type"], /json/, "Correct content type");
});

test("Does not inject the live-reload client script", async (t) => {
const res = await get("/index.html");
t.false(res.text.includes(INJECT_SCRIPT_TAG),
"The live-reload client script is not injected in the embedding API");
});
test(`Does not inject the live-reload client script (${host.name})`, async (t) => {
const res = await get(requests[host.name], "/index.html");
t.false(res.text.includes(INJECT_SCRIPT_TAG),
"The live-reload client script is not injected in the embedding API");
});
}

// Unit: the module contract independent of a real graph. The returned middleware is the router
// from the shared core, close() releases the BuildServer once and is idempotent, and the
Expand Down
Loading