From 1a003dafcb9bcb25b86fa3fad2c27a335d313175 Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Wed, 22 Jul 2026 12:35:28 +0200 Subject: [PATCH 1/3] refactor(logger): Extract shared message formatting --- packages/logger/lib/writers/Console.js | 7 ++-- .../logger/lib/writers/InteractiveConsole.js | 9 +--- .../logger/lib/writers/internal/format.js | 41 +++++++++++++++++++ .../lib/writers/internal/levelPrefix.js | 25 ----------- packages/logger/test/lib/writers/Console.js | 2 +- 5 files changed, 47 insertions(+), 37 deletions(-) create mode 100644 packages/logger/lib/writers/internal/format.js delete mode 100644 packages/logger/lib/writers/internal/levelPrefix.js diff --git a/packages/logger/lib/writers/Console.js b/packages/logger/lib/writers/Console.js index b9587aa864e..8f308704539 100644 --- a/packages/logger/lib/writers/Console.js +++ b/packages/logger/lib/writers/Console.js @@ -3,7 +3,7 @@ import {chalkStderr as chalk} from "chalk"; import figures from "figures"; import {MultiBar} from "cli-progress"; import Logger from "../loggers/Logger.js"; -import {getLevelPrefix} from "./internal/levelPrefix.js"; +import {formatLogLine, prefixModuleName} from "./internal/format.js"; import {REMOTE_CONNECTIONS_WARNING_LINES} from "./interactiveConsole/remoteConnectionsWarning.js"; /** @@ -199,8 +199,7 @@ class Console { if (!Logger.isLevelEnabled(level)) { return; } - const levelPrefix = getLevelPrefix(level); - const msg = `${levelPrefix} ${message}\n`; + const msg = formatLogLine(level, message) + "\n"; if (this.#progressBarContainer) { // If a progress bar is in use, we have to log through it's API @@ -213,7 +212,7 @@ class Console { #handleLogEvent({level, message, moduleName}) { if (this.#filterModule(moduleName)) { - this.#writeMessage(level, `${chalk.blue(moduleName)} ${message}`); + this.#writeMessage(level, prefixModuleName(message, moduleName)); } } diff --git a/packages/logger/lib/writers/InteractiveConsole.js b/packages/logger/lib/writers/InteractiveConsole.js index f9b7f60a672..595fac54198 100644 --- a/packages/logger/lib/writers/InteractiveConsole.js +++ b/packages/logger/lib/writers/InteractiveConsole.js @@ -1,9 +1,8 @@ import process from "node:process"; import {createLogUpdate} from "log-update"; import sliceAnsi from "slice-ansi"; -import chalk from "chalk"; import Logger from "../loggers/Logger.js"; -import {getLevelPrefix} from "./internal/levelPrefix.js"; +import {formatLogLine, prefixModuleName} from "./internal/format.js"; import {createHeaderState, setTool} from "./interactiveConsole/state/header.js"; import {createProjectState, setProject, enableProjectPlaceholders} from "./interactiveConsole/state/project.js"; import {createServerState, setListening, enableServerPlaceholders} from "./interactiveConsole/state/server.js"; @@ -273,11 +272,7 @@ class InteractiveConsole { if (!Logger.isLevelEnabled(level)) { return; } - const levelPrefix = getLevelPrefix(level); - const formatted = moduleName ? - `${levelPrefix} ${chalk.blue(moduleName)} ${message}` : - `${levelPrefix} ${message}`; - this.logAbove(formatted); + this.logAbove(formatLogLine(level, prefixModuleName(message, moduleName))); } #handleToolInfo(evt) { diff --git a/packages/logger/lib/writers/internal/format.js b/packages/logger/lib/writers/internal/format.js new file mode 100644 index 00000000000..a6d7d1ad5b8 --- /dev/null +++ b/packages/logger/lib/writers/internal/format.js @@ -0,0 +1,41 @@ +import {chalkStderr as chalk} from "chalk"; + +// Shared message-formatting helpers used by every console-writing writer so +// scrolled log lines look identical regardless of which writer produced them. +// Kept as an internal helper module rather than a public export — the exact +// styling is an implementation detail of `writers/Console` and its siblings. + +function getLevelPrefix(level) { + switch (level) { + case "silly": + return chalk.inverse(level); + case "verbose": + return chalk.cyan("verb"); + case "perf": + return chalk.bgYellow.red(level); + case "info": + return chalk.green(level); + case "warn": + return chalk.yellow(level); + case "error": + return chalk.bgRed.white(level); + default: + // Log level silent does not produce messages + throw new Error(`writers/internal/format: Invalid message log level "${level}"`); + } +} + +// Prepend the blue module name to a message when one is present. Both console +// writers render log messages as " ", omitting the module +// name when it is absent. +export function prefixModuleName(message, moduleName) { + return moduleName ? `${chalk.blue(moduleName)} ${message}` : message; +} + +// Render a full log line as " ". The trailing newline is +// left to the caller: `writers/Console` writes straight to the stream and needs +// it, while `writers/InteractiveConsole` hands the line to log-update, which +// adds its own. +export function formatLogLine(level, message) { + return `${getLevelPrefix(level)} ${message}`; +} diff --git a/packages/logger/lib/writers/internal/levelPrefix.js b/packages/logger/lib/writers/internal/levelPrefix.js deleted file mode 100644 index 6eccf95a36b..00000000000 --- a/packages/logger/lib/writers/internal/levelPrefix.js +++ /dev/null @@ -1,25 +0,0 @@ -import {chalkStderr as chalk} from "chalk"; - -// Shared level-prefix renderer used by every console-writing writer so scrolled -// log lines look identical regardless of which writer produced them. Kept as -// an internal helper module rather than a public export — the exact styling is -// an implementation detail of `writers/Console` and its siblings. -export function getLevelPrefix(level) { - switch (level) { - case "silly": - return chalk.inverse(level); - case "verbose": - return chalk.cyan("verb"); - case "perf": - return chalk.bgYellow.red(level); - case "info": - return chalk.green(level); - case "warn": - return chalk.yellow(level); - case "error": - return chalk.bgRed.white(level); - default: - // Log level silent does not produce messages - throw new Error(`writers/internal/levelPrefix: Invalid message log level "${level}"`); - } -} diff --git a/packages/logger/test/lib/writers/Console.js b/packages/logger/test/lib/writers/Console.js index 18715214744..e0bf2a9473f 100644 --- a/packages/logger/test/lib/writers/Console.js +++ b/packages/logger/test/lib/writers/Console.js @@ -42,7 +42,7 @@ test.serial("Log event with invalid log level 'silent'", (t) => { moduleName: "my:module" }); }, { - message: `writers/internal/levelPrefix: Invalid message log level "silent"` + message: `writers/internal/format: Invalid message log level "silent"` }); t.is(stderrWriteStub.callCount, 0, "Logged no message"); From c822c3bcc7dd48e23f4821fa01fe408e23226f9c Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Wed, 22 Jul 2026 12:37:37 +0200 Subject: [PATCH 2/3] refactor(logger): Show all logs in interactive console This addresses the planned follow-up of the initial implementation: https://github.com/UI5/cli/pull/1439#issuecomment-4863576624 The output will be more verbose again, but some logs should be filtered out in case the interactive console contains the information already. --- packages/logger/lib/writers/InteractiveConsole.js | 9 --------- packages/logger/test/lib/writers/InteractiveConsole.js | 6 ++---- 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/packages/logger/lib/writers/InteractiveConsole.js b/packages/logger/lib/writers/InteractiveConsole.js index 595fac54198..5f6d7743f36 100644 --- a/packages/logger/lib/writers/InteractiveConsole.js +++ b/packages/logger/lib/writers/InteractiveConsole.js @@ -260,15 +260,6 @@ class InteractiveConsole { } #handleLog({level, message, moduleName}) { - // The banner curates `info` away (the status line already represents - // the build's state). Verbose / perf / silly users hit the fallback - // path before the writer ever activates. Warnings and errors persist — - // but only if the configured log level lets them through. The standard - // ConsoleWriter does this filtering implicitly via its `#writeMessage`; - // this writer acts as its own sink here, so it has to check too. - if (level !== "warn" && level !== "error") { - return; - } if (!Logger.isLevelEnabled(level)) { return; } diff --git a/packages/logger/test/lib/writers/InteractiveConsole.js b/packages/logger/test/lib/writers/InteractiveConsole.js index 6762e170dbc..2e23b73a390 100644 --- a/packages/logger/test/lib/writers/InteractiveConsole.js +++ b/packages/logger/test/lib/writers/InteractiveConsole.js @@ -199,7 +199,7 @@ test.serial("warn / error logs scroll above the live region", (t) => { writer.disable(); }); -test.serial("info logs are filtered — status line represents build state", (t) => { +test.serial("info logs logs scroll above the live region", (t) => { const {writer, stderr} = createWriter(); process.emit("ui5.log", { @@ -208,10 +208,8 @@ test.serial("info logs are filtered — status line represents build state", (t) moduleName: "my:module", }); - // The persistent frame renders on info events too because #render is - // called; but no line containing the info text should scroll above. const output = stripAnsi(stderr.writes.join("")); - t.notRegex(output, /quiet info/); + t.regex(output, /quiet info/); writer.disable(); }); From 3d58b55da5b70b09eebb7dbb37efce093e2b6d54 Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Wed, 22 Jul 2026 13:39:39 +0200 Subject: [PATCH 3/3] refactor(logger): Suppress redundant info logs in interactive console Info-level logs from ProjectBuilder duplicate build state that the interactive console already renders in its banner via the named ui5.build-metadata / ui5.build-status events. Suppress those info lines in the InteractiveConsole writer so they no longer scroll above the live region, while warnings, errors and all other modules still pass through. The filtering lives entirely in the consumer, keyed by moduleName; the emitting code remains unaware of how its logs are displayed. --- .../logger/lib/writers/InteractiveConsole.js | 9 +++++ .../test/lib/writers/InteractiveConsole.js | 37 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/packages/logger/lib/writers/InteractiveConsole.js b/packages/logger/lib/writers/InteractiveConsole.js index 5f6d7743f36..55e78334f99 100644 --- a/packages/logger/lib/writers/InteractiveConsole.js +++ b/packages/logger/lib/writers/InteractiveConsole.js @@ -17,6 +17,12 @@ import { // Spinner tick interval while in `building` or `validating` state. const BUILDING_TICK_MS = 120; +// Module names whose info-level logs duplicate state the banner already renders +// via named build events (ui5.build-metadata / ui5.build-status). Their info +// lines are suppressed so they don't scroll above the live region. Warnings and +// errors from these modules, and all other levels, still pass through. +const BANNER_REDUNDANT_INFO_MODULES = new Set(["ProjectBuilder"]); + // Decode the tail of `stream.write(chunk[, encoding][, callback])`. `encoding` // falls back to "utf8" (Node's default) when not supplied. function parseWriteArgs(encodingOrCallback, maybeCallback) { @@ -263,6 +269,9 @@ class InteractiveConsole { if (!Logger.isLevelEnabled(level)) { return; } + if (level === "info" && BANNER_REDUNDANT_INFO_MODULES.has(moduleName)) { + return; + } this.logAbove(formatLogLine(level, prefixModuleName(message, moduleName))); } diff --git a/packages/logger/test/lib/writers/InteractiveConsole.js b/packages/logger/test/lib/writers/InteractiveConsole.js index 2e23b73a390..936a9dcbfa3 100644 --- a/packages/logger/test/lib/writers/InteractiveConsole.js +++ b/packages/logger/test/lib/writers/InteractiveConsole.js @@ -214,6 +214,43 @@ test.serial("info logs logs scroll above the live region", (t) => { writer.disable(); }); +test.serial("info logs from ProjectBuilder are suppressed (banner-redundant)", (t) => { + const {writer, stderr} = createWriter(); + + process.emit("ui5.log", { + level: "info", + message: "Preparing build for projects", + moduleName: "ProjectBuilder", + }); + + const output = stripAnsi(stderr.writes.join("")); + t.notRegex(output, /Preparing build for projects/, + "ProjectBuilder info line does not scroll above the live region"); + + writer.disable(); +}); + +test.serial("non-info logs from ProjectBuilder still scroll above the live region", (t) => { + const {writer, stderr} = createWriter(); + + process.emit("ui5.log", { + level: "warn", + message: "Build warning", + moduleName: "ProjectBuilder", + }); + process.emit("ui5.log", { + level: "error", + message: "Build failed", + moduleName: "ProjectBuilder", + }); + + const output = stripAnsi(stderr.writes.join("")); + t.regex(output, /Build warning/); + t.regex(output, /Build failed/); + + writer.disable(); +}); + test.serial("build-metadata a second time resets and continues (rebuild)", (t) => { const {writer} = createWriter();