Skip to content
Draft
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
4491977
refactor(project): Handle recoverable build errors without crash
RandomByte Jul 13, 2026
2ca5658
refactor(project): Gate rebuilds on ERRORED and clear on graph activity
RandomByte Jul 13, 2026
7ed90d8
refactor(server): Add terminal Express error handler
matz3 Jul 1, 2026
13d0953
refactor(project): Recover from dropped file watcher events
RandomByte Jul 13, 2026
af5e0a5
refactor(project): Reset failing project state to recover build errors
RandomByte Jul 13, 2026
ebeb25c
refactor(project): Emit sourcesChanged on leading edge with trailing …
RandomByte Jul 13, 2026
886f053
refactor(project): Defer build restart after abort until changes settle
RandomByte Jul 13, 2026
3f3c34a
refactor(project): Add SETTLING server state for deferred rebuilds
RandomByte Jul 13, 2026
a5225c8
refactor(project): Add first-build settle window reporting SETTLING
RandomByte Jul 13, 2026
04ffbbf
refactor(logger): Drop redundant spinFrame reset on serve-validating
RandomByte Jul 13, 2026
7b2380c
docs(project): Sync incremental-build skill with BuildServer state ma…
RandomByte Jul 13, 2026
6c1fd37
refactor(server): Serve build-error page with live-reload on HTML nav…
RandomByte Jul 13, 2026
e00e2a4
refactor(server): Reuse escape-html in the build-error handler
RandomByte Jul 13, 2026
c2a0cd2
refactor(project): Drop redundant stale-name args at #setState calls
RandomByte Jul 13, 2026
e2359eb
refactor(project): Rename failingProjectContext to currentProjectContext
RandomByte Jul 17, 2026
f08d698
refactor(server): Use Node.js response API in errorHandler
RandomByte Jul 17, 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
132 changes: 111 additions & 21 deletions .claude/skills/incremental-build/architecture.md

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion packages/logger/lib/loggers/Serve.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Logger from "./Logger.js";

