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
32 changes: 24 additions & 8 deletions packages/logger/lib/writers/InteractiveConsole.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import sliceAnsi from "slice-ansi";
import Logger from "../loggers/Logger.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 {
createProjectState,
setProject,
setFramework,
enableProjectPlaceholders,
} from "./interactiveConsole/state/project.js";
import {createServerState, setListening, enableServerPlaceholders} from "./interactiveConsole/state/server.js";
import {
createBuildState, beginBuild, advanceToProject, setTask, transitionTo, setError, STATES,
Expand Down Expand Up @@ -45,9 +50,11 @@ function parseWriteArgs(encodingOrCallback, maybeCallback) {
* No command knowledge. Regions render top-to-bottom in a fixed order —
* header, root project, server, build status — and hidden regions collapse.
* <br><br>
* All state is event-driven. See the design at
* <code>docs/interactive-console-writer.md</code> and the public event API
* exposed by <code>@ui5/logger</code>.
* All state is event-driven. It reacts to process events emitted across
* <code>@ui5/logger</code> and related packages, including CLI, project,
* server, build, serve, and log events. These event shapes are internal
* cross-package contracts within the UI5 CLI codebase and should be kept
* stable for their in-repo consumers.
*
* @public
* @class
Expand Down Expand Up @@ -93,6 +100,7 @@ class InteractiveConsole {
#onToolInfo;
#onToolMode;
#onProjectResolved;
#onProjectFrameworkResolved;
#onServerListening;
#onStopConsole;
#onResize;
Expand Down Expand Up @@ -230,6 +238,7 @@ class InteractiveConsole {
this.#onToolInfo = (evt) => this.#handleToolInfo(evt);
this.#onToolMode = (evt) => this.#handleToolMode(evt);
this.#onProjectResolved = (evt) => this.#handleProjectResolved(evt);
this.#onProjectFrameworkResolved = (evt) => this.#handleProjectFrameworkResolved(evt);
this.#onServerListening = (evt) => this.#handleServerListening(evt);
this.#onStopConsole = () => this.disable();
this.#onResize = () => this.#handleResize();
Expand All @@ -242,6 +251,7 @@ class InteractiveConsole {
process.on("ui5.tool-info", this.#onToolInfo);
process.on("ui5.tool-mode", this.#onToolMode);
process.on("ui5.project-resolved", this.#onProjectResolved);
process.on("ui5.project-framework-resolved", this.#onProjectFrameworkResolved);
process.on("ui5.server-listening", this.#onServerListening);
process.on("ui5.log.stop-console", this.#onStopConsole);
if (typeof this.#stderr.on === "function") {
Expand All @@ -258,6 +268,7 @@ class InteractiveConsole {
process.off("ui5.tool-info", this.#onToolInfo);
process.off("ui5.tool-mode", this.#onToolMode);
process.off("ui5.project-resolved", this.#onProjectResolved);
process.off("ui5.project-framework-resolved", this.#onProjectFrameworkResolved);
process.off("ui5.server-listening", this.#onServerListening);
process.off("ui5.log.stop-console", this.#onStopConsole);
if (typeof this.#stderr.off === "function") {
Expand Down Expand Up @@ -299,10 +310,10 @@ class InteractiveConsole {

#handleProjectResolved(evt) {
if (this.#seenProjectResolved) {
// See docs/interactive-console-writer.md § Ordering rules: the
// writer's model is single-root-project. Two events means the
// caller's invariant is violated and any subsequent event
// attribution is ambiguous.
// The writer's model is single-root-project. A second
// `ui5.project-resolved` event means the emitter violated that
// invariant, making subsequent event attribution ambiguous, so fail
// fast instead of trying to deduplicate.
throw new Error(
`writers/InteractiveConsole: Received duplicate ui5.project-resolved event`);
}
Expand All @@ -311,6 +322,11 @@ class InteractiveConsole {
this.#render();
}

#handleProjectFrameworkResolved({framework}) {
setFramework(this.#projectState, framework);
this.#render();
}

#handleServerListening(evt) {
setListening(this.#serverState, evt);
this.#render();
Expand Down
2 changes: 1 addition & 1 deletion packages/logger/lib/writers/interactiveConsole/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function renderProjectRegion(projectState) {
}
} else {
// Placeholder mode: reserve only the Project row. The Framework row is
// deferred until project-resolved reveals a real framework name.
// deferred until framework resolution reveals a real framework name.
lines.push(`${chalk.dim("Project")} ${placeholder("resolving…")}`);
}
return lines;
Expand Down
12 changes: 10 additions & 2 deletions packages/logger/lib/writers/interactiveConsole/state/project.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// Region 2 — root project. Populated by `ui5.project-resolved`.
// Region 2 — root project. Populated by `ui5.project-resolved` and, when
// framework usage is actually resolved for the current run, by
// `ui5.project-framework-resolved`.
export function createProjectState() {
return {
project: null, // {name, type, version}
Expand All @@ -12,7 +14,13 @@ export function createProjectState() {

export function setProject(state, evt) {
state.project = {name: evt.name, type: evt.type, version: evt.version};
state.framework = evt.framework ? {name: evt.framework.name, version: evt.framework.version} : null;
}

export function setFramework(state, framework) {
state.framework = framework ? {
name: framework.name,
version: framework.version,
} : null;
}

export function enableProjectPlaceholders(state) {
Expand Down
46 changes: 38 additions & 8 deletions packages/logger/test/lib/writers/InteractiveConsole.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,30 @@ test.serial("project-resolved populates the project region", (t) => {
name: "my.app",
type: "application",
version: "1.0.0",
framework: {name: "SAPUI5", version: "1.150.0"},
});

const state = writer._getStateForTest();
t.deepEqual(state.project.project, {name: "my.app", type: "application", version: "1.0.0"});
t.deepEqual(state.project.framework, {name: "SAPUI5", version: "1.150.0"});
t.is(state.project.framework, null);

writer.disable();
});

test.serial("project-framework-resolved populates the framework region", (t) => {
const {writer} = createWriter();

process.emit("ui5.project-framework-resolved", {
framework: {
name: "SAPUI5",
version: "1.150.0",
},
});

const state = writer._getStateForTest();
t.deepEqual(state.project.framework, {
name: "SAPUI5",
version: "1.150.0",
});

writer.disable();
});
Expand All @@ -68,7 +86,6 @@ test.serial("duplicate project-resolved throws", (t) => {
name: "my.app",
type: "application",
version: "1.0.0",
framework: null,
});

// The writer's model is single-root-project. A second event means the
Expand All @@ -78,7 +95,6 @@ test.serial("duplicate project-resolved throws", (t) => {
name: "other.app",
type: "application",
version: "2.0.0",
framework: null,
});
}, {
message: /duplicate ui5\.project-resolved/,
Expand Down Expand Up @@ -174,7 +190,7 @@ test.serial("regions are order-tolerant — server before project", (t) => {
acceptRemoteConnections: false,
});
process.emit("ui5.project-resolved", {
name: "my.app", type: "application", version: "1.0.0", framework: null,
name: "my.app", type: "application", version: "1.0.0",
});

const state = writer._getStateForTest();
Expand Down Expand Up @@ -288,7 +304,12 @@ test.serial("frame includes visible content for each populated region", (t) => {
process.emit("ui5.tool-info", {name: "UI5 CLI", version: "1.2.3"});
process.emit("ui5.project-resolved", {
name: "my.app", type: "application", version: "1.0.0",
framework: {name: "SAPUI5", version: "1.150.0"},
});
process.emit("ui5.project-framework-resolved", {
framework: {
name: "SAPUI5",
version: "1.150.0",
},
});
process.emit("ui5.server-listening", {
urls: [{label: "Local", url: "http://localhost:8080"}],
Expand Down Expand Up @@ -361,7 +382,12 @@ test.serial("tool-mode 'serve' placeholders are replaced by real data", (t) => {

process.emit("ui5.project-resolved", {
name: "my.app", type: "application", version: "1.0.0",
framework: {name: "SAPUI5", version: "1.150.0"},
});
process.emit("ui5.project-framework-resolved", {
framework: {
name: "SAPUI5",
version: "1.150.0",
},
});
process.emit("ui5.server-listening", {
urls: [{label: "Local", url: "http://localhost:8080"}],
Expand All @@ -372,6 +398,10 @@ test.serial("tool-mode 'serve' placeholders are replaced by real data", (t) => {
// State reflects real data now — the placeholder rendering path is gone.
const state = writer._getStateForTest();
t.deepEqual(state.project.project, {name: "my.app", type: "application", version: "1.0.0"});
t.deepEqual(state.project.framework, {
name: "SAPUI5",
version: "1.150.0",
});
t.truthy(state.server.urls);
t.is(state.build.state, STATES.READY);

Expand Down Expand Up @@ -410,7 +440,7 @@ test.serial("region blocks are separated by a blank line in the composed frame",
setTool(header, {name: "UI5 CLI", version: "1.2.3"});
const project = createProjectState();
setProject(project, {
name: "my.app", type: "application", version: "1.0.0", framework: null,
name: "my.app", type: "application", version: "1.0.0",
});
const server = createServerState();
setListening(server, {
Expand Down
7 changes: 3 additions & 4 deletions packages/logger/test/lib/writers/interactiveConsole/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
} from "../../../../lib/writers/interactiveConsole/state/build.js";
import {createHeaderState, setTool} from
"../../../../lib/writers/interactiveConsole/state/header.js";
import {createProjectState, setProject, enableProjectPlaceholders} from
import {createProjectState, setProject, setFramework, enableProjectPlaceholders} from
"../../../../lib/writers/interactiveConsole/state/project.js";
import {createServerState, setListening, enableServerPlaceholders} from
"../../../../lib/writers/interactiveConsole/state/server.js";
Expand Down Expand Up @@ -83,8 +83,8 @@ test("renderProjectRegion: renders project, type, and version", (t) => {
name: "my.app",
type: "application",
version: "1.0.0",
framework: {name: "SAPUI5", version: "1.150.0"},
});
setFramework(state, {name: "SAPUI5", version: "1.150.0"});
const plain = renderProjectRegion(state).map(stripAnsi).join("\n");
t.regex(plain, /Project\s+my\.app\s+\(application\)\s+v1\.0\.0/);
t.regex(plain, /Framework\s+SAPUI5 1\.150\.0/);
Expand All @@ -96,8 +96,8 @@ test("renderProjectRegion: renders framework without a version when only the nam
name: "my.app",
type: "application",
version: "1.0.0",
framework: {name: "OpenUI5"},
});
setFramework(state, {name: "OpenUI5"});
const plain = renderProjectRegion(state).map(stripAnsi).join("\n");
t.regex(plain, /Framework\s+OpenUI5$/m);
});
Expand All @@ -112,7 +112,6 @@ test("renderProjectRegion: omits the framework row when project has no framework
name: "my.app",
type: "application",
version: "1.0.0",
framework: null,
});
const rendered = renderProjectRegion(state);
t.is(rendered.length, 2, "two lines: separator + Project");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import test from "ava";

import {createProjectState, setProject, enableProjectPlaceholders} from
import {createProjectState, setProject, setFramework, enableProjectPlaceholders} from
"../../../../../lib/writers/interactiveConsole/state/project.js";

test("createProjectState: fresh state has no project/framework and placeholders disabled", (t) => {
Expand All @@ -17,16 +17,28 @@ test("setProject: keeps only {name, type, version} from the incoming event", (t)
name: "my.app",
type: "application",
version: "1.0.0",
framework: {name: "SAPUI5", version: "1.150.0"},
extraNoise: "dropped",
});
t.deepEqual(state.project, {name: "my.app", type: "application", version: "1.0.0"});
t.deepEqual(state.framework, {name: "SAPUI5", version: "1.150.0"});
t.is(state.framework, null);
});

test("setFramework: keeps known framework fields from the incoming event", (t) => {
const state = createProjectState();
setFramework(state, {
name: "SAPUI5",
version: "1.150.0",
extraNoise: "dropped",
});
t.deepEqual(state.framework, {
name: "SAPUI5",
version: "1.150.0",
});
});

test("setProject: framework becomes null when the event omits one", (t) => {
test("setFramework: framework becomes null when the event omits one", (t) => {
const state = createProjectState();
setProject(state, {name: "my.app", type: "application", version: "1.0.0", framework: null});
setFramework(state, null);
t.is(state.framework, null);
});

Expand Down
10 changes: 10 additions & 0 deletions packages/project/lib/graph/helpers/ui5Framework.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Module from "../Module.js";
import ProjectGraph from "../ProjectGraph.js";
import process from "node:process";
import {getLogger} from "@ui5/logger";
const log = getLogger("graph:helpers:ui5Framework");
import Configuration from "../../config/Configuration.js";
Expand Down Expand Up @@ -397,6 +398,15 @@ export default {

const {libraryMetadata} = await resolver.install(referencedLibraries);

if (version) {
process.emit("ui5.project-framework-resolved", {
framework: {
name: frameworkName,
version,
},
});
}

if (log.isLevelEnabled("verbose")) {
const timeDiff = process.hrtime(startTime);
const {default: prettyHrtime} = await import("pretty-hrtime");
Expand Down
7 changes: 2 additions & 5 deletions packages/project/lib/graph/projectGraphBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,12 @@ async function projectGraphBuilder(nodeProvider, workspace) {

// Announce the resolved root project on the event bus, before dependency
// traversal. Consumed by @ui5/logger writers to populate their header /
// scrollback lines. Framework name/version may be null for projects without
// a UI5 framework dependency.
const frameworkName = rootProject.getFrameworkName?.();
const frameworkVersion = rootProject.getFrameworkVersion?.();
// scrollback lines. Framework information is emitted separately once a
// caller actually resolves framework usage for the current run.
process.emit("ui5.project-resolved", {
name: rootProject.getName(),
type: rootProject.getType(),
version: rootProject.getVersion(),
framework: frameworkName ? {name: frameworkName, version: frameworkVersion} : null,
});

function handleExtensions(extensions) {
Expand Down
Loading
Loading