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..55e78334f99 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"; @@ -18,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) { @@ -261,23 +266,13 @@ 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") { + if (!Logger.isLevelEnabled(level)) { return; } - if (!Logger.isLevelEnabled(level)) { + if (level === "info" && BANNER_REDUNDANT_INFO_MODULES.has(moduleName)) { 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"); diff --git a/packages/logger/test/lib/writers/InteractiveConsole.js b/packages/logger/test/lib/writers/InteractiveConsole.js index 6762e170dbc..936a9dcbfa3 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,45 @@ 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(); +}); + +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(); });