Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
4 changes: 2 additions & 2 deletions internal/documentation/docs/pages/Server.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ Please be aware of the following risks when using the server:
When started in an interactive terminal, `ui5 serve` renders a live status banner that shows the server URLs, the root project's name/type/version, the configured UI5 framework, and a single-line status indicator that cycles between three states as you work:

- **● ready** — the server is idle; no source changes are pending and no build is in flight.
- **○ stale files changed, waiting to rebuild** — one or more watched source files changed; the next request will trigger a rebuild.
- **○ stale · files changed, rebuild on next request** — one or more watched source files changed; the next request will trigger a rebuild.
- **◐ building — *N*/*M* projects · *current project* · *current task*** — a build cycle is running. The counter, project name, and task name update in place.

The status line stays pinned at the bottom of the terminal; warnings and errors scroll above it and remain in your terminal scrollback. While the banner is active, `info`-level log messages are suppressed — the status line already shows what they would report. The header repaints in place as more information becomes known (project graph resolved, server bound), so early frames may show dim placeholders for sections that have not been populated yet.

The banner is automatically disabled and `ui5 serve` falls back to plain log output when:

- `stdout` is not a TTY (e.g. output is piped to a file or another process).
- `stderr` is not a TTY (e.g. output is piped to a file or another process).
- The log level is set to `--silent`, `--perf`, `--verbose`, or `--silly` — these levels emit per-task chatter that the status line cannot represent.

In every other case (the default `info` level, `--loglevel warn`, and `--loglevel error`), the banner is active.
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

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

104 changes: 13 additions & 91 deletions packages/cli/lib/cli/commands/serve.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,11 @@
import path from "node:path";
import os from "node:os";
import process from "node:process";
import baseMiddleware from "../middlewares/base.js";
import {applyProjectConfigOptions, applyWorkspaceOptions, dedupeArray} from "../options.js";
import {REMOTE_CONNECTIONS_WARNING_LINES} from "../../serve/remoteConnectionsWarning.js";
import {getLogger} from "@ui5/logger";
import Logger from "@ui5/logger/Logger";
import ConsoleWriter from "@ui5/logger/writers/Console";
import {getVersion as getCliVersion} from "../version.js";
const log = getLogger("cli:commands:serve");

// Log levels that fall back to the plain-output path because the live banner
// cannot represent the level's intent (firehose verbose logging) or because
// the user has asked for silence entirely.
const NON_BANNER_LEVELS = new Set(["perf", "verbose", "silly", "silent"]);

// Collects all non-internal IPv4 addresses from the host's network
// interfaces so the banner can list every reachable URL when the server
// binds to all interfaces. Returns an empty array if no suitable address
// is found.
function findNetworkInterfaceAddresses() {
const interfaces = os.networkInterfaces();
const addresses = [];
for (const name of Object.keys(interfaces)) {
for (const iface of interfaces[name] ?? []) {
if (iface.family === "IPv4" && !iface.internal) {
addresses.push(iface.address);
}
}
}
return addresses;
}

// Serve
const serve = {
command: "serve",
Expand Down Expand Up @@ -160,31 +135,16 @@ serve.builder = function(cli) {
};

serve.handler = async function(argv) {
const useBanner =
process.stdout.isTTY === true &&
!NON_BANNER_LEVELS.has(Logger.getLevel());

let banner;
// Discover network addresses up front so the banner's initial paint can
// reserve the correct number of lines for the "Network:" section. Once the
// header is painted, swapping placeholder lines for real URLs without
// changing the line count keeps the live region from re-flowing.
const networkAddresses = (useBanner && argv.acceptRemoteConnections) ?
findNetworkInterfaceAddresses() : [];
if (useBanner) {
const {default: Banner} = await import("../../serve/Banner.js");
// Banner takes over all output
ConsoleWriter.stop();
banner = Banner.observe({
brand: {name: "UI5 CLI", version: getCliVersion() || ""},
acceptRemoteConnections: !!argv.acceptRemoteConnections,
networkAddressCount: networkAddresses.length,
});
}
// Announce the mode up front so the interactive writer can render its full
// frame (with placeholders for anything not resolved yet) before graph and
// server work begins. The server's `ui5.server-listening` event later
// supplies the authoritative URLs.
process.emit("ui5.tool-mode", {
mode: "serve",
acceptRemoteConnections: !!argv.acceptRemoteConnections,
});

const {graphFromStaticFile, graphFromPackageDependencies} = await import("@ui5/project/graph");
const {serve: serverServe} = await import("@ui5/server");
const {getSslCertificate} = await import("@ui5/server/internal/sslUtil");

let graph;
if (argv.dependencyDefinition) {
Expand All @@ -204,23 +164,6 @@ serve.handler = async function(argv) {
});
}

if (useBanner) {
// Fill in the project + framework section now that the graph has
// resolved — the rest of the header still shows placeholders until
// the server is bound.
const rootProject = graph.getRoot();
const frameworkName = rootProject.getFrameworkName?.();
const frameworkVersion = rootProject.getFrameworkVersion?.();
banner.setProject({
name: rootProject.getName(),
type: rootProject.getType(),
version: rootProject.getVersion(),
framework: frameworkName ?
{name: frameworkName, version: frameworkVersion} :
null,
});
}

let port = argv.port;
let changePortIfInUse = false;

Expand Down Expand Up @@ -267,42 +210,21 @@ serve.handler = async function(argv) {
};

if (serverConfig.h2) {
const {getSslCertificate} = await import("@ui5/server/internal/sslUtil");
const {key, cert} = await getSslCertificate(serverConfig.key, serverConfig.cert);
serverConfig.key = key;
serverConfig.cert = cert;
}

const {promise: pOnError, reject} = Promise.withResolvers();
const {serve: serverServe} = await import("@ui5/server");
const {h2, port: actualPort} = await serverServe(graph, serverConfig, function(err) {
reject(err);
});

const protocol = h2 ? "https" : "http";
let browserUrl = protocol + "://localhost:" + actualPort;

if (useBanner) {
banner.setUrls({
local: browserUrl,
network: networkAddresses.length ?
networkAddresses.map((addr) => protocol + "://" + addr + ":" + actualPort) :
undefined,
});
} else {
if (argv.acceptRemoteConnections) {
process.stderr.write("\n");
for (const line of REMOTE_CONNECTIONS_WARNING_LINES) {
process.stderr.write(line);
process.stderr.write("\n");
}
process.stderr.write("\n");
}
process.stdout.write("Server started");
process.stdout.write("\n");
process.stdout.write("URL: " + browserUrl);
process.stdout.write("\n");
}

if (argv.open !== undefined) {
const protocol = h2 ? "https" : "http";
let browserUrl = protocol + "://localhost:" + actualPort;
if (typeof argv.open === "string") {
let relPath = argv.open || "/";
if (!relPath.startsWith("/")) {
Expand Down
33 changes: 29 additions & 4 deletions packages/cli/lib/cli/middlewares/logger.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import {setLogLevel, isLogLevelEnabled, getLogger} from "@ui5/logger";
import process from "node:process";
import {setLogLevel, isLogLevelEnabled, getLogger, getLogLevel} from "@ui5/logger";
import ConsoleWriter from "@ui5/logger/writers/Console";
import {getVersionWithLocation} from "../version.js";
import {getVersion, getVersionWithLocation} from "../version.js";

// Log levels that the interactive writer cannot represent (firehose verbose
// logging, or user-requested silence). Fall back to the plain Console writer
// in those cases.
const NON_INTERACTIVE_LEVELS = new Set(["perf", "verbose", "silly", "silent"]);

/**
* Logger middleware to enable logging capabilities
*
Expand All @@ -23,8 +30,26 @@ export async function initLogger(argv) {
setLogLevel(argv.loglevel);
}

// Initialize writer
ConsoleWriter.init();
const commandName = Array.isArray(argv._) ? argv._[0] : null;
const useInteractive =
commandName === "serve" &&
process.stderr.isTTY === true &&
!NON_INTERACTIVE_LEVELS.has(getLogLevel()) &&
!process.env.UI5_CLI_NO_INTERACTIVE;

if (useInteractive) {
const {default: InteractiveConsole} = await import("@ui5/logger/writers/InteractiveConsole");
InteractiveConsole.init();
// Populate the interactive writer's header region before any command
// work starts, so the tool identity is visible from the first frame.
process.emit("ui5.tool-info", {
name: "UI5 CLI",
version: getVersion() || "",
});
} else {
ConsoleWriter.init();
}

if (isLogLevelEnabled("verbose")) {
const log = getLogger("cli:middlewares:base");
log.verbose(`using @ui5/cli version ${getVersionWithLocation()}`);
Expand Down
Loading
Loading