/**
* Logger for emitting status events on the lifecycle of a UI5 development server
* (idle / stale / building / build-done / error).
* (idle / stale / settling / building / build-done / error).
* <br><br>
* Emits <code>ui5.log</code> and <code>ui5.serve-status</code> events on the
* [<code>process</code>]{@link https://nodejs.org/api/process.html} object, which can be handled
Expand Down Expand Up @@ -46,6 +46,14 @@ class Serve extends Logger {
`Sources changed in: ${changedProjects.join(", ")}`);
}

settling(pendingProjects) {
if (!pendingProjects || !Array.isArray(pendingProjects)) {
throw new Error("loggers/Serve#settling: Missing or incorrect pendingProjects parameter");
}
this.#emitStatus("serve-settling", {pendingProjects},
`Waiting for changes to settle in: ${pendingProjects.join(", ")}`);
}

validating(validatingProjects) {
if (!validatingProjects || !Array.isArray(validatingProjects)) {
throw new Error(
Expand Down
5 changes: 4 additions & 1 deletion packages/logger/lib/writers/InteractiveConsole.js
Original file line number Diff line number Diff line change
Expand Up @@ -354,13 +354,16 @@ class InteractiveConsole {
this.#buildState.changedProjects = evt.changedProjects || [];
this.#transitionTo(STATES.STALE);
break;
case "serve-settling":
this.#buildState.pendingProjects = evt.pendingProjects || [];
this.#transitionTo(STATES.SETTLING);
break;
case "serve-building":
beginBuild(this.#buildState, this.#buildState.projectOrder);
this.#transitionTo(STATES.BUILDING);
break;
case "serve-validating":
this.#buildState.validatingProjects = evt.validatingProjects || [];
this.#buildState.spinFrame = 0;
this.#transitionTo(STATES.VALIDATING);
break;
case "serve-build-done":
Expand Down
5 changes: 5 additions & 0 deletions packages/logger/lib/writers/interactiveConsole/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@ function renderStatusLine(state) {
case STATES.STALE:
return `${label}${chalk.yellow(figures.circle)} ${chalk.yellow(pad("stale"))} ` +
`${chalk.dim("· files changed, rebuild on next request")}`;
case STATES.SETTLING: {
const frame = SPINNER_FRAMES[state.spinFrame % SPINNER_FRAMES.length];
return `${label}${chalk.yellow(frame)} ${chalk.yellow(pad("settling"))} ` +
`${chalk.dim("· waiting for changes to settle")}`;
}
case STATES.VALIDATING: {
const frame = SPINNER_FRAMES[state.spinFrame % SPINNER_FRAMES.length];
const parts = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const STATES = Object.freeze({
STARTING: "starting", // pre-populated placeholder before the first real state arrives
READY: "ready",
STALE: "stale",
SETTLING: "settling", // changes seen, rebuild deferred until they quiesce
BUILDING: "building",
VALIDATING: "validating",
ERROR: "error",
Expand All @@ -14,7 +15,7 @@ export const STATES = Object.freeze({
// States that animate a spinner. Consulted by both the tick scheduler in the
// interactive console writer and the status-line renderer, so a state either
// spins in both places or in neither.
export const SPINNING_STATES = new Set([STATES.BUILDING, STATES.VALIDATING]);
export const SPINNING_STATES = new Set([STATES.SETTLING, STATES.BUILDING, STATES.VALIDATING]);

export function createBuildState() {
return {
Expand All @@ -31,6 +32,9 @@ export function createBuildState() {
// Names of projects collected via `serve-validating` payloads — used to
// label the validating state if/when the renderer wants to.
validatingProjects: [],
// Names of projects collected via `serve-settling` payloads — used to
// label the settling state if/when the renderer wants to.
pendingProjects: [],
// Frame counter for the spinner (incremented by the tick loop).
spinFrame: 0,
// Most recent error captured by `serve-error`.
Expand Down
32 changes: 32 additions & 0 deletions packages/logger/test/lib/loggers/Serve.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,26 @@ test.serial("stale: Missing parameter", (t) => {
}, "Threw with expected error message");
});

test.serial("settling emits serve-settling", (t) => {
const {serveLogger, statusHandler} = t.context;
serveLogger.settling(["project.a", "project.b"]);
t.is(statusHandler.callCount, 1, "One serve-status event emitted");
t.deepEqual(statusHandler.getCall(0).args[0], {
level: "info",
status: "serve-settling",
pendingProjects: ["project.a", "project.b"],
}, "Status event has expected payload");
});

test.serial("settling: Missing parameter", (t) => {
const {serveLogger} = t.context;
t.throws(() => {
serveLogger.settling();
}, {
message: "loggers/Serve#settling: Missing or incorrect pendingProjects parameter",
}, "Threw with expected error message");
});

test.serial("validating emits serve-validating", (t) => {
const {serveLogger, statusHandler} = t.context;
serveLogger.validating(["library.a", "library.b"]);
Expand Down Expand Up @@ -161,6 +181,18 @@ test.serial("No event listener: validating", (t) => {
t.is(logHandler.callCount, 0, "No log event emitted");
});

test.serial("No event listener: settling", (t) => {
const {serveLogger, statusHandler, logHandler, logStub} = t.context;
process.off(ServeLogger.SERVE_STATUS_EVENT_NAME, statusHandler);
serveLogger.settling(["project.a", "project.b"]);
t.is(logStub.callCount, 1, "_log got called once");
t.is(logStub.getCall(0).args[0], "info", "Logged with expected log-level");
t.is(logStub.getCall(0).args[1],
"Waiting for changes to settle in: project.a, project.b",
"Logged expected message");
t.is(logHandler.callCount, 0, "No log event emitted");
});

test.serial("No event listener: building", (t) => {
const {serveLogger, statusHandler, logHandler, logStub} = t.context;
process.off(ServeLogger.SERVE_STATUS_EVENT_NAME, statusHandler);
Expand Down
18 changes: 18 additions & 0 deletions packages/logger/test/lib/writers/InteractiveConsole.js
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,24 @@ test.serial("serve-status: serve-stale without a payload falls back to an empty
writer.disable();
});

test.serial("serve-status: serve-settling records pendingProjects and transitions to SETTLING", (t) => {
const {writer, stderr} = createWriter();
process.emit("ui5.serve-status", {status: "serve-settling", pendingProjects: ["my.app"]});
const state = writer._getStateForTest().build;
t.is(state.state, STATES.SETTLING);
t.deepEqual(state.pendingProjects, ["my.app"]);
const tail = stripAnsi(stderr.writes.slice(-5).join(""));
t.regex(tail, /settling/);
writer.disable();
});

test.serial("serve-status: serve-settling without a payload falls back to an empty list", (t) => {
const {writer} = createWriter();
process.emit("ui5.serve-status", {status: "serve-settling"});
t.deepEqual(writer._getStateForTest().build.pendingProjects, []);
writer.disable();
});

test.serial("serve-status: serve-validating records projects and transitions to VALIDATING", (t) => {
const {writer, stderr} = createWriter();
process.emit("ui5.serve-status", {
Expand Down
7 changes: 7 additions & 0 deletions packages/logger/test/lib/writers/interactiveConsole/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,13 @@ test("renderBuildRegion: stale state includes 'files changed' hint", (t) => {
t.regex(plain, /Status\s+.+?\s+stale\s+·\s+files changed/);
});

test("renderBuildRegion: settling state includes 'waiting for changes to settle' hint", (t) => {
const state = createBuildState();
transitionTo(state, STATES.SETTLING);
const plain = renderBuildRegion(state).map(stripAnsi).join("\n");
t.regex(plain, /Status\s+.+?\s+settling\s+·\s+waiting for changes to settle/);
});

test("renderBuildRegion: validating state includes 'checking dependency caches' hint", (t) => {
const state = createBuildState();
transitionTo(state, STATES.VALIDATING);
Expand Down
Loading
Loading