Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d33abac
refactor(project): Handle recoverable build errors without crash
RandomByte Jul 13, 2026
df56422
refactor(project): Gate rebuilds on ERRORED and clear on graph activity
RandomByte Jul 13, 2026
7133d51
refactor(server): Add terminal Express error handler
matz3 Jul 1, 2026
c1e35ca
refactor(project): Drop resurrected resources from delta merge
RandomByte Jul 13, 2026
c6c7e1a
refactor(project): Handle unresolved reads in ResourceRequestManager
RandomByte Jul 13, 2026
a477c90
refactor(project): Extract request-vs-paths predicate in ResourceRequ…
RandomByte Jul 13, 2026
29f8bef
refactor(project): Recover from dropped file watcher events
RandomByte Jul 13, 2026
abbcddf
refactor(project): Reset failing project state to recover build errors
RandomByte Jul 13, 2026
8fd80b9
refactor(project): Emit sourcesChanged on leading edge with trailing …
RandomByte Jul 13, 2026
534e0ee
refactor(project): Defer build restart after abort until changes settle
RandomByte Jul 13, 2026
4f41bcd
refactor(project): Add SETTLING server state for deferred rebuilds
RandomByte Jul 13, 2026
36951bc
refactor(project): Add first-build settle window reporting SETTLING
RandomByte Jul 13, 2026
d02986d
refactor(logger): Drop redundant spinFrame reset on serve-validating
RandomByte Jul 13, 2026
7d4b1b2
docs(project): Sync incremental-build skill with BuildServer state ma…
RandomByte Jul 13, 2026
3656dd5
refactor(server): Serve build-error page with live-reload on HTML nav…
RandomByte Jul 13, 2026
db87ad5
refactor(server): Reuse escape-html in the build-error handler
RandomByte Jul 13, 2026
cdd2f4a
refactor(project): Use a Set for the delta-merge membership check
RandomByte Jul 13, 2026
83c1e34
refactor(project): Drop redundant stale-name args at #setState calls
RandomByte Jul 13, 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