Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a9b0e55
refactor(logger): Use pretty-hrtime in Serve buildDone fallback
matz3 Jul 2, 2026
af47ad2
docs(cli): Refine serve state.js header comment
matz3 Jul 2, 2026
b92b668
refactor(logger): Move ui5 serve banner into InteractiveConsole writer
matz3 Jul 3, 2026
8480e6d
feat(logger): Render InteractiveConsole placeholders up front via ui5…
matz3 Jul 3, 2026
fb97d34
fix(logger): Prevent InteractiveConsole from eating scrollback on fra…
matz3 Jul 3, 2026
0744aa2
refactor(logger): Drop networkAddressCount hint from ui5.tool-mode
matz3 Jul 3, 2026
3451923
fix(logger): Restore non-interactive ui5 serve output shape
matz3 Jul 3, 2026
f4a25a9
build: Update package.json and lockfile
matz3 Jul 3, 2026
6acb7ba
refactor(cli): Only emit ui5.tool-info when InteractiveConsole is used
matz3 Jul 3, 2026
9bbeea3
docs(logger): Reword Console.js comments to not reference history
matz3 Jul 3, 2026
63b48a4
docs(documentation): Update ui5 serve banner status text and TTY detail
matz3 Jul 3, 2026
c2bbc5c
refactor(logger): Simplify InteractiveConsole render path
matz3 Jul 3, 2026
fbe4023
refactor(logger): Rename enablePlaceholders per state region
matz3 Jul 3, 2026
fe85d40
test(logger): Restore banner-move test coverage to 100%
matz3 Jul 3, 2026
8cdb0fa
test(cli): Cover interactive writer branch in logger middleware
matz3 Jul 3, 2026
ac1e4ee
test(logger): Tolerate figures Windows fallback in starting-state ass…
matz3 Jul 3, 2026
0e6cf1c
fix(logger): Omit Framework row when project declares no framework
matz3 Jul 6, 2026
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