From e69992c39987d3173546a1f17d144ac663320faa Mon Sep 17 00:00:00 2001 From: Aditya Jalkhare Date: Sat, 9 May 2026 02:57:44 +0530 Subject: [PATCH 01/21] Implement collapsible console sections with line count display --- .../pipeline-console/main/ConsoleLogCard.tsx | 7 + .../main/ConsoleLogStream.tsx | 24 ++- .../main/ConsoleSection.spec.tsx | 150 ++++++++++++++++++ .../pipeline-console/main/ConsoleSection.tsx | 102 ++++++++++++ .../main/console-log-card.scss | 8 + .../main/console-section.scss | 64 ++++++++ .../main/parseConsoleSections.spec.ts | 42 +++++ .../main/parseConsoleSections.ts | 41 +++++ 8 files changed, 431 insertions(+), 7 deletions(-) create mode 100644 src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.spec.tsx create mode 100644 src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.tsx create mode 100644 src/main/frontend/pipeline-console-view/pipeline-console/main/console-section.scss create mode 100644 src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts create mode 100644 src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogCard.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogCard.tsx index a9b675869..91681258a 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogCard.tsx +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogCard.tsx @@ -55,6 +55,8 @@ export default function ConsoleLogCard({ const messages = useMessages(); + const lineCount = stepBuffers.get(step.id)?.lines?.length ?? 0; + const inputStep = step.inputStep; if (inputStep && !inputStep.parameters) { return ; @@ -106,6 +108,11 @@ export default function ConsoleLogCard({
+ {lineCount > 0 && ( + + {lineCount} {lineCount === 1 ? "line" : "lines"} + + )} parseConsoleSections(logBuffer.lines), + [logBuffer.lines], + ); + return (
- {logBuffer.lines.map((content, index) => ( - ( + { + const baseProps = { + stepId: "5", + startByte: 0, + stopTailingLogs: vi.fn(), + currentRunPath: "/jenkins/job/test/1/", + }; + + it("renders a line node as ConsoleLine", () => { + const node: ConsoleSectionLine = { + kind: "line", + index: 0, + content: "Hello, world!", + }; + const { container } = render( + , + ); + expect(container.querySelector(".console-output-line")).toBeTruthy(); + expect(container.textContent).toContain("Hello, world!"); + }); + + it("renders a group node as details/summary", () => { + const node: ConsoleSectionGroup = { + kind: "group", + title: "Build", + startIndex: 0, + endIndex: 3, + children: [ + { kind: "line", index: 1, content: "compiling..." }, + { kind: "line", index: 2, content: "done" }, + ], + }; + const { container } = render( + , + ); + const details = container.querySelector("details"); + expect(details).toBeTruthy(); + expect( + container.querySelector(".pgv-console-section__title")?.textContent, + ).toBe("Build"); + expect( + container.querySelector(".pgv-console-section__count")?.textContent, + ).toContain("2 lines"); + }); +}); + +describe("ConsoleSection", () => { + const baseProps = { + stepId: "5", + startByte: 0, + stopTailingLogs: vi.fn(), + currentRunPath: "/jenkins/job/test/1/", + }; + + it("renders closed by default for a closed group", () => { + const group: ConsoleSectionGroup = { + kind: "group", + title: "Install", + startIndex: 0, + endIndex: 5, + children: [{ kind: "line", index: 1, content: "npm ci" }], + }; + const { container } = render( + , + ); + const details = container.querySelector("details"); + expect(details).toBeTruthy(); + expect(details!.open).toBe(false); + }); + + it("renders open by default for an unclosed group (still streaming)", () => { + const group: ConsoleSectionGroup = { + kind: "group", + title: "Running", + startIndex: 0, + endIndex: -1, + children: [{ kind: "line", index: 1, content: "in progress..." }], + }; + const { container } = render( + , + ); + const details = container.querySelector("details"); + expect(details).toBeTruthy(); + expect(details!.open).toBe(true); + }); + + it("displays title and line count", () => { + const group: ConsoleSectionGroup = { + kind: "group", + title: "Test Suite", + startIndex: 0, + endIndex: 4, + children: [ + { kind: "line", index: 1, content: "test 1" }, + { kind: "line", index: 2, content: "test 2" }, + { kind: "line", index: 3, content: "test 3" }, + ], + }; + render(); + expect(screen.getByText("Test Suite")).toBeTruthy(); + expect(screen.getByText(/3 lines/)).toBeTruthy(); + }); + + it("shows singular 'line' for single child", () => { + const group: ConsoleSectionGroup = { + kind: "group", + title: "Single", + startIndex: 0, + endIndex: 2, + children: [{ kind: "line", index: 1, content: "only" }], + }; + render(); + expect(screen.getByText(/1 line$/)).toBeTruthy(); + }); + + it("toggles open/closed on click", async () => { + const group: ConsoleSectionGroup = { + kind: "group", + title: "Toggle Me", + startIndex: 0, + endIndex: 3, + children: [{ kind: "line", index: 1, content: "inside" }], + }; + const { container } = render( + , + ); + const summary = container.querySelector("summary")!; + const details = container.querySelector("details")!; + expect(details.open).toBe(false); + + fireEvent.click(summary); + expect(details.open).toBe(true); + + fireEvent.click(summary); + expect(details.open).toBe(false); + }); +}); diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.tsx new file mode 100644 index 000000000..4c933376d --- /dev/null +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.tsx @@ -0,0 +1,102 @@ +import { memo, useState } from "react"; + +import { + ConsoleSectionGroup, + ConsoleSectionNode, +} from "./parseConsoleSections.ts"; +import { ConsoleLine } from "./ConsoleLine.tsx"; + +export interface ConsoleSectionProps { + group: ConsoleSectionGroup; + stepId: string; + startByte: number; + stopTailingLogs: () => void; + currentRunPath: string; +} + +export const ConsoleSection = memo(function ConsoleSection({ + group, + stepId, + startByte, + stopTailingLogs, + currentRunPath, +}: ConsoleSectionProps) { + // Unclosed groups (still streaming) default to open. + const [open, setOpen] = useState(group.endIndex === -1); + + return ( +
setOpen((e.target as HTMLDetailsElement).open)} + className="pgv-console-section" + > + + {group.title} + + {group.children.length}{" "} + {group.children.length === 1 ? "line" : "lines"} + + +
+ {group.children.map((node) => ( + + ))} +
+
+ ); +}); + +export interface ConsoleSectionNodeRendererProps { + node: ConsoleSectionNode; + stepId: string; + startByte: number; + stopTailingLogs: () => void; + currentRunPath: string; +} + +export const ConsoleSectionNodeRenderer = memo( + function ConsoleSectionNodeRenderer({ + node, + stepId, + startByte, + stopTailingLogs, + currentRunPath, + }: ConsoleSectionNodeRendererProps) { + if (node.kind === "line") { + return ( + + ); + } + return ( + + ); + }, +); + +function nodeKey(node: ConsoleSectionNode): string { + return node.kind === "line" + ? `line-${node.index}` + : `section-${node.startIndex}`; +} diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/console-log-card.scss b/src/main/frontend/pipeline-console-view/pipeline-console/main/console-log-card.scss index 2af10269d..d99e871c6 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/console-log-card.scss +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/console-log-card.scss @@ -99,6 +99,14 @@ } } +.pgv-step-detail-header__line-count { + color: var(--text-color-secondary); + font-size: var(--font-size-xs); + font-weight: normal; + white-space: nowrap; + opacity: 0.75; +} + .pgv-show-more-logs { position: sticky; top: calc(var(--pgv-header-height) + 0.375rem + 0.375rem + 2.375rem); diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/console-section.scss b/src/main/frontend/pipeline-console-view/pipeline-console/main/console-section.scss new file mode 100644 index 000000000..d930b7271 --- /dev/null +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/console-section.scss @@ -0,0 +1,64 @@ +.pgv-console-section { + border-left: 2px solid + color-mix(in srgb, var(--text-color-secondary) 20%, transparent); + margin-left: 0.25rem; + padding-left: 0.5rem; + margin-block: 2px; + + &[open] > .pgv-console-section__summary { + margin-bottom: 2px; + } +} + +.pgv-console-section__summary { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + font-size: var(--font-size-sm); + padding: 2px 0.25rem; + border-radius: 0.25rem; + user-select: none; + list-style: none; + + &::-webkit-details-marker { + display: none; + } + + &::before { + content: "▶"; + font-size: 0.6em; + transition: transform 0.15s ease; + display: inline-block; + color: var(--text-color-secondary); + } + + &:hover { + background-color: color-mix( + in srgb, + var(--text-color-secondary) 5%, + transparent + ); + } + + details[open] > & { + &::before { + transform: rotate(90deg); + } + } +} + +.pgv-console-section__title { + font-weight: var(--font-bold-weight); + color: var(--text-color); +} + +.pgv-console-section__count { + color: var(--text-color-secondary); + font-size: var(--font-size-xs); + opacity: 0.75; +} + +.pgv-console-section__body { + /* No extra padding needed; lines have their own styling. */ +} diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts new file mode 100644 index 000000000..12df02d77 --- /dev/null +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts @@ -0,0 +1,42 @@ +/** * @vitest-environment jsdom */ + +import { parseConsoleSections } from "./parseConsoleSections.ts"; + +describe("parseConsoleSections", () => { + it("returns empty array for empty input", () => { + const result = parseConsoleSections([]); + expect(result).toEqual([]); + }); + + it("returns flat line nodes for plain lines", () => { + const lines = ["hello", "world", "foo"]; + const result = parseConsoleSections(lines); + expect(result).toHaveLength(3); + expect(result[0]).toEqual({ kind: "line", index: 0, content: "hello" }); + expect(result[1]).toEqual({ kind: "line", index: 1, content: "world" }); + expect(result[2]).toEqual({ kind: "line", index: 2, content: "foo" }); + }); + + it("preserves original line indices", () => { + const lines = ["a", "b", "c", "d"]; + const result = parseConsoleSections(lines); + for (let i = 0; i < lines.length; i++) { + expect(result[i]).toMatchObject({ kind: "line", index: i }); + } + }); + + it("handles single line input", () => { + const result = parseConsoleSections(["only line"]); + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ kind: "line", index: 0, content: "only line" }); + }); + + it("preserves ANSI escape sequences in content", () => { + const lines = ["\x1b[31mred text\x1b[0m", "normal"]; + const result = parseConsoleSections(lines); + expect(result[0]).toMatchObject({ + kind: "line", + content: "\x1b[31mred text\x1b[0m", + }); + }); +}); diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts new file mode 100644 index 000000000..1f2d2509b --- /dev/null +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts @@ -0,0 +1,41 @@ +/** + * Console section parser. + * + * Phase 1: flat pass-through (every line is a top-level node). + * Phase 2: marker detection (##[group]/##[endgroup] and ::group::/::endgroup::). + */ + +export interface ConsoleSectionLine { + kind: "line"; + /** Original 0-based index into the raw lines array. */ + index: number; + content: string; +} + +export interface ConsoleSectionGroup { + kind: "group"; + title: string; + /** Index of the marker line that opened this group. */ + startIndex: number; + /** Index of the marker line that closed this group, or -1 if unclosed. */ + endIndex: number; + children: ConsoleSectionNode[]; +} + +export type ConsoleSectionNode = ConsoleSectionLine | ConsoleSectionGroup; + +/** + * Parse raw console lines into a tree of sections. + * + * Currently returns a flat list (every line is a top-level node). + * Phase 2 will add marker detection here. + */ +export function parseConsoleSections( + lines: string[], +): ConsoleSectionNode[] { + return lines.map((content, index) => ({ + kind: "line" as const, + index, + content, + })); +} From 74b8108ddae936941587c3602c148f90708ccbfd Mon Sep 17 00:00:00 2001 From: Aditya Jalkhare Date: Sat, 9 May 2026 02:59:46 +0530 Subject: [PATCH 02/21] Enhance console section parser with marker detection and nesting support --- .../main/parseConsoleSections.spec.ts | 295 +++++++++++++++++- .../main/parseConsoleSections.ts | 89 +++++- 2 files changed, 374 insertions(+), 10 deletions(-) diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts index 12df02d77..6ef36baa4 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts @@ -1,8 +1,14 @@ /** * @vitest-environment jsdom */ -import { parseConsoleSections } from "./parseConsoleSections.ts"; +import { + parseConsoleSections, + ConsoleSectionGroup, + ConsoleSectionLine, +} from "./parseConsoleSections.ts"; describe("parseConsoleSections", () => { + // --- Phase 1: flat pass-through (no markers) --- + it("returns empty array for empty input", () => { const result = parseConsoleSections([]); expect(result).toEqual([]); @@ -39,4 +45,291 @@ describe("parseConsoleSections", () => { content: "\x1b[31mred text\x1b[0m", }); }); + + // --- Phase 2: marker detection --- + + describe("Azure DevOps syntax (##[group]/##[endgroup])", () => { + it("creates a group from ##[group] and ##[endgroup]", () => { + const lines = [ + "before", + "##[group]Build", + "compiling...", + "done", + "##[endgroup]", + "after", + ]; + const result = parseConsoleSections(lines); + expect(result).toHaveLength(3); + expect(result[0]).toMatchObject({ kind: "line", content: "before" }); + expect(result[2]).toMatchObject({ kind: "line", content: "after" }); + + const group = result[1] as ConsoleSectionGroup; + expect(group.kind).toBe("group"); + expect(group.title).toBe("Build"); + expect(group.startIndex).toBe(1); + expect(group.endIndex).toBe(4); + expect(group.children).toHaveLength(2); + expect(group.children[0]).toMatchObject({ + kind: "line", + index: 2, + content: "compiling...", + }); + expect(group.children[1]).toMatchObject({ + kind: "line", + index: 3, + content: "done", + }); + }); + + it("uses 'Section' as default title when none provided", () => { + const lines = ["##[group]", "inside", "##[endgroup]"]; + const result = parseConsoleSections(lines); + const group = result[0] as ConsoleSectionGroup; + expect(group.title).toBe("Section"); + }); + + it("preserves trailing whitespace in title", () => { + const lines = ["##[group]Install Dependencies ", "npm ci", "##[endgroup]"]; + const result = parseConsoleSections(lines); + const group = result[0] as ConsoleSectionGroup; + expect(group.title).toBe("Install Dependencies "); + }); + }); + + describe("GitHub Actions syntax (::group::/::endgroup::)", () => { + it("creates a group from ::group:: and ::endgroup::", () => { + const lines = [ + "::group::Test Suite", + "test 1 passed", + "test 2 passed", + "::endgroup::", + ]; + const result = parseConsoleSections(lines); + expect(result).toHaveLength(1); + + const group = result[0] as ConsoleSectionGroup; + expect(group.kind).toBe("group"); + expect(group.title).toBe("Test Suite"); + expect(group.startIndex).toBe(0); + expect(group.endIndex).toBe(3); + expect(group.children).toHaveLength(2); + }); + }); + + describe("mixed syntax", () => { + it("handles ##[group] opened and ::endgroup:: closed", () => { + const lines = [ + "##[group]Mixed", + "inside", + "::endgroup::", + ]; + const result = parseConsoleSections(lines); + expect(result).toHaveLength(1); + const group = result[0] as ConsoleSectionGroup; + expect(group.title).toBe("Mixed"); + expect(group.endIndex).toBe(2); + }); + + it("handles ::group:: opened and ##[endgroup] closed", () => { + const lines = [ + "::group::Mixed2", + "inside", + "##[endgroup]", + ]; + const result = parseConsoleSections(lines); + const group = result[0] as ConsoleSectionGroup; + expect(group.title).toBe("Mixed2"); + expect(group.endIndex).toBe(2); + }); + }); + + describe("ANSI stripping for detection", () => { + it("detects marker wrapped in ANSI codes", () => { + const lines = [ + "\x1b[32m##[group]Colored\x1b[0m", + "inside", + "\x1b[32m##[endgroup]\x1b[0m", + ]; + const result = parseConsoleSections(lines); + expect(result).toHaveLength(1); + const group = result[0] as ConsoleSectionGroup; + expect(group.kind).toBe("group"); + expect(group.title).toBe("Colored"); + }); + + it("detects marker with leading whitespace", () => { + const lines = [ + " ##[group]Indented", + "inside", + " ##[endgroup]", + ]; + const result = parseConsoleSections(lines); + expect(result).toHaveLength(1); + expect((result[0] as ConsoleSectionGroup).title).toBe("Indented"); + }); + }); + + describe("shell trace rejection", () => { + it("does not treat '+ echo ##[group]...' as a marker", () => { + const lines = [ + "+ echo ##[group]Build", + "##[group]Build", + "compiling", + "##[endgroup]", + ]; + const result = parseConsoleSections(lines); + expect(result).toHaveLength(2); + expect(result[0]).toMatchObject({ + kind: "line", + index: 0, + content: "+ echo ##[group]Build", + }); + const group = result[1] as ConsoleSectionGroup; + expect(group.kind).toBe("group"); + expect(group.title).toBe("Build"); + }); + + it("does not treat '+ echo ::group::...' as a marker", () => { + const lines = [ + "+ echo ::group::Test", + "::group::Test", + "testing", + "::endgroup::", + ]; + const result = parseConsoleSections(lines); + expect(result[0]).toMatchObject({ kind: "line" }); + expect(result[1]).toMatchObject({ kind: "group" }); + }); + }); + + describe("nesting", () => { + it("supports nested groups", () => { + const lines = [ + "##[group]Outer", + "outer line", + "##[group]Inner", + "inner line", + "##[endgroup]", + "after inner", + "##[endgroup]", + ]; + const result = parseConsoleSections(lines); + expect(result).toHaveLength(1); + + const outer = result[0] as ConsoleSectionGroup; + expect(outer.title).toBe("Outer"); + expect(outer.children).toHaveLength(3); + expect(outer.children[0]).toMatchObject({ + kind: "line", + content: "outer line", + }); + + const inner = outer.children[1] as ConsoleSectionGroup; + expect(inner.kind).toBe("group"); + expect(inner.title).toBe("Inner"); + expect(inner.children).toHaveLength(1); + expect(inner.children[0]).toMatchObject({ + kind: "line", + content: "inner line", + }); + + expect(outer.children[2]).toMatchObject({ + kind: "line", + content: "after inner", + }); + }); + + it("supports deeply nested groups", () => { + const lines = [ + "##[group]L1", + "##[group]L2", + "##[group]L3", + "deep", + "##[endgroup]", + "##[endgroup]", + "##[endgroup]", + ]; + const result = parseConsoleSections(lines); + const l1 = result[0] as ConsoleSectionGroup; + const l2 = l1.children[0] as ConsoleSectionGroup; + const l3 = l2.children[0] as ConsoleSectionGroup; + expect(l3.children[0]).toMatchObject({ + kind: "line", + content: "deep", + }); + }); + }); + + describe("unclosed groups", () => { + it("leaves endIndex as -1 for unclosed group", () => { + const lines = [ + "##[group]Streaming", + "line 1", + "line 2", + ]; + const result = parseConsoleSections(lines); + expect(result).toHaveLength(1); + const group = result[0] as ConsoleSectionGroup; + expect(group.endIndex).toBe(-1); + expect(group.children).toHaveLength(2); + }); + + it("handles unclosed nested group", () => { + const lines = [ + "##[group]Outer", + "##[group]Inner", + "still going", + ]; + const result = parseConsoleSections(lines); + const outer = result[0] as ConsoleSectionGroup; + expect(outer.endIndex).toBe(-1); + const inner = outer.children[0] as ConsoleSectionGroup; + expect(inner.endIndex).toBe(-1); + }); + }); + + describe("stray endgroup", () => { + it("treats stray ##[endgroup] as a plain line", () => { + const lines = [ + "some output", + "##[endgroup]", + "more output", + ]; + const result = parseConsoleSections(lines); + expect(result).toHaveLength(3); + expect(result[1]).toMatchObject({ + kind: "line", + index: 1, + content: "##[endgroup]", + }); + }); + }); + + describe("multiple groups", () => { + it("handles sequential sibling groups", () => { + const lines = [ + "##[group]First", + "a", + "##[endgroup]", + "between", + "##[group]Second", + "b", + "##[endgroup]", + ]; + const result = parseConsoleSections(lines); + expect(result).toHaveLength(3); + expect((result[0] as ConsoleSectionGroup).title).toBe("First"); + expect(result[1]).toMatchObject({ kind: "line", content: "between" }); + expect((result[2] as ConsoleSectionGroup).title).toBe("Second"); + }); + }); + + describe("no-marker regression (Phase 1 compat)", () => { + it("returns flat lines when no markers are present", () => { + const lines = ["hello", "world", "foo"]; + const result = parseConsoleSections(lines); + expect(result).toHaveLength(3); + expect(result.every((n) => n.kind === "line")).toBe(true); + }); + }); }); diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts index 1f2d2509b..cadc85dd8 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts @@ -1,8 +1,14 @@ /** * Console section parser. * - * Phase 1: flat pass-through (every line is a top-level node). - * Phase 2: marker detection (##[group]/##[endgroup] and ::group::/::endgroup::). + * Detects collapsible section markers in console output: + * - Azure DevOps / GitHub Actions style: ##[group]Title / ##[endgroup] + * - GitHub Actions style alias: ::group::Title / ::endgroup:: + * + * Lines between markers are grouped into ConsoleSectionGroup nodes. + * Unmatched lines remain flat ConsoleSectionLine nodes. + * Nesting is supported (groups can contain groups). + * Unclosed groups auto-close at end of input (endIndex = -1). */ export interface ConsoleSectionLine { @@ -24,18 +30,83 @@ export interface ConsoleSectionGroup { export type ConsoleSectionNode = ConsoleSectionLine | ConsoleSectionGroup; +// ANSI escape sequence pattern for stripping before marker detection. +const ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]/g; + +// Start markers: ##[group]Title or ::group::Title +const GROUP_START_RE = /^(?:##\[group\]|::group::)\s*(.*)$/; + +// End markers: ##[endgroup] or ::endgroup:: +const GROUP_END_RE = /^(?:##\[endgroup\]|::endgroup::)\s*$/; + +/** + * Strip ANSI escape codes and leading whitespace from a line + * for marker detection purposes. The original content is preserved + * in the output nodes. + */ +function stripForDetection(line: string): string { + return line.replace(ANSI_RE, "").trimStart(); +} + /** * Parse raw console lines into a tree of sections. * - * Currently returns a flat list (every line is a top-level node). - * Phase 2 will add marker detection here. + * Detects ##[group]/##[endgroup] and ::group::/::endgroup:: markers. + * Lines between markers are grouped; all other lines remain flat. + * Supports nesting. Unclosed groups get endIndex = -1. */ export function parseConsoleSections( lines: string[], ): ConsoleSectionNode[] { - return lines.map((content, index) => ({ - kind: "line" as const, - index, - content, - })); + const root: ConsoleSectionNode[] = []; + // Stack of open groups: each entry is the children array of the current group. + const stack: { group: ConsoleSectionGroup; parent: ConsoleSectionNode[] }[] = + []; + + function current(): ConsoleSectionNode[] { + return stack.length > 0 ? stack[stack.length - 1].group.children : root; + } + + for (let i = 0; i < lines.length; i++) { + const raw = lines[i]; + const stripped = stripForDetection(raw); + + // Reject lines that look like shell trace of the echo command. + // e.g. "+ echo ##[group]Build" should not be treated as a marker. + if (stripped.startsWith("+ ")) { + current().push({ kind: "line", index: i, content: raw }); + continue; + } + + const startMatch = GROUP_START_RE.exec(stripped); + if (startMatch) { + const group: ConsoleSectionGroup = { + kind: "group", + title: startMatch[1] || "Section", + startIndex: i, + endIndex: -1, + children: [], + }; + current().push(group); + stack.push({ group, parent: current() }); + continue; + } + + if (GROUP_END_RE.test(stripped)) { + if (stack.length > 0) { + stack[stack.length - 1].group.endIndex = i; + stack.pop(); + } + // If no open group, ignore the stray endgroup marker (treat as normal line). + else { + current().push({ kind: "line", index: i, content: raw }); + } + continue; + } + + current().push({ kind: "line", index: i, content: raw }); + } + + // Unclosed groups keep endIndex = -1 (still streaming). + return root; } From 6e32a31d9fceeef5fa0a7fc0ade5b0bed8678487 Mon Sep 17 00:00:00 2001 From: Aditya Jalkhare Date: Sat, 9 May 2026 03:04:17 +0530 Subject: [PATCH 03/21] Add console section rules and related functionality for collapsible sections --- src/main/frontend/common/RestClient.tsx | 21 +++ .../main/parseConsoleSections.spec.ts | 149 ++++++++++++++++++ .../main/parseConsoleSections.ts | 140 ++++++++++++++++ .../BuiltInConsoleSectionRules.java | 132 ++++++++++++++++ .../consoleview/ConsoleSectionRule.java | 66 ++++++++ .../PipelineConsoleViewAction.java | 21 +++ .../consoleview/ConsoleSectionRuleTest.java | 104 ++++++++++++ 7 files changed, 633 insertions(+) create mode 100644 src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/BuiltInConsoleSectionRules.java create mode 100644 src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionRule.java create mode 100644 src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionRuleTest.java diff --git a/src/main/frontend/common/RestClient.tsx b/src/main/frontend/common/RestClient.tsx index 4f2954dca..9bd45410a 100644 --- a/src/main/frontend/common/RestClient.tsx +++ b/src/main/frontend/common/RestClient.tsx @@ -152,6 +152,27 @@ export async function getConsoleBuildOutput( } } +export interface ConsoleSectionRuleData { + id: string; + displayName: string; + startPattern: string; + endPattern: string; + enabledByDefault: boolean; +} + +export async function getConsoleSectionRules( + url: string, +): Promise { + try { + const response = await fetch(`${url}stages/consoleSectionRules`); + if (!response.ok) throw response.statusText; + return await response.json(); + } catch (e) { + console.error(`Caught error when fetching console section rules: '${e}'`); + return []; + } +} + export async function getResourceBundle( resource: string, ): Promise { diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts index 6ef36baa4..c12066b14 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts @@ -4,6 +4,8 @@ import { parseConsoleSections, ConsoleSectionGroup, ConsoleSectionLine, + compileSectionRules, + applyRulesToSections, } from "./parseConsoleSections.ts"; describe("parseConsoleSections", () => { @@ -333,3 +335,150 @@ describe("parseConsoleSections", () => { }); }); }); + +describe("compileSectionRules", () => { + it("compiles valid rules", () => { + const rules = compileSectionRules([ + { + id: "test", + displayName: "Test", + startPattern: "^start$", + endPattern: "^end$", + }, + ]); + expect(rules).toHaveLength(1); + expect(rules[0].startPattern.test("start")).toBe(true); + expect(rules[0].endPattern.test("end")).toBe(true); + }); + + it("skips rules with invalid regex", () => { + const rules = compileSectionRules([ + { + id: "bad", + displayName: "Bad", + startPattern: "^valid$", + endPattern: "[invalid", + }, + { + id: "good", + displayName: "Good", + startPattern: "^start$", + endPattern: "^end$", + }, + ]); + expect(rules).toHaveLength(1); + expect(rules[0].id).toBe("good"); + }); + + it("returns empty for empty input", () => { + expect(compileSectionRules([])).toEqual([]); + }); +}); + +describe("applyRulesToSections", () => { + const mavenRules = compileSectionRules([ + { + id: "maven", + displayName: "Maven Phase", + startPattern: "^\\[INFO\\] --- (.+) ---$", + endPattern: "^\\[INFO\\] --- .+ ---|^\\[INFO\\] -+$", + }, + ]); + + it("creates a group from matching start/end lines", () => { + const lines = [ + "[INFO] --- maven-compiler-plugin:3.8.1:compile (default) ---", + "[INFO] Compiling 5 source files", + "[INFO] All files up to date", + "[INFO] -------------------------------------------------------", + ]; + const flat = parseConsoleSections(lines); + const result = applyRulesToSections(flat, mavenRules); + expect(result).toHaveLength(2); + const group = result[0] as ConsoleSectionGroup; + expect(group.kind).toBe("group"); + expect(group.title).toBe("maven-compiler-plugin:3.8.1:compile (default)"); + expect(group.children).toHaveLength(2); + // The end line is not inside the group. + expect(result[1]).toMatchObject({ kind: "line", index: 3 }); + }); + + it("chains sequential groups when end line matches new start", () => { + const lines = [ + "[INFO] --- maven-compiler-plugin:3.8.1:compile ---", + "compiling...", + "[INFO] --- maven-surefire-plugin:2.22:test ---", + "testing...", + "[INFO] -------------------------------------------------------", + ]; + const flat = parseConsoleSections(lines); + const result = applyRulesToSections(flat, mavenRules); + expect(result).toHaveLength(3); + expect((result[0] as ConsoleSectionGroup).title).toBe( + "maven-compiler-plugin:3.8.1:compile", + ); + expect((result[0] as ConsoleSectionGroup).children).toHaveLength(1); + expect((result[1] as ConsoleSectionGroup).title).toBe( + "maven-surefire-plugin:2.22:test", + ); + expect((result[1] as ConsoleSectionGroup).children).toHaveLength(1); + }); + + it("passes through marker-based groups unchanged", () => { + const lines = [ + "##[group]Custom", + "inside custom", + "##[endgroup]", + "[INFO] --- maven-compiler-plugin:3.8.1:compile ---", + "compiling", + "[INFO] -------------------------------------------------------", + ]; + const markerParsed = parseConsoleSections(lines); + const result = applyRulesToSections(markerParsed, mavenRules); + // First is the marker group, second is the maven rule group, third is the end line. + expect(result).toHaveLength(3); + expect((result[0] as ConsoleSectionGroup).title).toBe("Custom"); + expect((result[1] as ConsoleSectionGroup).title).toBe( + "maven-compiler-plugin:3.8.1:compile", + ); + }); + + it("returns nodes unchanged when no rules provided", () => { + const lines = ["a", "b", "c"]; + const flat = parseConsoleSections(lines); + const result = applyRulesToSections(flat, []); + expect(result).toEqual(flat); + }); + + it("leaves unclosed rule group with endIndex -1", () => { + const lines = [ + "[INFO] --- maven-compiler-plugin:3.8.1:compile ---", + "still compiling...", + ]; + const flat = parseConsoleSections(lines); + const result = applyRulesToSections(flat, mavenRules); + expect(result).toHaveLength(1); + const group = result[0] as ConsoleSectionGroup; + expect(group.endIndex).toBe(-1); + expect(group.children).toHaveLength(1); + }); + + it("uses displayName as fallback when no capture group", () => { + const noCapture = compileSectionRules([ + { + id: "tf", + displayName: "Terraform Plan", + startPattern: "^Terraform will perform", + endPattern: "^Plan: ", + }, + ]); + const lines = [ + "Terraform will perform the following actions:", + " + resource", + "Plan: 1 to add", + ]; + const flat = parseConsoleSections(lines); + const result = applyRulesToSections(flat, noCapture); + expect((result[0] as ConsoleSectionGroup).title).toBe("Terraform Plan"); + }); +}); diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts index cadc85dd8..a4b6817ad 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts @@ -110,3 +110,143 @@ export function parseConsoleSections( // Unclosed groups keep endIndex = -1 (still streaming). return root; } + +/** + * A compiled section rule for client-side application. + */ +export interface CompiledSectionRule { + id: string; + displayName: string; + startPattern: RegExp; + endPattern: RegExp; +} + +/** + * Compile rule data from the server into RegExp-bearing objects. + * Invalid patterns are silently skipped. + */ +export function compileSectionRules( + rules: Array<{ + id: string; + displayName: string; + startPattern: string; + endPattern: string; + }>, +): CompiledSectionRule[] { + const compiled: CompiledSectionRule[] = []; + for (const rule of rules) { + try { + compiled.push({ + id: rule.id, + displayName: rule.displayName, + startPattern: new RegExp(rule.startPattern), + endPattern: new RegExp(rule.endPattern), + }); + } catch { + // Skip rules with invalid regex. + } + } + return compiled; +} + +/** + * Apply compiled section rules to a flat list of ConsoleSectionNode[]. + * + * Walks the top-level lines, matching start/end patterns to create groups. + * Already-grouped nodes (from marker detection) are left untouched. + * Rules are applied in order; the first matching rule wins for a given line. + */ +export function applyRulesToSections( + nodes: ConsoleSectionNode[], + rules: CompiledSectionRule[], +): ConsoleSectionNode[] { + if (rules.length === 0) return nodes; + + const result: ConsoleSectionNode[] = []; + let activeRule: CompiledSectionRule | null = null; + let activeGroup: ConsoleSectionGroup | null = null; + + for (const node of nodes) { + // Only process flat lines; pass groups through unchanged. + if (node.kind === "group") { + if (activeGroup) { + activeGroup.children.push(node); + } else { + result.push(node); + } + continue; + } + + const stripped = node.content.replace(ANSI_RE, "").trimStart(); + + // Check if current line ends an active rule-based group. + if (activeRule && activeGroup && activeRule.endPattern.test(stripped)) { + // The end-line starts a new section of the same rule (e.g. next Maven phase). + const startMatch = matchAnyRule(stripped, rules); + if (startMatch) { + activeGroup.endIndex = node.index; + result.push(activeGroup); + activeGroup = { + kind: "group", + title: startMatch.title, + startIndex: node.index, + endIndex: -1, + children: [], + }; + activeRule = startMatch.rule; + continue; + } + activeGroup.endIndex = node.index; + result.push(activeGroup); + activeGroup = null; + activeRule = null; + result.push(node); + continue; + } + + // Check if a new rule-based group starts. + if (!activeRule) { + const startMatch = matchAnyRule(stripped, rules); + if (startMatch) { + activeGroup = { + kind: "group", + title: startMatch.title, + startIndex: node.index, + endIndex: -1, + children: [], + }; + activeRule = startMatch.rule; + continue; + } + } + + if (activeGroup) { + activeGroup.children.push(node); + } else { + result.push(node); + } + } + + // Close any unclosed rule-based group. + if (activeGroup) { + result.push(activeGroup); + } + + return result; +} + +function matchAnyRule( + stripped: string, + rules: CompiledSectionRule[], +): { rule: CompiledSectionRule; title: string } | null { + for (const rule of rules) { + const m = rule.startPattern.exec(stripped); + if (m) { + return { + rule, + title: m[1] || rule.displayName, + }; + } + } + return null; +} diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/BuiltInConsoleSectionRules.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/BuiltInConsoleSectionRules.java new file mode 100644 index 000000000..56a2b9045 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/BuiltInConsoleSectionRules.java @@ -0,0 +1,132 @@ +package io.jenkins.plugins.pipelinegraphview.consoleview; + +import hudson.Extension; + +/** + * Built-in {@link ConsoleSectionRule} implementations for common CI tool output. + */ +public final class BuiltInConsoleSectionRules { + + private BuiltInConsoleSectionRules() {} + + @Extension + public static class MavenPhaseRule extends ConsoleSectionRule { + @Override + public String getId() { + return "maven-phase"; + } + + @Override + public String getDisplayName() { + return "Maven Phase"; + } + + @Override + public String getStartPattern() { + return "^\\[INFO\\] --- (.+) ---$"; + } + + @Override + public String getEndPattern() { + return "^\\[INFO\\] --- .+ ---|^\\[INFO\\] -+$"; + } + } + + @Extension + public static class GradleTaskRule extends ConsoleSectionRule { + @Override + public String getId() { + return "gradle-task"; + } + + @Override + public String getDisplayName() { + return "Gradle Task"; + } + + @Override + public String getStartPattern() { + return "^> Task (:.+)$"; + } + + @Override + public String getEndPattern() { + return "^> Task :.+|^BUILD "; + } + } + + @Extension + public static class NpmScriptRule extends ConsoleSectionRule { + @Override + public String getId() { + return "npm-script"; + } + + @Override + public String getDisplayName() { + return "npm Script"; + } + + @Override + public String getStartPattern() { + return "^> (.+)$"; + } + + @Override + public String getEndPattern() { + return "^> .+|^npm warn|^npm error"; + } + + @Override + public boolean isEnabledByDefault() { + // npm output is less uniform; opt-in by default. + return false; + } + } + + @Extension + public static class DockerPullRule extends ConsoleSectionRule { + @Override + public String getId() { + return "docker-pull"; + } + + @Override + public String getDisplayName() { + return "Docker Pull"; + } + + @Override + public String getStartPattern() { + return "^(?:Pulling from|Pulling image) (.+)"; + } + + @Override + public String getEndPattern() { + return "^(?:Digest: |Status: )"; + } + } + + @Extension + public static class TerraformPlanRule extends ConsoleSectionRule { + @Override + public String getId() { + return "terraform-plan"; + } + + @Override + public String getDisplayName() { + return "Terraform Plan"; + } + + @Override + public String getStartPattern() { + return "^Terraform will perform the following actions:"; + } + + @Override + public String getEndPattern() { + return "^Plan: "; + } + } +} diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionRule.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionRule.java new file mode 100644 index 000000000..91a912c49 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionRule.java @@ -0,0 +1,66 @@ +package io.jenkins.plugins.pipelinegraphview.consoleview; + +import hudson.ExtensionList; +import hudson.ExtensionPoint; + +/** + * Extension point for contributing console section detection rules. + * + *

Plugin authors can implement this to define regex-based collapsible sections + * in Pipeline Graph View console output. Each rule specifies a start pattern (that + * opens a collapsible section) and an end pattern (that closes it). + * + *

Rules are discovered via {@link ExtensionList} and sent to the frontend + * as part of the console view configuration. + * + *

Example: + *

+ * {@literal @}Extension
+ * public class MavenPhaseRule extends ConsoleSectionRule {
+ *     {@literal @}Override public String getId() { return "maven-phase"; }
+ *     {@literal @}Override public String getDisplayName() { return "Maven Phase"; }
+ *     {@literal @}Override public String getStartPattern() { return "\\[INFO\\] --- .+ ---"; }
+ *     {@literal @}Override public String getEndPattern() { return "\\[INFO\\] --- .+ ---|\\[INFO\\] BUILD"; }
+ *     {@literal @}Override public boolean isEnabledByDefault() { return true; }
+ * }
+ * 
+ */ +public abstract class ConsoleSectionRule implements ExtensionPoint { + + /** + * Unique identifier for this rule. + * Used as a key for user preference toggles. + */ + public abstract String getId(); + + /** + * Human-readable name shown in the settings panel. + */ + public abstract String getDisplayName(); + + /** + * Java regex pattern that matches the start of a collapsible section. + * The first capture group, if present, is used as the section title. + */ + public abstract String getStartPattern(); + + /** + * Java regex pattern that matches the end of a collapsible section. + */ + public abstract String getEndPattern(); + + /** + * Whether this rule is enabled by default. + * Users can override per-job or globally. + */ + public boolean isEnabledByDefault() { + return true; + } + + /** + * All registered section rules. + */ + public static ExtensionList all() { + return ExtensionList.lookup(ConsoleSectionRule.class); + } +} diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction.java index 79e52ce4f..e0b502d35 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction.java @@ -386,6 +386,27 @@ public String getNextBuildNumber() { return getBuildNumber(run.getNextBuild()); } + @GET + @WebMethod(name = "consoleSectionRules") + public void getConsoleSectionRules(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException { + run.checkPermission(Item.READ); + rsp.setStatus(200); + rsp.setContentType("application/json;charset=UTF-8"); + rsp.setHeader("Cache-Control", "private, max-age=300"); + + List rules = new ArrayList<>(); + for (ConsoleSectionRule rule : ConsoleSectionRule.all()) { + JSONObject obj = new JSONObject(); + obj.put("id", rule.getId()); + obj.put("displayName", rule.getDisplayName()); + obj.put("startPattern", rule.getStartPattern()); + obj.put("endPattern", rule.getEndPattern()); + obj.put("enabledByDefault", rule.isEnabledByDefault()); + rules.add(obj); + } + net.sf.json.JSONArray.fromObject(rules).write(rsp.getWriter()); + } + @GET @WebMethod(name = "tree") public void getTree(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException, ServletException { diff --git a/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionRuleTest.java b/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionRuleTest.java new file mode 100644 index 000000000..f051d6367 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionRuleTest.java @@ -0,0 +1,104 @@ +package io.jenkins.plugins.pipelinegraphview.consoleview; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +import java.util.List; +import java.util.regex.Pattern; +import org.junit.jupiter.api.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; + +@WithJenkins +class ConsoleSectionRuleTest { + + @Test + void builtInRulesAreDiscovered(JenkinsRule j) { + List rules = + ConsoleSectionRule.all().stream().toList(); + assertThat(rules.size(), greaterThanOrEqualTo(5)); + + List ids = rules.stream().map(ConsoleSectionRule::getId).toList(); + assertThat(ids, hasItem("maven-phase")); + assertThat(ids, hasItem("gradle-task")); + assertThat(ids, hasItem("npm-script")); + assertThat(ids, hasItem("docker-pull")); + assertThat(ids, hasItem("terraform-plan")); + } + + @Test + void eachRuleHasValidPatterns(JenkinsRule j) { + for (ConsoleSectionRule rule : ConsoleSectionRule.all()) { + assertThat(rule.getId(), not(emptyOrNullString())); + assertThat(rule.getDisplayName(), not(emptyOrNullString())); + // Verify patterns compile without error. + Pattern.compile(rule.getStartPattern()); + Pattern.compile(rule.getEndPattern()); + } + } + + @Test + void mavenPhaseRuleMatchesExpectedOutput(JenkinsRule j) { + ConsoleSectionRule maven = ConsoleSectionRule.all().stream() + .filter(r -> "maven-phase".equals(r.getId())) + .findFirst() + .orElseThrow(); + Pattern start = Pattern.compile(maven.getStartPattern()); + Pattern end = Pattern.compile(maven.getEndPattern()); + + assertThat(start.matcher("[INFO] --- maven-compiler-plugin:3.8.1:compile (default) ---").matches(), is(true)); + assertThat(start.matcher("[INFO] Compiling 5 source files").matches(), is(false)); + assertThat(end.matcher("[INFO] --- maven-surefire-plugin:2.22:test ---").find(), is(true)); + assertThat(end.matcher("[INFO] -------------------------").find(), is(true)); + } + + @Test + void gradleTaskRuleMatchesExpectedOutput(JenkinsRule j) { + ConsoleSectionRule gradle = ConsoleSectionRule.all().stream() + .filter(r -> "gradle-task".equals(r.getId())) + .findFirst() + .orElseThrow(); + Pattern start = Pattern.compile(gradle.getStartPattern()); + + assertThat(start.matcher("> Task :compileJava").matches(), is(true)); + assertThat(start.matcher("> Task :test").matches(), is(true)); + assertThat(start.matcher("some other output").matches(), is(false)); + } + + @Test + void dockerPullRuleMatchesExpectedOutput(JenkinsRule j) { + ConsoleSectionRule docker = ConsoleSectionRule.all().stream() + .filter(r -> "docker-pull".equals(r.getId())) + .findFirst() + .orElseThrow(); + Pattern start = Pattern.compile(docker.getStartPattern()); + Pattern end = Pattern.compile(docker.getEndPattern()); + + assertThat(start.matcher("Pulling from library/node").find(), is(true)); + assertThat(start.matcher("Pulling image docker.io/library/maven:3.8").find(), is(true)); + assertThat(end.matcher("Digest: sha256:abc123").find(), is(true)); + assertThat(end.matcher("Status: Downloaded newer image").find(), is(true)); + } + + @Test + void npmScriptRuleIsDisabledByDefault(JenkinsRule j) { + ConsoleSectionRule npm = ConsoleSectionRule.all().stream() + .filter(r -> "npm-script".equals(r.getId())) + .findFirst() + .orElseThrow(); + assertThat(npm.isEnabledByDefault(), is(false)); + } + + @Test + void terraformPlanRuleMatchesExpectedOutput(JenkinsRule j) { + ConsoleSectionRule tf = ConsoleSectionRule.all().stream() + .filter(r -> "terraform-plan".equals(r.getId())) + .findFirst() + .orElseThrow(); + Pattern start = Pattern.compile(tf.getStartPattern()); + Pattern end = Pattern.compile(tf.getEndPattern()); + + assertThat(start.matcher("Terraform will perform the following actions:").find(), is(true)); + assertThat(end.matcher("Plan: 3 to add, 1 to change, 0 to destroy.").find(), is(true)); + } +} From bd7be21ce04d6398eb1954ff0a4014f65845de75 Mon Sep 17 00:00:00 2001 From: Aditya Jalkhare Date: Sat, 9 May 2026 03:06:41 +0530 Subject: [PATCH 04/21] Implement ConsoleSectionAnnotator and MarkerConsoleSectionAnnotator with detection logic for console output markers --- .../consoleview/ConsoleSectionAnnotator.java | 126 ++++++++++++++++++ .../MarkerConsoleSectionAnnotator.java | 55 ++++++++ .../ConsoleSectionAnnotatorTest.java | 99 ++++++++++++++ 3 files changed, 280 insertions(+) create mode 100644 src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotator.java create mode 100644 src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/MarkerConsoleSectionAnnotator.java create mode 100644 src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotatorTest.java diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotator.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotator.java new file mode 100644 index 000000000..bcb6db517 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotator.java @@ -0,0 +1,126 @@ +package io.jenkins.plugins.pipelinegraphview.consoleview; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import hudson.ExtensionList; +import hudson.ExtensionPoint; + +/** + * Extension point for stateful console section detection. + * + *

Unlike {@link ConsoleSectionRule} which uses simple regex pairs, + * this extension allows stateful, line-by-line analysis for richer + * section detection (multi-line pattern matching, context-dependent + * titles, dynamic enable/disable conditions). + * + *

Implementations receive lines one at a time via {@link #detect(String)} + * and return section boundary events. State is maintained per annotator + * instance; a fresh instance is created for each log stream. + * + *

Example: + *

+ * {@literal @}Extension
+ * public class StackTraceAnnotator extends ConsoleSectionAnnotator {
+ *     private boolean inTrace = false;
+ *
+ *     {@literal @}Override public String getId() { return "stack-trace"; }
+ *     {@literal @}Override public String getDisplayName() { return "Stack Trace"; }
+ *
+ *     {@literal @}Override
+ *     public SectionBoundary detect(String line) {
+ *         if (!inTrace && line.matches(".*Exception.*")) {
+ *             inTrace = true;
+ *             return SectionBoundary.start(line.trim());
+ *         }
+ *         if (inTrace && !line.startsWith("\tat ") && !line.startsWith("Caused by:")) {
+ *             inTrace = false;
+ *             return SectionBoundary.END;
+ *         }
+ *         return SectionBoundary.NONE;
+ *     }
+ * }
+ * 
+ */ +public abstract class ConsoleSectionAnnotator implements ExtensionPoint { + + /** + * Unique identifier for this annotator. + */ + public abstract String getId(); + + /** + * Human-readable name for the settings panel. + */ + public abstract String getDisplayName(); + + /** + * Whether this annotator is enabled by default. + */ + public boolean isEnabledByDefault() { + return true; + } + + /** + * Analyze a single line and return a section boundary event. + * + *

Called once per line, in order. The annotator may maintain + * internal state across calls within a single log stream. + * + * @param line the raw console output line (may contain ANSI escapes) + * @return a boundary event, or {@link SectionBoundary#NONE} if no transition + */ + @CheckForNull + public abstract SectionBoundary detect(String line); + + /** + * Reset internal state. Called before processing a new log stream. + */ + public void reset() { + // Default no-op; subclasses override as needed. + } + + /** + * All registered annotators. + */ + public static ExtensionList all() { + return ExtensionList.lookup(ConsoleSectionAnnotator.class); + } + + /** + * Represents a section boundary event returned by {@link #detect(String)}. + */ + public static final class SectionBoundary { + /** No section transition on this line. */ + public static final SectionBoundary NONE = new SectionBoundary(Type.NONE, null); + + /** Close the current section. */ + public static final SectionBoundary END = new SectionBoundary(Type.END, null); + + private final Type type; + private final String title; + + private SectionBoundary(Type type, String title) { + this.type = type; + this.title = title; + } + + /** Open a new section with the given title. */ + public static SectionBoundary start(String title) { + return new SectionBoundary(Type.START, title); + } + + public Type getType() { + return type; + } + + @CheckForNull + public String getTitle() { + return title; + } + + public enum Type { + NONE, + START, + END + } + } +} diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/MarkerConsoleSectionAnnotator.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/MarkerConsoleSectionAnnotator.java new file mode 100644 index 000000000..69debe1a0 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/MarkerConsoleSectionAnnotator.java @@ -0,0 +1,55 @@ +package io.jenkins.plugins.pipelinegraphview.consoleview; + +import hudson.Extension; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Built-in annotator that detects {@code ##[group]Title / ##[endgroup]} + * and {@code ::group::Title / ::endgroup::} markers in console output. + * + *

This is the server-side counterpart to the frontend marker parser + * in {@code parseConsoleSections.ts}. Both must agree on the syntax. + */ +@Extension +public class MarkerConsoleSectionAnnotator extends ConsoleSectionAnnotator { + + // Same ANSI stripping as the frontend. + private static final Pattern ANSI_RE = Pattern.compile("\033\\[[0-9;]*[a-zA-Z]"); + private static final Pattern GROUP_START = + Pattern.compile("^(?:##\\[group\\]|::group::)\\s*(.*)$"); + private static final Pattern GROUP_END = + Pattern.compile("^(?:##\\[endgroup\\]|::endgroup::)\\s*$"); + + @Override + public String getId() { + return "markers"; + } + + @Override + public String getDisplayName() { + return "Section Markers"; + } + + @Override + public SectionBoundary detect(String line) { + String stripped = ANSI_RE.matcher(line).replaceAll("").stripLeading(); + + // Reject shell trace lines. + if (stripped.startsWith("+ ")) { + return SectionBoundary.NONE; + } + + Matcher startMatch = GROUP_START.matcher(stripped); + if (startMatch.matches()) { + String title = startMatch.group(1); + return SectionBoundary.start(title.isEmpty() ? "Section" : title); + } + + if (GROUP_END.matcher(stripped).matches()) { + return SectionBoundary.END; + } + + return SectionBoundary.NONE; + } +} diff --git a/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotatorTest.java b/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotatorTest.java new file mode 100644 index 000000000..b0e35a48b --- /dev/null +++ b/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotatorTest.java @@ -0,0 +1,99 @@ +package io.jenkins.plugins.pipelinegraphview.consoleview; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +import java.util.List; +import org.junit.jupiter.api.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; + +@WithJenkins +class ConsoleSectionAnnotatorTest { + + @Test + void markerAnnotatorIsDiscovered(JenkinsRule j) { + List annotators = + ConsoleSectionAnnotator.all().stream().toList(); + List ids = + annotators.stream().map(ConsoleSectionAnnotator::getId).toList(); + assertThat(ids, hasItem("markers")); + } + + @Test + void markerAnnotatorDetectsAzureDevOpsGroup(JenkinsRule j) { + MarkerConsoleSectionAnnotator annotator = new MarkerConsoleSectionAnnotator(); + ConsoleSectionAnnotator.SectionBoundary result = annotator.detect("##[group]Build"); + assertThat(result.getType(), is(ConsoleSectionAnnotator.SectionBoundary.Type.START)); + assertThat(result.getTitle(), is("Build")); + } + + @Test + void markerAnnotatorDetectsGitHubActionsGroup(JenkinsRule j) { + MarkerConsoleSectionAnnotator annotator = new MarkerConsoleSectionAnnotator(); + ConsoleSectionAnnotator.SectionBoundary result = annotator.detect("::group::Test Suite"); + assertThat(result.getType(), is(ConsoleSectionAnnotator.SectionBoundary.Type.START)); + assertThat(result.getTitle(), is("Test Suite")); + } + + @Test + void markerAnnotatorDetectsEndgroup(JenkinsRule j) { + MarkerConsoleSectionAnnotator annotator = new MarkerConsoleSectionAnnotator(); + assertThat( + annotator.detect("##[endgroup]").getType(), + is(ConsoleSectionAnnotator.SectionBoundary.Type.END)); + assertThat( + annotator.detect("::endgroup::").getType(), + is(ConsoleSectionAnnotator.SectionBoundary.Type.END)); + } + + @Test + void markerAnnotatorRejectsShellTrace(JenkinsRule j) { + MarkerConsoleSectionAnnotator annotator = new MarkerConsoleSectionAnnotator(); + ConsoleSectionAnnotator.SectionBoundary result = annotator.detect("+ echo ##[group]Build"); + assertThat(result.getType(), is(ConsoleSectionAnnotator.SectionBoundary.Type.NONE)); + } + + @Test + void markerAnnotatorHandlesAnsiEscapes(JenkinsRule j) { + MarkerConsoleSectionAnnotator annotator = new MarkerConsoleSectionAnnotator(); + ConsoleSectionAnnotator.SectionBoundary result = annotator.detect("\033[32m##[group]Colored\033[0m"); + assertThat(result.getType(), is(ConsoleSectionAnnotator.SectionBoundary.Type.START)); + assertThat(result.getTitle(), is("Colored")); + } + + @Test + void markerAnnotatorUsesDefaultTitleWhenEmpty(JenkinsRule j) { + MarkerConsoleSectionAnnotator annotator = new MarkerConsoleSectionAnnotator(); + ConsoleSectionAnnotator.SectionBoundary result = annotator.detect("##[group]"); + assertThat(result.getType(), is(ConsoleSectionAnnotator.SectionBoundary.Type.START)); + assertThat(result.getTitle(), is("Section")); + } + + @Test + void markerAnnotatorReturnsNoneForPlainLine(JenkinsRule j) { + MarkerConsoleSectionAnnotator annotator = new MarkerConsoleSectionAnnotator(); + ConsoleSectionAnnotator.SectionBoundary result = annotator.detect("just a normal line"); + assertThat(result.getType(), is(ConsoleSectionAnnotator.SectionBoundary.Type.NONE)); + } + + @Test + void sectionBoundaryStartCarriesTitle(JenkinsRule j) { + ConsoleSectionAnnotator.SectionBoundary boundary = + ConsoleSectionAnnotator.SectionBoundary.start("My Title"); + assertThat(boundary.getType(), is(ConsoleSectionAnnotator.SectionBoundary.Type.START)); + assertThat(boundary.getTitle(), is("My Title")); + } + + @Test + void sectionBoundaryEndAndNoneHaveNoTitle(JenkinsRule j) { + assertThat(ConsoleSectionAnnotator.SectionBoundary.END.getTitle(), nullValue()); + assertThat(ConsoleSectionAnnotator.SectionBoundary.NONE.getTitle(), nullValue()); + } + + @Test + void markerAnnotatorIsEnabledByDefault(JenkinsRule j) { + MarkerConsoleSectionAnnotator annotator = new MarkerConsoleSectionAnnotator(); + assertThat(annotator.isEnabledByDefault(), is(true)); + } +} From 76c9dc8ba1d17bb9345d5e2eddc95729181255c5 Mon Sep 17 00:00:00 2001 From: Aditya Jalkhare Date: Sat, 9 May 2026 03:25:04 +0530 Subject: [PATCH 05/21] Refactor ConsoleSectionRuleTest to verify extension point discovery and remove built-in rule checks --- .../BuiltInConsoleSectionRules.java | 132 ------------------ .../consoleview/ConsoleSectionRuleTest.java | 112 ++++----------- 2 files changed, 30 insertions(+), 214 deletions(-) delete mode 100644 src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/BuiltInConsoleSectionRules.java diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/BuiltInConsoleSectionRules.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/BuiltInConsoleSectionRules.java deleted file mode 100644 index 56a2b9045..000000000 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/BuiltInConsoleSectionRules.java +++ /dev/null @@ -1,132 +0,0 @@ -package io.jenkins.plugins.pipelinegraphview.consoleview; - -import hudson.Extension; - -/** - * Built-in {@link ConsoleSectionRule} implementations for common CI tool output. - */ -public final class BuiltInConsoleSectionRules { - - private BuiltInConsoleSectionRules() {} - - @Extension - public static class MavenPhaseRule extends ConsoleSectionRule { - @Override - public String getId() { - return "maven-phase"; - } - - @Override - public String getDisplayName() { - return "Maven Phase"; - } - - @Override - public String getStartPattern() { - return "^\\[INFO\\] --- (.+) ---$"; - } - - @Override - public String getEndPattern() { - return "^\\[INFO\\] --- .+ ---|^\\[INFO\\] -+$"; - } - } - - @Extension - public static class GradleTaskRule extends ConsoleSectionRule { - @Override - public String getId() { - return "gradle-task"; - } - - @Override - public String getDisplayName() { - return "Gradle Task"; - } - - @Override - public String getStartPattern() { - return "^> Task (:.+)$"; - } - - @Override - public String getEndPattern() { - return "^> Task :.+|^BUILD "; - } - } - - @Extension - public static class NpmScriptRule extends ConsoleSectionRule { - @Override - public String getId() { - return "npm-script"; - } - - @Override - public String getDisplayName() { - return "npm Script"; - } - - @Override - public String getStartPattern() { - return "^> (.+)$"; - } - - @Override - public String getEndPattern() { - return "^> .+|^npm warn|^npm error"; - } - - @Override - public boolean isEnabledByDefault() { - // npm output is less uniform; opt-in by default. - return false; - } - } - - @Extension - public static class DockerPullRule extends ConsoleSectionRule { - @Override - public String getId() { - return "docker-pull"; - } - - @Override - public String getDisplayName() { - return "Docker Pull"; - } - - @Override - public String getStartPattern() { - return "^(?:Pulling from|Pulling image) (.+)"; - } - - @Override - public String getEndPattern() { - return "^(?:Digest: |Status: )"; - } - } - - @Extension - public static class TerraformPlanRule extends ConsoleSectionRule { - @Override - public String getId() { - return "terraform-plan"; - } - - @Override - public String getDisplayName() { - return "Terraform Plan"; - } - - @Override - public String getStartPattern() { - return "^Terraform will perform the following actions:"; - } - - @Override - public String getEndPattern() { - return "^Plan: "; - } - } -} diff --git a/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionRuleTest.java b/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionRuleTest.java index f051d6367..dc118ffeb 100644 --- a/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionRuleTest.java +++ b/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionRuleTest.java @@ -13,92 +13,40 @@ class ConsoleSectionRuleTest { @Test - void builtInRulesAreDiscovered(JenkinsRule j) { + void extensionPointIsDiscoverable(JenkinsRule j) { + // No built-in rules shipped; the list should be empty by default. + // Plugin maintainers contribute their own rules via @Extension. List rules = ConsoleSectionRule.all().stream().toList(); - assertThat(rules.size(), greaterThanOrEqualTo(5)); - - List ids = rules.stream().map(ConsoleSectionRule::getId).toList(); - assertThat(ids, hasItem("maven-phase")); - assertThat(ids, hasItem("gradle-task")); - assertThat(ids, hasItem("npm-script")); - assertThat(ids, hasItem("docker-pull")); - assertThat(ids, hasItem("terraform-plan")); - } - - @Test - void eachRuleHasValidPatterns(JenkinsRule j) { - for (ConsoleSectionRule rule : ConsoleSectionRule.all()) { - assertThat(rule.getId(), not(emptyOrNullString())); - assertThat(rule.getDisplayName(), not(emptyOrNullString())); - // Verify patterns compile without error. - Pattern.compile(rule.getStartPattern()); - Pattern.compile(rule.getEndPattern()); - } - } - - @Test - void mavenPhaseRuleMatchesExpectedOutput(JenkinsRule j) { - ConsoleSectionRule maven = ConsoleSectionRule.all().stream() - .filter(r -> "maven-phase".equals(r.getId())) - .findFirst() - .orElseThrow(); - Pattern start = Pattern.compile(maven.getStartPattern()); - Pattern end = Pattern.compile(maven.getEndPattern()); - - assertThat(start.matcher("[INFO] --- maven-compiler-plugin:3.8.1:compile (default) ---").matches(), is(true)); - assertThat(start.matcher("[INFO] Compiling 5 source files").matches(), is(false)); - assertThat(end.matcher("[INFO] --- maven-surefire-plugin:2.22:test ---").find(), is(true)); - assertThat(end.matcher("[INFO] -------------------------").find(), is(true)); - } - - @Test - void gradleTaskRuleMatchesExpectedOutput(JenkinsRule j) { - ConsoleSectionRule gradle = ConsoleSectionRule.all().stream() - .filter(r -> "gradle-task".equals(r.getId())) - .findFirst() - .orElseThrow(); - Pattern start = Pattern.compile(gradle.getStartPattern()); - - assertThat(start.matcher("> Task :compileJava").matches(), is(true)); - assertThat(start.matcher("> Task :test").matches(), is(true)); - assertThat(start.matcher("some other output").matches(), is(false)); - } - - @Test - void dockerPullRuleMatchesExpectedOutput(JenkinsRule j) { - ConsoleSectionRule docker = ConsoleSectionRule.all().stream() - .filter(r -> "docker-pull".equals(r.getId())) - .findFirst() - .orElseThrow(); - Pattern start = Pattern.compile(docker.getStartPattern()); - Pattern end = Pattern.compile(docker.getEndPattern()); - - assertThat(start.matcher("Pulling from library/node").find(), is(true)); - assertThat(start.matcher("Pulling image docker.io/library/maven:3.8").find(), is(true)); - assertThat(end.matcher("Digest: sha256:abc123").find(), is(true)); - assertThat(end.matcher("Status: Downloaded newer image").find(), is(true)); + assertThat(rules, is(empty())); } @Test - void npmScriptRuleIsDisabledByDefault(JenkinsRule j) { - ConsoleSectionRule npm = ConsoleSectionRule.all().stream() - .filter(r -> "npm-script".equals(r.getId())) - .findFirst() - .orElseThrow(); - assertThat(npm.isEnabledByDefault(), is(false)); - } - - @Test - void terraformPlanRuleMatchesExpectedOutput(JenkinsRule j) { - ConsoleSectionRule tf = ConsoleSectionRule.all().stream() - .filter(r -> "terraform-plan".equals(r.getId())) - .findFirst() - .orElseThrow(); - Pattern start = Pattern.compile(tf.getStartPattern()); - Pattern end = Pattern.compile(tf.getEndPattern()); - - assertThat(start.matcher("Terraform will perform the following actions:").find(), is(true)); - assertThat(end.matcher("Plan: 3 to add, 1 to change, 0 to destroy.").find(), is(true)); + void defaultEnabledByDefaultReturnsTrue(JenkinsRule j) { + ConsoleSectionRule stub = new ConsoleSectionRule() { + @Override + public String getId() { + return "test"; + } + + @Override + public String getDisplayName() { + return "Test Rule"; + } + + @Override + public String getStartPattern() { + return "^START$"; + } + + @Override + public String getEndPattern() { + return "^END$"; + } + }; + assertThat(stub.isEnabledByDefault(), is(true)); + // Patterns compile without error. + Pattern.compile(stub.getStartPattern()); + Pattern.compile(stub.getEndPattern()); } } From efae1f93adcf1107973cb90454b65e0e94f1ca13 Mon Sep 17 00:00:00 2001 From: Aditya Jalkhare Date: Sat, 9 May 2026 04:03:11 +0530 Subject: [PATCH 06/21] Enhance ConsoleSection behavior and styling for collapsible groups - Update default rendering behavior for small closed groups to be open. - Add logic to collapse groups with more than 25 children. - Refactor ConsoleSection component to handle open state based on child count. - Improve styling for chevron indicator in console sections. - Update tests to reflect changes in rendering logic and title trimming. --- .../main/ConsoleSection.spec.tsx | 30 +++++++++-- .../pipeline-console/main/ConsoleSection.tsx | 35 +++++++++++-- .../main/console-section.scss | 26 +++++----- .../main/parseConsoleSections.spec.ts | 52 +++++++++++++++++-- .../main/parseConsoleSections.ts | 18 +++++-- 5 files changed, 135 insertions(+), 26 deletions(-) diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.spec.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.spec.tsx index 7c193e42b..5113acf52 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.spec.tsx +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.spec.tsx @@ -65,7 +65,7 @@ describe("ConsoleSection", () => { currentRunPath: "/jenkins/job/test/1/", }; - it("renders closed by default for a closed group", () => { + it("renders open by default for a small closed group", () => { const group: ConsoleSectionGroup = { kind: "group", title: "Install", @@ -78,6 +78,27 @@ describe("ConsoleSection", () => { ); const details = container.querySelector("details"); expect(details).toBeTruthy(); + expect(details!.open).toBe(true); + }); + + it("renders closed by default when children exceed 25", () => { + const children = Array.from({ length: 30 }, (_, i) => ({ + kind: "line" as const, + index: i + 1, + content: `line ${i + 1}`, + })); + const group: ConsoleSectionGroup = { + kind: "group", + title: "Big Section", + startIndex: 0, + endIndex: 31, + children, + }; + const { container } = render( + , + ); + const details = container.querySelector("details"); + expect(details).toBeTruthy(); expect(details!.open).toBe(false); }); @@ -139,12 +160,13 @@ describe("ConsoleSection", () => { ); const summary = container.querySelector("summary")!; const details = container.querySelector("details")!; - expect(details.open).toBe(false); - - fireEvent.click(summary); + // Small group defaults to open expect(details.open).toBe(true); fireEvent.click(summary); expect(details.open).toBe(false); + + fireEvent.click(summary); + expect(details.open).toBe(true); }); }); diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.tsx index 4c933376d..002182529 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.tsx +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.tsx @@ -4,8 +4,12 @@ import { ConsoleSectionGroup, ConsoleSectionNode, } from "./parseConsoleSections.ts"; +import { makeReactChildren, tokenizeANSIString } from "./Ansi.tsx"; import { ConsoleLine } from "./ConsoleLine.tsx"; +/** Sections with more children than this default to collapsed. */ +const COLLAPSE_THRESHOLD = 25; + export interface ConsoleSectionProps { group: ConsoleSectionGroup; stepId: string; @@ -22,16 +26,41 @@ export const ConsoleSection = memo(function ConsoleSection({ currentRunPath, }: ConsoleSectionProps) { // Unclosed groups (still streaming) default to open. - const [open, setOpen] = useState(group.endIndex === -1); + // Closed groups with many children default to collapsed. + const [open, setOpen] = useState( + group.endIndex === -1 || group.children.length <= COLLAPSE_THRESHOLD, + ); return (

setOpen((e.target as HTMLDetailsElement).open)} + onToggle={(e) => { + e.stopPropagation(); + setOpen((e.target as HTMLDetailsElement).open); + }} className="pgv-console-section" > - {group.title} + + + + + {makeReactChildren( + tokenizeANSIString(group.title), + `section-title-${group.startIndex}`, + )} + {group.children.length}{" "} {group.children.length === 1 ? "line" : "lines"} diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/console-section.scss b/src/main/frontend/pipeline-console-view/pipeline-console/main/console-section.scss index d930b7271..d8f5d96b5 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/console-section.scss +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/console-section.scss @@ -25,14 +25,6 @@ display: none; } - &::before { - content: "▶"; - font-size: 0.6em; - transition: transform 0.15s ease; - display: inline-block; - color: var(--text-color-secondary); - } - &:hover { background-color: color-mix( in srgb, @@ -40,11 +32,21 @@ transparent ); } +} + +.pgv-console-section__chevron { + width: 0.75rem; + height: 0.75rem; + flex-shrink: 0; + color: var(--text-color-secondary); + transition: var(--standard-transition); + + * { + stroke-width: 48px; + } - details[open] > & { - &::before { - transform: rotate(90deg); - } + details[open] > summary > & { + rotate: 90deg; } } diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts index c12066b14..9ac340a71 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts @@ -90,11 +90,11 @@ describe("parseConsoleSections", () => { expect(group.title).toBe("Section"); }); - it("preserves trailing whitespace in title", () => { + it("trims trailing whitespace in title", () => { const lines = ["##[group]Install Dependencies ", "npm ci", "##[endgroup]"]; const result = parseConsoleSections(lines); const group = result[0] as ConsoleSectionGroup; - expect(group.title).toBe("Install Dependencies "); + expect(group.title).toBe("Install Dependencies"); }); }); @@ -146,7 +146,7 @@ describe("parseConsoleSections", () => { }); describe("ANSI stripping for detection", () => { - it("detects marker wrapped in ANSI codes", () => { + it("detects marker wrapped in ANSI codes and preserves ANSI in title", () => { const lines = [ "\x1b[32m##[group]Colored\x1b[0m", "inside", @@ -156,7 +156,19 @@ describe("parseConsoleSections", () => { expect(result).toHaveLength(1); const group = result[0] as ConsoleSectionGroup; expect(group.kind).toBe("group"); - expect(group.title).toBe("Colored"); + // Marker stripped, surrounding ANSI preserved for color rendering. + expect(group.title).toBe("\x1b[32mColored\x1b[0m"); + }); + + it("preserves ANSI codes placed after the marker", () => { + const lines = [ + "##[group]\x1b[1;34mBold Blue Title\x1b[0m", + "inside", + "##[endgroup]", + ]; + const result = parseConsoleSections(lines); + const group = result[0] as ConsoleSectionGroup; + expect(group.title).toBe("\x1b[1;34mBold Blue Title\x1b[0m"); }); it("detects marker with leading whitespace", () => { @@ -171,6 +183,38 @@ describe("parseConsoleSections", () => { }); }); + describe("HTML tag stripping for detection (AnsiColor plugin)", () => { + it("detects marker inside HTML span tags", () => { + const lines = [ + '##[group]Green Title', + "inside", + '##[endgroup]', + ]; + const result = parseConsoleSections(lines); + expect(result).toHaveLength(1); + const group = result[0] as ConsoleSectionGroup; + expect(group.kind).toBe("group"); + // HTML preserved so dangerouslySetInnerHTML can render the color. + expect(group.title).toBe( + 'Green Title', + ); + }); + + it("detects ::group:: inside HTML spans", () => { + const lines = [ + '::group::Blue Section', + "inside", + '::endgroup::', + ]; + const result = parseConsoleSections(lines); + expect(result).toHaveLength(1); + const group = result[0] as ConsoleSectionGroup; + expect(group.title).toBe( + 'Blue Section', + ); + }); + }); + describe("shell trace rejection", () => { it("does not treat '+ echo ##[group]...' as a marker", () => { const lines = [ diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts index a4b6817ad..9c74c242d 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts @@ -33,6 +33,10 @@ export type ConsoleSectionNode = ConsoleSectionLine | ConsoleSectionGroup; // ANSI escape sequence pattern for stripping before marker detection. const ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]/g; +// HTML tag pattern for stripping before marker detection. +// progressiveHtml returns HTML; AnsiColor plugin wraps ANSI codes in tags. +const HTML_TAG_RE = /<[^>]*>/g; + // Start markers: ##[group]Title or ::group::Title const GROUP_START_RE = /^(?:##\[group\]|::group::)\s*(.*)$/; @@ -40,14 +44,19 @@ const GROUP_START_RE = /^(?:##\[group\]|::group::)\s*(.*)$/; const GROUP_END_RE = /^(?:##\[endgroup\]|::endgroup::)\s*$/; /** - * Strip ANSI escape codes and leading whitespace from a line + * Strip ANSI escape codes, HTML tags, and leading whitespace from a line * for marker detection purposes. The original content is preserved * in the output nodes. */ function stripForDetection(line: string): string { - return line.replace(ANSI_RE, "").trimStart(); + return line.replace(ANSI_RE, "").replace(HTML_TAG_RE, "").trimStart(); } +// Pattern matching the group-start marker itself (no capture). +// Used to strip the marker from the raw line while preserving surrounding +// HTML tags and ANSI codes for colored title rendering. +const GROUP_MARKER_RE = /##\[group\]|::group::/; + /** * Parse raw console lines into a tree of sections. * @@ -80,9 +89,12 @@ export function parseConsoleSections( const startMatch = GROUP_START_RE.exec(stripped); if (startMatch) { + // Strip the marker from the raw line, preserving surrounding HTML/ANSI + // so the title renderer can show colors from the AnsiColor plugin. + const title = raw.replace(GROUP_MARKER_RE, "").trim() || "Section"; const group: ConsoleSectionGroup = { kind: "group", - title: startMatch[1] || "Section", + title, startIndex: i, endIndex: -1, children: [], From e6faaa400f7594b29c2ef7818d6dcc166062e2be Mon Sep 17 00:00:00 2001 From: Aditya Jalkhare Date: Sat, 9 May 2026 04:03:21 +0530 Subject: [PATCH 07/21] Add example Jenkinsfile for collapsible sections with marker detection --- .../collapsible-sections-test.jenkinsfile | 319 ++++++++++++++++++ 1 file changed, 319 insertions(+) create mode 100644 docs/examples/collapsible-sections-test.jenkinsfile diff --git a/docs/examples/collapsible-sections-test.jenkinsfile b/docs/examples/collapsible-sections-test.jenkinsfile new file mode 100644 index 000000000..58a68ee61 --- /dev/null +++ b/docs/examples/collapsible-sections-test.jenkinsfile @@ -0,0 +1,319 @@ +pipeline { + agent any + stages { + // ── Phase 1: Structural auto-grouping ────────────────────── + // Line count display in step header. Many-line steps show count. + // Short steps show fewer lines. Step card expand/collapse is + // controlled by clicking the step header. + + stage('[P1] Build - 60 lines') { + steps { + echo 'Building...' + sh '''#!/bin/bash + for i in $(seq 1 60); do + echo "Build output line $i: compiling module-$i..." + done + ''' + } + } + + stage('[P1] Lint - 1 line') { + steps { echo 'Linting... all clean.' } + } + + // ── Phase 2: Marker detection ────────────────────────────── + // Tests ##[group]/##[endgroup] and ::group::/::endgroup:: syntax. + // Sections inside a step collapse/expand independently. + + stage('[P2] Unit Tests - markers') { + steps { + echo 'Running unit tests...' + sh '''#!/bin/bash + echo "##[group]Test Suite: core" + for i in $(seq 1 20); do + echo " TEST $i: test_core_$i ... PASSED" + done + echo "##[endgroup]" + + echo "##[group]Test Suite: api" + for i in $(seq 1 20); do + echo " TEST $i: test_api_$i ... PASSED" + done + echo "##[endgroup]" + + echo "40 tests passed, 0 failed" + ''' + } + } + + stage('[P2] npm install - ##[group]') { + steps { + echo 'Starting dependency installation...' + sh '''#!/bin/bash + echo "##[group]npm install" + echo "added 1245 packages in 32s" + echo "npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported" + echo "npm warn deprecated inflight@1.0.6: This module is not supported" + echo "127 packages are looking for funding" + echo "##[endgroup]" + ''' + } + } + + stage('[P2] pip install - ::group::') { + steps { + sh '''#!/bin/bash + echo "::group::pip install" + echo "Collecting flask==2.3.2" + echo " Downloading Flask-2.3.2-py3-none-any.whl (96 kB)" + echo "Collecting requests>=2.28" + echo " Downloading requests-2.31.0-py3-none-any.whl (62 kB)" + echo "Installing collected packages: flask, requests" + echo "Successfully installed flask-2.3.2 requests-2.31.0" + echo "::endgroup::" + ''' + } + } + + stage('[P2] Nested Sections') { + steps { + sh '''#!/bin/bash + echo "##[group]Compile All Modules" + echo "Starting compilation..." + echo "##[group]Module: core" + echo "Compiling core/src/main/java..." + echo "42 source files compiled" + echo "##[endgroup]" + echo "##[group]Module: api" + echo "Compiling api/src/main/java..." + echo "18 source files compiled" + echo "##[endgroup]" + echo "##[group]Module: web" + echo "Compiling web/src/main/java..." + echo "7 source files compiled" + echo "##[endgroup]" + echo "All modules compiled successfully." + echo "##[endgroup]" + ''' + } + } + + stage('[P2] Edge - shell trace') { + steps { + sh ''' + set -x + echo "##[group]This should be a real marker" + echo "The set -x trace above (+ echo ...) must be ignored" + echo "##[endgroup]" + set +x + ''' + } + } + + stage('[P2] Edge - ANSI colors') { + steps { + sh '''#!/bin/bash + echo -e "\033[32m##[group]Green Section\033[0m" + echo "This section marker has green ANSI wrapping" + echo -e "\033[32m##[endgroup]\033[0m" + + echo -e "##[group]\033[1;34mBold Blue Title\033[0m" + echo "Title has bold blue ANSI after the marker" + echo "##[endgroup]" + + echo -e "##[group]\033[33mYellow\033[0m \033[1;31mRed\033[0m" + echo "Mixed ANSI colors within a single title" + echo "##[endgroup]" + ''' + } + } + + stage('[P2] Edge - mixed syntax') { + steps { + sh '''#!/bin/bash + echo "##[group]Mixed Open" + echo "Opened with ## syntax, closed with :: syntax" + echo "::endgroup::" + ''' + } + } + + stage('[P2] Edge - stray endgroup') { + steps { + sh '''#!/bin/bash + echo "Some normal output" + echo "##[endgroup]" + echo "More normal output" + ''' + } + } + + stage('[P2] Edge - unclosed group') { + steps { + sh '''#!/bin/bash + echo "##[group]Unclosed Section" + echo "This group never closes in this step" + echo "It should render open by default" + ''' + } + } + + // ── Phase 3: ConsoleSectionRule extension point ────────── + // No built-in rules shipped. Plugin maintainers contribute + // their own rules via @Extension on ConsoleSectionRule. + // The extension point + API endpoint + frontend wiring are + // all in place; stages here are placeholders until a plugin + // registers a rule. + + // ── Phase 4: Combined / interaction testing ──────────────── + // Markers wrapping different kinds of output. + + stage('[P4] Markers + Maven output') { + steps { + sh '''#!/bin/bash + echo "##[group]Full Build Pipeline" + echo "[INFO] --- maven-compiler-plugin:3.8.1:compile ---" + echo "[INFO] Compiling sources..." + echo "[INFO] --- maven-surefire-plugin:2.22.2:test ---" + echo "[INFO] Running tests..." + echo "[INFO] -------------------------------------------------------" + echo "##[endgroup]" + ''' + } + } + + stage('[P4] Markers + Docker') { + steps { + sh '''#!/bin/bash + echo "::group::Docker Image Build" + echo "Step 1/8 : FROM node:18-alpine" + echo "Step 2/8 : WORKDIR /app" + echo "Step 3/8 : COPY package*.json ./" + echo "Step 4/8 : RUN npm ci --production" + echo "Step 5/8 : COPY . ." + echo "Step 6/8 : RUN npm run build" + echo "Step 7/8 : EXPOSE 3000" + echo "Step 8/8 : CMD [\\"node\\", \\"server.js\\"]" + echo "Successfully built abc123def456" + echo "::endgroup::" + ''' + } + } + + stage('[P4] Sequential marker groups') { + steps { + sh '''#!/bin/bash + echo "##[group]API Tests" + for i in $(seq 1 15); do + echo " PASS: test_api_endpoint_$i ($(( RANDOM % 200 + 50 ))ms)" + done + echo "##[endgroup]" + + echo "##[group]E2E Tests" + for i in $(seq 1 10); do + echo " PASS: test_e2e_flow_$i ($(( RANDOM % 3000 + 1000 ))ms)" + done + echo "##[endgroup]" + + echo "##[group]Load Test Results" + echo "Requests/sec: 2,450" + echo "Avg latency: 12ms" + echo "P95 latency: 45ms" + echo "P99 latency: 120ms" + echo "Error rate: 0.01%" + echo "##[endgroup]" + ''' + } + } + + stage('[P4] Deploy with markers') { + steps { + sh '''#!/bin/bash + echo "::group::Deploying to staging" + echo "Connecting to staging-cluster..." + echo "Applying kubernetes manifests..." + echo "deployment.apps/web-app configured" + echo "service/web-app unchanged" + echo "ingress.networking.k8s.io/web-app configured" + echo "Waiting for rollout..." + echo "deployment \\"web-app\\" successfully rolled out" + echo "::endgroup::" + ''' + } + } + + stage('[P4] Package with markers') { + steps { + sh '''#!/bin/bash + echo "##[group]Create Distribution Archive" + for i in $(seq 1 20); do + echo "Adding file: dist/module-$i.js ($(( RANDOM % 500 + 10 ))KB)" + done + echo "Archive size: 4.2MB" + echo "##[endgroup]" + ''' + } + } + + // ── Phase 1: Parallel stages (structural grouping) ──────── + stage('[P1] Check All Services') { + parallel { + stage('Check service-1') { steps { echo 'Checking service-1' } } + stage('Check service-2') { steps { echo 'Checking service-2' } } + stage('Check service-3') { steps { echo 'Checking service-3' } } + stage('Check service-4') { steps { echo 'Checking service-4' } } + stage('Check service-5') { steps { echo 'Checking service-5' } } + stage('Check service-6') { steps { echo 'Checking service-6' } } + stage('Check service-7') { steps { echo 'Checking service-7' } } + stage('Check service-8') { steps { echo 'Checking service-8' } } + stage('Check service-9') { steps { echo 'Checking service-9' } } + stage('Check service-10') { steps { echo 'Checking service-10' } } + stage('Check service-11') { steps { echo 'Checking service-11' } } + stage('Check service-12') { steps { echo 'Checking service-12' } } + stage('Check service-13') { steps { echo 'Checking service-13' } } + stage('Check service-14') { steps { echo 'Checking service-14' } } + stage('Check service-15') { steps { echo 'Checking service-15' } } + stage('Check service-16') { steps { echo 'Checking service-16' } } + stage('Check service-17') { steps { echo 'Checking service-17' } } + stage('Check service-18') { steps { echo 'Checking service-18' } } + stage('Check service-19') { steps { echo 'Checking service-19' } } + stage('Check service-20') { steps { echo 'Checking service-20' } } + stage('Check service-21') { steps { echo 'Checking service-21' } } + stage('Check service-22') { steps { echo 'Checking service-22' } } + stage('Check service-23') { steps { echo 'Checking service-23' } } + stage('Check service-24') { steps { echo 'Checking service-24' } } + stage('Check service-25') { steps { echo 'Checking service-25' } } + stage('Check service-26') { steps { echo 'Checking service-26' } } + stage('Check service-27') { steps { echo 'Checking service-27' } } + stage('Check service-28') { steps { echo 'Checking service-28' } } + stage('Check service-29') { steps { echo 'Checking service-29' } } + stage('Check service-30') { steps { echo 'Checking service-30' } } + stage('Check service-31') { steps { echo 'Checking service-31' } } + stage('Check service-32') { steps { echo 'Checking service-32' } } + stage('Check service-33') { steps { echo 'Checking service-33' } } + stage('Check service-34') { steps { echo 'Checking service-34' } } + stage('Check service-35') { steps { echo 'Checking service-35' } } + stage('Check service-36') { steps { echo 'Checking service-36' } } + stage('Check service-37') { steps { echo 'Checking service-37' } } + stage('Check service-38') { steps { echo 'Checking service-38' } } + stage('Check service-39') { steps { echo 'Checking service-39' } } + stage('Check service-40') { steps { echo 'Checking service-40' } } + stage('Check service-41') { steps { echo 'Checking service-41' } } + stage('Check service-42') { steps { echo 'Checking service-42' } } + stage('Check service-43') { steps { echo 'Checking service-43' } } + stage('Check service-44') { steps { echo 'Checking service-44' } } + stage('Check service-45') { steps { echo 'Checking service-45' } } + stage('Check service-46') { steps { echo 'Checking service-46' } } + stage('Check service-47') { steps { echo 'Checking service-47' } } + stage('Check service-48') { steps { echo 'Checking service-48' } } + stage('Check service-49') { steps { echo 'Checking service-49' } } + stage('Check service-50') { steps { echo 'Checking service-50' } } + stage('Check service-51') { steps { echo 'Checking service-51' } } + stage('Check service-52') { steps { echo 'Checking service-52' } } + stage('Check service-53') { steps { echo 'Checking service-53' } } + stage('Check service-54') { steps { echo 'Checking service-54' } } + stage('Check service-55') { steps { echo 'Checking service-55' } } + } + } + } +} From 7a26c455160fcdb0d4a576b454e82dc683f72ef4 Mon Sep 17 00:00:00 2001 From: Aditya Jalkhare Date: Sat, 9 May 2026 04:17:45 +0530 Subject: [PATCH 08/21] Add documentation for collapsible console sections and extension points --- README.md | 23 ++++++ docs/collapsible-console-sections.md | 65 +++++++++++++++ docs/extending-console-sections.md | 114 +++++++++++++++++++++++++++ 3 files changed, 202 insertions(+) create mode 100644 docs/collapsible-console-sections.md create mode 100644 docs/extending-console-sections.md diff --git a/README.md b/README.md index 07890d5b0..198051c44 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,20 @@ See a live demonstration from a Jenkins Contributor Summit: ## Pipeline DSL Extensions +### Collapsible Console Sections + +Console output can be split into collapsible sections using `##[group]` / `##[endgroup]` markers (Azure DevOps style) or `::group::` / `::endgroup::` (GitHub Actions style): + +```groovy +sh ''' + echo "##[group]Unit Tests" + ./run-tests.sh + echo "##[endgroup]" +''' +``` + +Sections with more than 25 lines start collapsed. Nested sections are supported. See [collapsible-console-sections.md](docs/collapsible-console-sections.md) for full details including colored titles with the AnsiColor plugin. + ### Hiding Steps from View You can mark specific pipeline steps as hidden from the view by wrapping them with the `hideFromView` step: @@ -74,6 +88,15 @@ pipeline { The REST API documentation can be found [here](https://editor-next.swagger.io/?url=https://raw.githubusercontent.com/jenkinsci/pipeline-graph-view-plugin/refs/heads/main/openapi.yaml). +## Extending the Plugin + +Plugin authors can contribute custom collapsible section rules via two extension points: + +- `ConsoleSectionRule` - regex-based, runs client-side. Good for simple delimited output (npm install, pip, Docker builds, etc.). +- `ConsoleSectionAnnotator` - stateful, runs server-side line by line. Good for context-dependent grouping like stack traces or test suite blocks. + +See [extending-console-sections.md](docs/extending-console-sections.md) for examples of both. + ## Contributing Refer to our [contribution guidelines](./CONTRIBUTING.md). diff --git a/docs/collapsible-console-sections.md b/docs/collapsible-console-sections.md new file mode 100644 index 000000000..4dcf85fc0 --- /dev/null +++ b/docs/collapsible-console-sections.md @@ -0,0 +1,65 @@ +# Collapsible Console Sections + +Steps in the Pipeline Graph View console can collapse sections of their output into expandable groups. This makes long logs easier to scan. + +## Using markers in your Jenkinsfile + +Add `##[group]` and `##[endgroup]` markers to your shell commands. The text after `##[group]` becomes the section title. + +```groovy +stage('Test') { + steps { + sh ''' + echo "##[group]Unit Tests" + ./run-tests.sh + echo "##[endgroup]" + + echo "##[group]Integration Tests" + ./run-integration.sh + echo "##[endgroup]" + ''' + } +} +``` + +Both Azure DevOps (`##[group]`) and GitHub Actions (`::group::`) syntax are supported and can be mixed freely. + +```groovy +sh ''' + echo "::group::Dependency install" + npm ci + echo "::endgroup::" +''' +``` + +## Behavior + +- Sections with more than 25 lines start collapsed. +- Sections with 25 or fewer lines start open. +- Sections still being written (not yet closed) stay open. +- Sections can be nested. +- Lines from `set -x` shell tracing (starting with `+ `) are never treated as markers. + +## Colored titles (AnsiColor plugin) + +If you install the [AnsiColor plugin](https://plugins.jenkins.io/ansicolor/), wrap your step in `ansiColor('xterm')`. ANSI codes in section titles are rendered as colors. + +```groovy +stage('Build') { + steps { + ansiColor('xterm') { + sh '''#!/bin/bash + echo -e "\033[32m##[group]Dependencies\033[0m" + npm ci + echo "##[endgroup]" + + echo -e "##[group]\033[1;34mCompile\033[0m" + npm run build + echo "##[endgroup]" + ''' + } + } +} +``` + +Note: use `echo -e` in bash (not the default `sh`) so escape sequences are interpreted. Add `#!/bin/bash` at the top of your `sh` block. diff --git a/docs/extending-console-sections.md b/docs/extending-console-sections.md new file mode 100644 index 000000000..f6b34ec0f --- /dev/null +++ b/docs/extending-console-sections.md @@ -0,0 +1,114 @@ +# Extending Console Sections + +Plugin authors can contribute custom collapsible section rules to Pipeline Graph View. There are two extension points depending on how much control you need. + +## Dependency + +Add Pipeline Graph View as a dependency in your plugin's `pom.xml`: + +```xml + + io.jenkins.plugins + pipeline-graph-view + + +``` + +## Option 1: ConsoleSectionRule (regex-based) + +For simple cases where a start line and end line can each be matched with a single regex. Rules are sent to the frontend and applied client-side as console output streams in. + +Extend `ConsoleSectionRule` and annotate with `@Extension`: + +```java +import hudson.Extension; +import io.jenkins.plugins.pipelinegraphview.consoleview.ConsoleSectionRule; + +@Extension +public class MavenPhaseRule extends ConsoleSectionRule { + + @Override + public String getId() { + return "maven-phase"; + } + + @Override + public String getDisplayName() { + return "Maven Phase"; + } + + @Override + public String getStartPattern() { + // First capture group, if present, becomes the section title. + return "\\[INFO\\] --- (.+) ---"; + } + + @Override + public String getEndPattern() { + return "\\[INFO\\] --- .+ ---|\\[INFO\\] BUILD"; + } + + @Override + public boolean isEnabledByDefault() { + return true; + } +} +``` + +## Option 2: ConsoleSectionAnnotator (stateful, server-side) + +For cases where detection requires state across lines - for example, grouping a stack trace that starts with an exception line and ends when indentation stops. Annotators run server-side, line by line, and can maintain state per log stream. + +Extend `ConsoleSectionAnnotator` and annotate with `@Extension`: + +```java +import hudson.Extension; +import io.jenkins.plugins.pipelinegraphview.consoleview.ConsoleSectionAnnotator; + +@Extension +public class StackTraceAnnotator extends ConsoleSectionAnnotator { + + private boolean inTrace = false; + + @Override + public String getId() { + return "stack-trace"; + } + + @Override + public String getDisplayName() { + return "Stack Trace"; + } + + @Override + public SectionBoundary detect(String line) { + if (!inTrace && line.matches(".*Exception.*")) { + inTrace = true; + return SectionBoundary.start(line.trim()); + } + if (inTrace && !line.startsWith("\tat ") && !line.startsWith("Caused by:")) { + inTrace = false; + return SectionBoundary.END; + } + return SectionBoundary.NONE; + } + + @Override + public void reset() { + inTrace = false; + } +} +``` + +`detect(String line)` is called once per line in order. Return `SectionBoundary.start("Title")` to open a section, `SectionBoundary.END` to close it, or `SectionBoundary.NONE` to do nothing. `reset()` is called before each new log stream. + +## Which to use + +| | `ConsoleSectionRule` | `ConsoleSectionAnnotator` | +|---|---|---| +| Detection | Regex pair (start / end) | Line-by-line with state | +| Runs | Client-side (frontend) | Server-side | +| Title | First capture group or full line | Set dynamically in `detect()` | +| Use for | Simple delimited output (npm, pip, etc.) | Context-dependent grouping (stack traces, test suites) | + +`##[group]` / `::group::` markers in console output are always detected regardless of any registered extensions. From d0e46391ba0e4b36be59c0ed885aafb3418623eb Mon Sep 17 00:00:00 2001 From: Aditya Jalkhare Date: Sat, 9 May 2026 04:22:32 +0530 Subject: [PATCH 09/21] run prettier --- docs/extending-console-sections.md | 12 +++--- .../main/parseConsoleSections.spec.ts | 42 +++++-------------- .../main/parseConsoleSections.ts | 4 +- 3 files changed, 18 insertions(+), 40 deletions(-) diff --git a/docs/extending-console-sections.md b/docs/extending-console-sections.md index f6b34ec0f..7285937fe 100644 --- a/docs/extending-console-sections.md +++ b/docs/extending-console-sections.md @@ -104,11 +104,11 @@ public class StackTraceAnnotator extends ConsoleSectionAnnotator { ## Which to use -| | `ConsoleSectionRule` | `ConsoleSectionAnnotator` | -|---|---|---| -| Detection | Regex pair (start / end) | Line-by-line with state | -| Runs | Client-side (frontend) | Server-side | -| Title | First capture group or full line | Set dynamically in `detect()` | -| Use for | Simple delimited output (npm, pip, etc.) | Context-dependent grouping (stack traces, test suites) | +| | `ConsoleSectionRule` | `ConsoleSectionAnnotator` | +| --------- | ---------------------------------------- | ------------------------------------------------------ | +| Detection | Regex pair (start / end) | Line-by-line with state | +| Runs | Client-side (frontend) | Server-side | +| Title | First capture group or full line | Set dynamically in `detect()` | +| Use for | Simple delimited output (npm, pip, etc.) | Context-dependent grouping (stack traces, test suites) | `##[group]` / `::group::` markers in console output are always detected regardless of any registered extensions. diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts index 9ac340a71..dbe708045 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts @@ -91,7 +91,11 @@ describe("parseConsoleSections", () => { }); it("trims trailing whitespace in title", () => { - const lines = ["##[group]Install Dependencies ", "npm ci", "##[endgroup]"]; + const lines = [ + "##[group]Install Dependencies ", + "npm ci", + "##[endgroup]", + ]; const result = parseConsoleSections(lines); const group = result[0] as ConsoleSectionGroup; expect(group.title).toBe("Install Dependencies"); @@ -120,11 +124,7 @@ describe("parseConsoleSections", () => { describe("mixed syntax", () => { it("handles ##[group] opened and ::endgroup:: closed", () => { - const lines = [ - "##[group]Mixed", - "inside", - "::endgroup::", - ]; + const lines = ["##[group]Mixed", "inside", "::endgroup::"]; const result = parseConsoleSections(lines); expect(result).toHaveLength(1); const group = result[0] as ConsoleSectionGroup; @@ -133,11 +133,7 @@ describe("parseConsoleSections", () => { }); it("handles ::group:: opened and ##[endgroup] closed", () => { - const lines = [ - "::group::Mixed2", - "inside", - "##[endgroup]", - ]; + const lines = ["::group::Mixed2", "inside", "##[endgroup]"]; const result = parseConsoleSections(lines); const group = result[0] as ConsoleSectionGroup; expect(group.title).toBe("Mixed2"); @@ -172,11 +168,7 @@ describe("parseConsoleSections", () => { }); it("detects marker with leading whitespace", () => { - const lines = [ - " ##[group]Indented", - "inside", - " ##[endgroup]", - ]; + const lines = [" ##[group]Indented", "inside", " ##[endgroup]"]; const result = parseConsoleSections(lines); expect(result).toHaveLength(1); expect((result[0] as ConsoleSectionGroup).title).toBe("Indented"); @@ -308,11 +300,7 @@ describe("parseConsoleSections", () => { describe("unclosed groups", () => { it("leaves endIndex as -1 for unclosed group", () => { - const lines = [ - "##[group]Streaming", - "line 1", - "line 2", - ]; + const lines = ["##[group]Streaming", "line 1", "line 2"]; const result = parseConsoleSections(lines); expect(result).toHaveLength(1); const group = result[0] as ConsoleSectionGroup; @@ -321,11 +309,7 @@ describe("parseConsoleSections", () => { }); it("handles unclosed nested group", () => { - const lines = [ - "##[group]Outer", - "##[group]Inner", - "still going", - ]; + const lines = ["##[group]Outer", "##[group]Inner", "still going"]; const result = parseConsoleSections(lines); const outer = result[0] as ConsoleSectionGroup; expect(outer.endIndex).toBe(-1); @@ -336,11 +320,7 @@ describe("parseConsoleSections", () => { describe("stray endgroup", () => { it("treats stray ##[endgroup] as a plain line", () => { - const lines = [ - "some output", - "##[endgroup]", - "more output", - ]; + const lines = ["some output", "##[endgroup]", "more output"]; const result = parseConsoleSections(lines); expect(result).toHaveLength(3); expect(result[1]).toMatchObject({ diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts index 9c74c242d..8e45b0d47 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts @@ -64,9 +64,7 @@ const GROUP_MARKER_RE = /##\[group\]|::group::/; * Lines between markers are grouped; all other lines remain flat. * Supports nesting. Unclosed groups get endIndex = -1. */ -export function parseConsoleSections( - lines: string[], -): ConsoleSectionNode[] { +export function parseConsoleSections(lines: string[]): ConsoleSectionNode[] { const root: ConsoleSectionNode[] = []; // Stack of open groups: each entry is the children array of the current group. const stack: { group: ConsoleSectionGroup; parent: ConsoleSectionNode[] }[] = From 79bf9575d2f641e058f7b52fdf87f9722df9ed1f Mon Sep 17 00:00:00 2001 From: Aditya Jalkhare Date: Mon, 11 May 2026 23:02:54 +0530 Subject: [PATCH 10/21] Implement collapsible sections in ConsoleSection and enhance ConsoleLogStream with section rules --- .../main/ConsoleLogStream.tsx | 26 ++++++++++-- .../main/ConsoleSection.spec.tsx | 40 +++++++++++++++++++ .../pipeline-console/main/ConsoleSection.tsx | 39 +++++++++++------- 3 files changed, 88 insertions(+), 17 deletions(-) diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.tsx index 2f05a5586..9f0831879 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.tsx +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.tsx @@ -1,7 +1,13 @@ import { useMemo, useEffect, useLayoutEffect, useRef, useState } from "react"; +import { getConsoleSectionRules } from "../../../common/RestClient.tsx"; import { ConsoleSectionNodeRenderer } from "./ConsoleSection.tsx"; -import { parseConsoleSections } from "./parseConsoleSections.ts"; +import { + applyRulesToSections, + CompiledSectionRule, + compileSectionRules, + parseConsoleSections, +} from "./parseConsoleSections.ts"; import { POLL_INTERVAL, Result, @@ -89,9 +95,23 @@ export default function ConsoleLogStream({ setScrollToLogLine(false); }, [scrollToLogLine, stepId, logBuffer.lines]); + const [sectionRules, setSectionRules] = useState([]); + useEffect(() => { + let cancelled = false; + getConsoleSectionRules(currentRunPath).then((data) => { + if (!cancelled) { + setSectionRules(compileSectionRules(data)); + } + }); + return () => { + cancelled = true; + }; + }, [currentRunPath]); + const sectionTree = useMemo( - () => parseConsoleSections(logBuffer.lines), - [logBuffer.lines], + () => + applyRulesToSections(parseConsoleSections(logBuffer.lines), sectionRules), + [logBuffer.lines, sectionRules], ); return ( diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.spec.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.spec.tsx index 5113acf52..9c04b47f1 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.spec.tsx +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.spec.tsx @@ -169,4 +169,44 @@ describe("ConsoleSection", () => { fireEvent.click(summary); expect(details.open).toBe(true); }); + + it("hides content when collapsed and restores on re-expand", () => { + const group: ConsoleSectionGroup = { + kind: "group", + title: "Collapsible Group", + startIndex: 0, + endIndex: 4, + children: [ + { kind: "line", index: 1, content: "line alpha" }, + { kind: "line", index: 2, content: "line beta" }, + { kind: "line", index: 3, content: "line gamma" }, + ], + }; + const { container } = render( + , + ); + const summary = container.querySelector("summary")!; + const details = container.querySelector("details")!; + const body = container.querySelector(".pgv-console-section__body")!; + + // Section shows up in the UI with title and content visible + expect(screen.getByText("Collapsible Group")).toBeTruthy(); + expect(details.open).toBe(true); + expect(body.textContent).toContain("line alpha"); + expect(body.textContent).toContain("line beta"); + expect(body.textContent).toContain("line gamma"); + + // Clicking collapses the section - content is not present + fireEvent.click(summary); + expect(details.open).toBe(false); + expect(container.querySelector(".pgv-console-section__body")).toBeFalsy(); + + // Clicking again restores the group + fireEvent.click(summary); + expect(details.open).toBe(true); + const restoredBody = container.querySelector(".pgv-console-section__body")!; + expect(restoredBody.textContent).toContain("line alpha"); + expect(restoredBody.textContent).toContain("line beta"); + expect(restoredBody.textContent).toContain("line gamma"); + }); }); diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.tsx index 002182529..f4cb43dc8 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.tsx +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.tsx @@ -36,11 +36,20 @@ export const ConsoleSection = memo(function ConsoleSection({ open={open} onToggle={(e) => { e.stopPropagation(); - setOpen((e.target as HTMLDetailsElement).open); + const isOpen = (e.target as HTMLDetailsElement).open; + if (isOpen !== open) { + setOpen(isOpen); + } }} className="pgv-console-section" > - + { + e.preventDefault(); + setOpen((prev) => !prev); + }} + > -
- {group.children.map((node) => ( - - ))} -
+ {open && ( +
+ {group.children.map((node) => ( + + ))} +
+ )}
); }); From b68782fecb2b8702984d391562cee74492adae5e Mon Sep 17 00:00:00 2001 From: Aditya Jalkhare Date: Mon, 11 May 2026 23:11:23 +0530 Subject: [PATCH 11/21] Merge branch 'feature/collapsible-console-elements' of https://github.com/adityajalkhare/pipeline-graph-view-plugin into feature/collapsible-console-elements --- .../pipeline-console/main/ConsoleLogStream.tsx | 7 ++++--- .../pipeline-console/main/ConsoleSection.tsx | 11 +++++++++-- .../consoleview/MarkerConsoleSectionAnnotator.java | 6 ++---- .../consoleview/ConsoleSectionAnnotatorTest.java | 11 +++-------- .../consoleview/ConsoleSectionRuleTest.java | 3 +-- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.tsx index a8cf20f9f..41411964c 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.tsx +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.tsx @@ -1,6 +1,8 @@ -import { useMemo, useEffect, useLayoutEffect, useRef, useState } from "react"; +import "./console-section.scss"; + +import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; -import { getConsoleSectionRules } from "../../../common/RestClient.tsx"; +import { BuildStep, getConsoleSectionRules } from "../../../common/RestClient.tsx"; import { ConsoleSectionNodeRenderer } from "./ConsoleSection.tsx"; import { applyRulesToSections, @@ -14,7 +16,6 @@ import { StepLogBufferInfo, TAIL_CONSOLE_LOG, } from "./PipelineConsoleModel.tsx"; -import "./console-section.scss"; export default function ConsoleLogStream({ tailLogs, diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.tsx index f4cb43dc8..9ed5c4751 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.tsx +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.tsx @@ -1,11 +1,12 @@ import { memo, useState } from "react"; +import { BuildStep } from "../../../common/RestClient.tsx"; +import { makeReactChildren, tokenizeANSIString } from "./Ansi.tsx"; +import { ConsoleLine } from "./ConsoleLine.tsx"; import { ConsoleSectionGroup, ConsoleSectionNode, } from "./parseConsoleSections.ts"; -import { makeReactChildren, tokenizeANSIString } from "./Ansi.tsx"; -import { ConsoleLine } from "./ConsoleLine.tsx"; /** Sections with more children than this default to collapsed. */ const COLLAPSE_THRESHOLD = 25; @@ -16,6 +17,7 @@ export interface ConsoleSectionProps { startByte: number; stopTailingLogs: () => void; currentRunPath: string; + buildStep?: BuildStep; } export const ConsoleSection = memo(function ConsoleSection({ @@ -24,6 +26,7 @@ export const ConsoleSection = memo(function ConsoleSection({ startByte, stopTailingLogs, currentRunPath, + buildStep, }: ConsoleSectionProps) { // Unclosed groups (still streaming) default to open. // Closed groups with many children default to collapsed. @@ -99,6 +102,7 @@ export interface ConsoleSectionNodeRendererProps { startByte: number; stopTailingLogs: () => void; currentRunPath: string; + buildStep?: BuildStep; } export const ConsoleSectionNodeRenderer = memo( @@ -108,6 +112,7 @@ export const ConsoleSectionNodeRenderer = memo( startByte, stopTailingLogs, currentRunPath, + buildStep, }: ConsoleSectionNodeRendererProps) { if (node.kind === "line") { return ( @@ -119,6 +124,7 @@ export const ConsoleSectionNodeRenderer = memo( startByte={startByte} stopTailingLogs={stopTailingLogs} currentRunPath={currentRunPath} + buildStep={buildStep} /> ); } @@ -130,6 +136,7 @@ export const ConsoleSectionNodeRenderer = memo( startByte={startByte} stopTailingLogs={stopTailingLogs} currentRunPath={currentRunPath} + buildStep={buildStep} /> ); }, diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/MarkerConsoleSectionAnnotator.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/MarkerConsoleSectionAnnotator.java index 69debe1a0..f99bd2586 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/MarkerConsoleSectionAnnotator.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/MarkerConsoleSectionAnnotator.java @@ -16,10 +16,8 @@ public class MarkerConsoleSectionAnnotator extends ConsoleSectionAnnotator { // Same ANSI stripping as the frontend. private static final Pattern ANSI_RE = Pattern.compile("\033\\[[0-9;]*[a-zA-Z]"); - private static final Pattern GROUP_START = - Pattern.compile("^(?:##\\[group\\]|::group::)\\s*(.*)$"); - private static final Pattern GROUP_END = - Pattern.compile("^(?:##\\[endgroup\\]|::endgroup::)\\s*$"); + private static final Pattern GROUP_START = Pattern.compile("^(?:##\\[group\\]|::group::)\\s*(.*)$"); + private static final Pattern GROUP_END = Pattern.compile("^(?:##\\[endgroup\\]|::endgroup::)\\s*$"); @Override public String getId() { diff --git a/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotatorTest.java b/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotatorTest.java index b0e35a48b..2c77ca681 100644 --- a/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotatorTest.java +++ b/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotatorTest.java @@ -39,12 +39,8 @@ void markerAnnotatorDetectsGitHubActionsGroup(JenkinsRule j) { @Test void markerAnnotatorDetectsEndgroup(JenkinsRule j) { MarkerConsoleSectionAnnotator annotator = new MarkerConsoleSectionAnnotator(); - assertThat( - annotator.detect("##[endgroup]").getType(), - is(ConsoleSectionAnnotator.SectionBoundary.Type.END)); - assertThat( - annotator.detect("::endgroup::").getType(), - is(ConsoleSectionAnnotator.SectionBoundary.Type.END)); + assertThat(annotator.detect("##[endgroup]").getType(), is(ConsoleSectionAnnotator.SectionBoundary.Type.END)); + assertThat(annotator.detect("::endgroup::").getType(), is(ConsoleSectionAnnotator.SectionBoundary.Type.END)); } @Test @@ -79,8 +75,7 @@ void markerAnnotatorReturnsNoneForPlainLine(JenkinsRule j) { @Test void sectionBoundaryStartCarriesTitle(JenkinsRule j) { - ConsoleSectionAnnotator.SectionBoundary boundary = - ConsoleSectionAnnotator.SectionBoundary.start("My Title"); + ConsoleSectionAnnotator.SectionBoundary boundary = ConsoleSectionAnnotator.SectionBoundary.start("My Title"); assertThat(boundary.getType(), is(ConsoleSectionAnnotator.SectionBoundary.Type.START)); assertThat(boundary.getTitle(), is("My Title")); } diff --git a/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionRuleTest.java b/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionRuleTest.java index dc118ffeb..12024de4c 100644 --- a/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionRuleTest.java +++ b/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionRuleTest.java @@ -16,8 +16,7 @@ class ConsoleSectionRuleTest { void extensionPointIsDiscoverable(JenkinsRule j) { // No built-in rules shipped; the list should be empty by default. // Plugin maintainers contribute their own rules via @Extension. - List rules = - ConsoleSectionRule.all().stream().toList(); + List rules = ConsoleSectionRule.all().stream().toList(); assertThat(rules, is(empty())); } From cae311bf647d714dcefa483e63eb6cba54e5b0dc Mon Sep 17 00:00:00 2001 From: Aditya Jalkhare Date: Mon, 11 May 2026 23:30:31 +0530 Subject: [PATCH 12/21] fix: eslint issues --- .../pipeline-console/main/ConsoleLogStream.tsx | 13 ++++++++----- .../pipeline-console/main/ConsoleSection.spec.tsx | 2 +- .../main/parseConsoleSections.spec.ts | 7 +++---- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.tsx index 4e19524fa..a6df584a9 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.tsx +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.tsx @@ -103,11 +103,14 @@ export default function ConsoleLogStream({ const [sectionRules, setSectionRules] = useState([]); useEffect(() => { let cancelled = false; - getConsoleSectionRules(currentRunPath).then((data) => { - if (!cancelled) { - setSectionRules(compileSectionRules(data)); - } - }); + getConsoleSectionRules(currentRunPath) + .then((data) => { + if (!cancelled) { + setSectionRules(compileSectionRules(data)); + } + return data; + }) + .catch(() => {}); return () => { cancelled = true; }; diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.spec.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.spec.tsx index 9c04b47f1..21c804017 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.spec.tsx +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.spec.tsx @@ -1,6 +1,6 @@ /** * @vitest-environment jsdom */ -import { render, screen, fireEvent } from "@testing-library/react"; +import { fireEvent,render, screen } from "@testing-library/react"; import { ConsoleSection, diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts index dbe708045..a16ed259b 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts @@ -1,11 +1,10 @@ /** * @vitest-environment jsdom */ import { - parseConsoleSections, - ConsoleSectionGroup, - ConsoleSectionLine, - compileSectionRules, applyRulesToSections, + compileSectionRules, + ConsoleSectionGroup, + parseConsoleSections, } from "./parseConsoleSections.ts"; describe("parseConsoleSections", () => { From 6db3e5c457ee6cd20a28211ee06a8977bba1b472 Mon Sep 17 00:00:00 2001 From: Aditya Jalkhare Date: Mon, 11 May 2026 23:41:31 +0530 Subject: [PATCH 13/21] fix: add eslint directive for no-control-regex rule --- .../pipeline-console/main/parseConsoleSections.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts index 8e45b0d47..13c0fb359 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts @@ -31,6 +31,7 @@ export interface ConsoleSectionGroup { export type ConsoleSectionNode = ConsoleSectionLine | ConsoleSectionGroup; // ANSI escape sequence pattern for stripping before marker detection. +// eslint-disable-next-line no-control-regex const ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]/g; // HTML tag pattern for stripping before marker detection. From f95d32863f4adccd369426809ac1c25cfe9a309b Mon Sep 17 00:00:00 2001 From: Aditya Jalkhare Date: Mon, 11 May 2026 23:42:41 +0530 Subject: [PATCH 14/21] fix: ran prettier --- .../pipeline-console/main/ConsoleSection.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.spec.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.spec.tsx index 21c804017..8fb9c08c3 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.spec.tsx +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.spec.tsx @@ -1,6 +1,6 @@ /** * @vitest-environment jsdom */ -import { fireEvent,render, screen } from "@testing-library/react"; +import { fireEvent, render, screen } from "@testing-library/react"; import { ConsoleSection, From c61964145008eae03a60fcb32e23f1064f0e3bdd Mon Sep 17 00:00:00 2001 From: Aditya Jalkhare Date: Tue, 12 May 2026 19:33:01 +0530 Subject: [PATCH 15/21] Add console section boundaries support - Introduced `getConsoleSectionBoundaries` API to fetch section boundaries from the backend. - Added `ConsoleSectionBoundary` interface to define the structure of boundary events. - Updated `ConsoleLogStream` to utilize the new API and integrate boundaries into the console section rendering. - Implemented `applyAnnotatorBoundaries` function to handle grouping of console lines based on boundaries. - Created tests for the new functionality, ensuring correct behavior of boundary processing and integration with existing section rules. - Added `ConsoleSectionProcessor` to manage the processing of console log text and collection of boundary events. - Updated existing `ConsoleSectionAnnotator` to ensure compatibility with the new boundary detection mechanism. --- package-lock.json | 1200 +++++++++-------- src/main/frontend/common/RestClient.tsx | 24 + .../main/ConsoleLogStream.tsx | 29 +- .../main/parseConsoleSections.spec.ts | 87 ++ .../main/parseConsoleSections.ts | 93 +- .../consoleview/ConsoleSectionAnnotator.java | 7 +- .../consoleview/ConsoleSectionProcessor.java | 91 ++ .../consoleview/ConsoleSectionRule.java | 8 +- .../MarkerConsoleSectionAnnotator.java | 6 +- .../PipelineConsoleViewAction.java | 56 + .../ConsoleSectionAnnotatorTest.java | 6 +- .../ConsoleSectionProcessorTest.java | 162 +++ 12 files changed, 1169 insertions(+), 600 deletions(-) create mode 100644 src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionProcessor.java create mode 100644 src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionProcessorTest.java diff --git a/package-lock.json b/package-lock.json index 724992568..60ab6ee26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,9 +43,9 @@ } }, "node_modules/@adobe/css-tools": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.2.tgz", - "integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==", + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", "dev": true, "license": "MIT" }, @@ -116,9 +116,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", + "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", "dev": true, "license": "MIT", "engines": { @@ -263,23 +263,23 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/types": "^7.29.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", + "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", "dev": true, "license": "MIT", "dependencies": { @@ -293,9 +293,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", - "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", "dev": true, "license": "MIT", "engines": { @@ -364,9 +364,9 @@ } }, "node_modules/@bufbuild/protobuf": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.9.0.tgz", - "integrity": "sha512-rnJenoStJ8nvmt9Gzye8nkYd6V22xUAnu4086ER7h1zJ508vStko4pMvDeQ446ilDTFpV5wnoc5YS7XvMwwMqA==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.12.0.tgz", + "integrity": "sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA==", "dev": true, "license": "(Apache-2.0 AND BSD-3-Clause)" }, @@ -651,19 +651,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@eslint/js": { "version": "9.39.4", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", @@ -720,72 +707,72 @@ } }, "node_modules/@formatjs/bigdecimal": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@formatjs/bigdecimal/-/bigdecimal-0.2.3.tgz", - "integrity": "sha512-d7LpumdsbHueHdlVMos2yROIumyisUJNlFAk3PpN/5YcnXhWnkYmeiaQnOjqmmGx8BbaphoIyaF2CPus3cownw==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@formatjs/bigdecimal/-/bigdecimal-0.2.4.tgz", + "integrity": "sha512-lQ0S33FRGRYimSKKy7siaDAZAwvOGZsbpx5/lzDvLldj7GqJISPntS0JjYzf8S+B4HjYrY1EkCcZFj8vV9fuYg==", "license": "MIT" }, "node_modules/@formatjs/fast-memoize": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.4.tgz", - "integrity": "sha512-Lbke1aOrsygKKR09Ux0NrZgbTqpDmiwXOgzyDOJ8Owr1zd5qOKTauf62hH+Seeku3ju77rHWH9I5SfX2CN0vuA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.5.tgz", + "integrity": "sha512-KLi3fan6WnCHmigd9pmEEN8Hid0v4wiFBW576M/d07KMWYecf1CvyMI3n34vCmHT4AoVqG2n702kiHbXjzZX2A==", "license": "MIT" }, "node_modules/@formatjs/intl-durationformat": { - "version": "0.10.8", - "resolved": "https://registry.npmjs.org/@formatjs/intl-durationformat/-/intl-durationformat-0.10.8.tgz", - "integrity": "sha512-1Crir41n1kMTVejBg7tkLfBRSWm7p1y+Bt4Nyny1DtxH41/+q2qZ2vSyTiQEdKbvSvmt0WG/gInFrMsK8lx1/A==", + "version": "0.10.9", + "resolved": "https://registry.npmjs.org/@formatjs/intl-durationformat/-/intl-durationformat-0.10.9.tgz", + "integrity": "sha512-iGUTNmGFR1QzzShzLE+uX6ShyBUsucGDm4Ujd4+8MYqS7E5PXAS59LAkKw1EoxCaYlIs8mPaqFBisK1ttWOJ9Q==", "license": "MIT", "dependencies": { - "@formatjs/bigdecimal": "0.2.3", - "@formatjs/intl-localematcher": "0.8.6" + "@formatjs/bigdecimal": "0.2.4", + "@formatjs/intl-localematcher": "0.8.7" } }, "node_modules/@formatjs/intl-localematcher": { - "version": "0.8.6", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.6.tgz", - "integrity": "sha512-AZRgUxj0q93lyF7Z5lFS85bLINXuBLX4R3tCKicO6fSWo6cvh9GQfoR3B1WlsqQwefZ1QORTivhInx7gM6HUzQ==", + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.7.tgz", + "integrity": "sha512-1R/ljfRKG1fUhKG4F0lUmrEKPkr/PlHqbgQ8xeYQYYunXu5/0+vbQeeVgGAgydp13Tq+S1X5Qjn6L90hijXjHg==", "license": "MIT", "dependencies": { - "@formatjs/fast-memoize": "3.1.4" + "@formatjs/fast-memoize": "3.1.5" } }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", - "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.3.0" + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, - "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", - "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", "dev": true, "license": "Apache-2.0", "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18.18.0" } }, "node_modules/@humanwhocodes/gitignore-to-minimatch": { @@ -814,9 +801,9 @@ } }, "node_modules/@humanwhocodes/retry": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", - "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -897,9 +884,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.128.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.128.0.tgz", - "integrity": "sha512-huv1Y/LzBJkBVHt3OlC7u0zHBW9qXf1FdD7sGmc1rXc2P1mTwHssYv7jyGx5KAACSCH+9B3Bhn6Z9luHRvf7pQ==", + "version": "0.129.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.129.0.tgz", + "integrity": "sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==", "dev": true, "license": "MIT", "funding": { @@ -907,18 +894,18 @@ } }, "node_modules/@parcel/watcher": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", - "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, "dependencies": { - "detect-libc": "^1.0.3", + "detect-libc": "^2.0.3", "is-glob": "^4.0.3", - "micromatch": "^4.0.5", - "node-addon-api": "^7.0.0" + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">= 10.0.0" @@ -928,25 +915,25 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.1", - "@parcel/watcher-darwin-arm64": "2.5.1", - "@parcel/watcher-darwin-x64": "2.5.1", - "@parcel/watcher-freebsd-x64": "2.5.1", - "@parcel/watcher-linux-arm-glibc": "2.5.1", - "@parcel/watcher-linux-arm-musl": "2.5.1", - "@parcel/watcher-linux-arm64-glibc": "2.5.1", - "@parcel/watcher-linux-arm64-musl": "2.5.1", - "@parcel/watcher-linux-x64-glibc": "2.5.1", - "@parcel/watcher-linux-x64-musl": "2.5.1", - "@parcel/watcher-win32-arm64": "2.5.1", - "@parcel/watcher-win32-ia32": "2.5.1", - "@parcel/watcher-win32-x64": "2.5.1" + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", - "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", "cpu": [ "arm64" ], @@ -965,9 +952,9 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", - "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", "cpu": [ "arm64" ], @@ -986,9 +973,9 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", - "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", "cpu": [ "x64" ], @@ -1007,9 +994,9 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", - "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", "cpu": [ "x64" ], @@ -1028,13 +1015,16 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", - "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1049,13 +1039,16 @@ } }, "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", - "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1070,13 +1063,16 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", - "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1091,13 +1087,16 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", - "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", "cpu": [ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1112,13 +1111,16 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", - "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -1133,13 +1135,16 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", - "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -1154,9 +1159,9 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", - "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", "cpu": [ "arm64" ], @@ -1175,9 +1180,9 @@ } }, "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", - "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", "cpu": [ "ia32" ], @@ -1196,9 +1201,9 @@ } }, "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", - "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", "cpu": [ "x64" ], @@ -1227,9 +1232,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.18.tgz", - "integrity": "sha512-lIDyUAfD7U3+BWKzdxMbJcsYHuqXqmGz40aeRqvuAm3y5TkJSYTBW2RDrn65DJFPQqVjUAUqq5uz8urzQ8aBdQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0.tgz", + "integrity": "sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==", "cpu": [ "arm64" ], @@ -1244,9 +1249,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.18.tgz", - "integrity": "sha512-apJq2ktnGp27nSInMR5Vcj8kY6xJzDAvfdIFlpDcAK/w4cDO58qVoi1YQsES/SKiFNge/6e4CUzgjfHduYqWpQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0.tgz", + "integrity": "sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==", "cpu": [ "arm64" ], @@ -1261,9 +1266,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.18.tgz", - "integrity": "sha512-5Ofot8xbs+pxRHJqm9/9N/4sTQOvdrwEsmPE9pdLEEoAbdZtG6F2LMDfO1sp6ZAtXJuJV/21ew2srq3W8NXB5g==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0.tgz", + "integrity": "sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==", "cpu": [ "x64" ], @@ -1278,9 +1283,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.18.tgz", - "integrity": "sha512-7h8eeOTT1eyqJyx64BFCnWZpNm486hGWt2sqeLLgDxA0xI1oGZ9H7gK1S85uNGmBhkdPwa/6reTxfFFKvIsebw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0.tgz", + "integrity": "sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==", "cpu": [ "x64" ], @@ -1295,9 +1300,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.18.tgz", - "integrity": "sha512-eRcm/HVt9U/JFu5RKAEKwGQYtDCKWLiaH6wOnsSEp6NMBb/3Os8LgHZlNyzMpFVNmiiMFlfb2zEnebfzJrHFmg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0.tgz", + "integrity": "sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==", "cpu": [ "arm" ], @@ -1312,9 +1317,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.18.tgz", - "integrity": "sha512-SOrT/cT4ukTmgnrEz/Hg3m7LBnuCLW9psDeMKrimRWY4I8DmnO7Lco8W2vtqPmMkbVu8iJ+g4GFLVLLOVjJ9DQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0.tgz", + "integrity": "sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==", "cpu": [ "arm64" ], @@ -1332,9 +1337,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.18.tgz", - "integrity": "sha512-QWjdxN1HJCpBTAcZ5N5F7wju3gVPzRzSpmGzx7na0c/1qpN9CFil+xt+l9lV/1M6/gqHSNXCiqPfwhVJPeLnug==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0.tgz", + "integrity": "sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==", "cpu": [ "arm64" ], @@ -1352,9 +1357,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.18.tgz", - "integrity": "sha512-ugCOyj7a4d9h3q9B+wXmf6g3a68UsjGh6dob5DHevHGMwDUbhsYNbSPxJsENcIttJZ9jv7qGM2UesLw5jqIhdg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0.tgz", + "integrity": "sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==", "cpu": [ "ppc64" ], @@ -1372,9 +1377,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.18.tgz", - "integrity": "sha512-kKWRhbsotpXkGbcd5dllUWg5gEXcDAa8u5YnP9AV5DYNbvJHGzzuwv7dpmhc8NqKMJldl0a+x76IHbspEpEmdA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0.tgz", + "integrity": "sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==", "cpu": [ "s390x" ], @@ -1392,9 +1397,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.18.tgz", - "integrity": "sha512-uCo8ElcCIAMyYAZyuIZ81oFkhTSIllNvUCHCAlbhlN4ji3uC28h7IIdlXyIvGO7HsuqnV9p3rD/bpH7XhIyhRw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0.tgz", + "integrity": "sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==", "cpu": [ "x64" ], @@ -1412,9 +1417,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.18.tgz", - "integrity": "sha512-XNOQZtuE6yUIvx4rwGemwh8kpL1xvU41FXy/s9K7T/3JVcqGzo3NfKM2HrbrGgfPYGFW42f07Wk++aOC6B9NWA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0.tgz", + "integrity": "sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==", "cpu": [ "x64" ], @@ -1432,9 +1437,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.18.tgz", - "integrity": "sha512-tSn/kzrfa7tNOXr7sEacDBN4YsIqTyLqh45IO0nHDwtpKIDNDJr+VFojt+4klSpChxB29JLyduSsE0MKEwa65A==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0.tgz", + "integrity": "sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==", "cpu": [ "arm64" ], @@ -1449,9 +1454,9 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.18.tgz", - "integrity": "sha512-+J9YGmc+czgqlhYmwun3S3O0FIZhsH8ep2456xwjAdIOmuJxM7xz4P4PtrxU+Bz17a/5bqPA8o3HAAoX0teUdg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0.tgz", + "integrity": "sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==", "cpu": [ "wasm32" ], @@ -1468,9 +1473,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.18.tgz", - "integrity": "sha512-zsu47DgU0FQzSwi6sU9dZoEdUv7pc1AptSEz/Z8HBg54sV0Pbs3N0+CrIbTsgiu6EyoaNN9CHboqbLaz9lhOyQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0.tgz", + "integrity": "sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==", "cpu": [ "arm64" ], @@ -1485,9 +1490,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.18.tgz", - "integrity": "sha512-7H+3yqGgmnlDTRRhw/xpYY9J1kf4GC681nVc4GqKhExZTDrVVrV2tsOR9kso0fvgBdcTCcQShx4SLLoHgaLwhg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0.tgz", + "integrity": "sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==", "cpu": [ "x64" ], @@ -1536,9 +1541,9 @@ } }, "node_modules/@testing-library/dom": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", - "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, "license": "MIT", "peer": true, @@ -1547,9 +1552,9 @@ "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", - "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", + "picocolors": "1.1.1", "pretty-format": "^27.0.2" }, "engines": { @@ -1644,13 +1649,14 @@ "peer": true }, "node_modules/@types/chai": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", - "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { - "@types/deep-eql": "*" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, "node_modules/@types/deep-eql": { @@ -1661,9 +1667,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -1675,9 +1681,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.12.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.3.tgz", - "integrity": "sha512-8oljBDGun9cIsZRJR6fkihn0TSXJI0UDOOhncYaERq6M0JMDoPLxyscwruJcb4GKS6dvK/d8xebYBg27h/duaQ==", + "version": "24.12.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz", + "integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==", "dev": true, "license": "MIT", "dependencies": { @@ -1705,17 +1711,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz", - "integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz", + "integrity": "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.59.0", - "@typescript-eslint/type-utils": "8.59.0", - "@typescript-eslint/utils": "8.59.0", - "@typescript-eslint/visitor-keys": "8.59.0", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/type-utils": "8.59.3", + "@typescript-eslint/utils": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -1728,7 +1734,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.59.0", + "@typescript-eslint/parser": "^8.59.3", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -1744,16 +1750,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz", - "integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.3.tgz", + "integrity": "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.59.0", - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/typescript-estree": "8.59.0", - "@typescript-eslint/visitor-keys": "8.59.0", + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", "debug": "^4.4.3" }, "engines": { @@ -1769,14 +1775,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz", - "integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.3.tgz", + "integrity": "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.59.0", - "@typescript-eslint/types": "^8.59.0", + "@typescript-eslint/tsconfig-utils": "^8.59.3", + "@typescript-eslint/types": "^8.59.3", "debug": "^4.4.3" }, "engines": { @@ -1791,14 +1797,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz", - "integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", + "integrity": "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/visitor-keys": "8.59.0" + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1809,9 +1815,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz", - "integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz", + "integrity": "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==", "dev": true, "license": "MIT", "engines": { @@ -1826,15 +1832,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz", - "integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz", + "integrity": "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/typescript-estree": "8.59.0", - "@typescript-eslint/utils": "8.59.0", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -1851,9 +1857,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz", - "integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.3.tgz", + "integrity": "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==", "dev": true, "license": "MIT", "engines": { @@ -1865,16 +1871,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz", - "integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz", + "integrity": "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.59.0", - "@typescript-eslint/tsconfig-utils": "8.59.0", - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/visitor-keys": "8.59.0", + "@typescript-eslint/project-service": "8.59.3", + "@typescript-eslint/tsconfig-utils": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/visitor-keys": "8.59.3", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -1903,9 +1909,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "dev": true, "license": "MIT", "dependencies": { @@ -1932,9 +1938,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "dev": true, "license": "ISC", "bin": { @@ -1945,16 +1951,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz", - "integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.3.tgz", + "integrity": "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.59.0", - "@typescript-eslint/types": "8.59.0", - "@typescript-eslint/typescript-estree": "8.59.0" + "@typescript-eslint/scope-manager": "8.59.3", + "@typescript-eslint/types": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1969,13 +1975,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz", - "integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz", + "integrity": "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.59.0", + "@typescript-eslint/types": "8.59.3", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -2026,16 +2032,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", - "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", + "integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.5", - "@vitest/utils": "4.1.5", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -2044,13 +2050,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz", - "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz", + "integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.5", + "@vitest/spy": "4.1.6", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -2071,9 +2077,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", - "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", + "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", "dev": true, "license": "MIT", "dependencies": { @@ -2084,13 +2090,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz", - "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz", + "integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.5", + "@vitest/utils": "4.1.6", "pathe": "^2.0.3" }, "funding": { @@ -2098,14 +2104,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz", - "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz", + "integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.5", - "@vitest/utils": "4.1.5", + "@vitest/pretty-format": "4.1.6", + "@vitest/utils": "4.1.6", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -2114,9 +2120,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", - "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz", + "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==", "dev": true, "license": "MIT", "funding": { @@ -2124,13 +2130,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", - "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", + "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.5", + "@vitest/pretty-format": "4.1.6", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -2139,9 +2145,9 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", "bin": { @@ -2162,9 +2168,9 @@ } }, "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -2240,18 +2246,20 @@ } }, "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2358,6 +2366,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -2392,13 +2410,16 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.19", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", - "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "version": "2.10.29", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.29.tgz", + "integrity": "sha512-Asa2krT+XTPZINCS+2QcyS8WTkObE77RwkydwF7h6DmnKqbvlalz93m/dnphUyCa6SWSP51VgtEUf2FN+gelFQ==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/bidi-js": { @@ -2412,9 +2433,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -2422,24 +2443,10 @@ "concat-map": "0.0.1" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, "funding": [ { @@ -2457,11 +2464,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -2471,15 +2478,15 @@ } }, "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" }, "engines": { @@ -2531,9 +2538,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001769", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", - "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "version": "1.0.30001792", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001792.tgz", + "integrity": "sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==", "dev": true, "funding": [ { @@ -2826,17 +2833,13 @@ } }, "node_modules/detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", - "optional": true, - "bin": { - "detect-libc": "bin/detect-libc.js" - }, "engines": { - "node": ">=0.10" + "node": ">=8" } }, "node_modules/doctrine": { @@ -2876,21 +2879,21 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.286", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", - "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "version": "1.5.353", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.353.tgz", + "integrity": "sha512-kOrWphBi8TOZyiJZqsgqIle0lw+tzmnQK83pV9dZUd01Nm2POECSyFQMAuarzZdYqQW7FH9RaYOuaRo3h+bQ3w==", "dev": true, "license": "ISC" }, "node_modules/enhanced-resolve": { - "version": "5.18.1", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", - "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", + "version": "5.21.3", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.3.tgz", + "integrity": "sha512-QyL119InA+XXEkNLNTPCXPugSvOfhwv0JOlGNzvxs0hZaiHLNvXSpudUWsOlsXGWJh8G6ckCScEkVHfX3kw/2Q==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.3" }, "engines": { "node": ">=10.13.0" @@ -2910,9 +2913,9 @@ } }, "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", "dev": true, "license": "MIT", "dependencies": { @@ -2920,18 +2923,18 @@ "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", @@ -2943,21 +2946,24 @@ "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", + "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", + "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", + "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -2966,7 +2972,7 @@ "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" + "which-typed-array": "^1.1.19" }, "engines": { "node": ">= 0.4" @@ -2996,33 +3002,40 @@ } }, "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.2.tgz", + "integrity": "sha512-HVLACW1TppGYjJ8H6/jqH/pqOtKRw6wMlrB23xfExmFWxFquAIWCmwoLsOyN96K4a5KbmOf5At9ZUO3GZbetAw==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", + "es-abstract": "^1.24.2", "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", + "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", + "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", - "safe-array-concat": "^1.1.3" + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -3183,9 +3196,9 @@ } }, "node_modules/eslint-compat-utils/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "dev": true, "license": "ISC", "bin": { @@ -3268,9 +3281,9 @@ } }, "node_modules/eslint-plugin-n/node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "dev": true, "license": "ISC", "bin": { @@ -3281,9 +3294,9 @@ } }, "node_modules/eslint-plugin-promise": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-7.2.1.tgz", - "integrity": "sha512-SWKjd+EuvWkYaS+uN2csvj0KoP43YTu7+phKQ5v+xw6+A0gutVX2yqCeCkC3uLCJFiPfR2dD8Es5L7yUsmvEaA==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-7.3.0.tgz", + "integrity": "sha512-6uGiOR0INuujr6PEQmeSSP7GbIMJ/ebEXXiEzb/nOj68LknH5Pxzb/AbZivmr6VE6TkTE8rTjRK9zhKpK6HsRA==", "dev": true, "license": "ISC", "dependencies": { @@ -3296,7 +3309,7 @@ "url": "https://opencollective.com/eslint" }, "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0" } }, "node_modules/eslint-plugin-react": { @@ -3352,24 +3365,6 @@ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/eslint-plugin-simple-import-sort": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-13.0.0.tgz", @@ -3429,9 +3424,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -3546,20 +3541,6 @@ "node": ">=16.0.0" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3592,9 +3573,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, @@ -3670,6 +3651,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3738,9 +3729,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", - "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", "dev": true, "license": "MIT", "dependencies": { @@ -3764,9 +3755,9 @@ } }, "node_modules/globals": { - "version": "17.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", - "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", "engines": { @@ -3902,9 +3893,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "dev": true, "license": "MIT", "dependencies": { @@ -4098,13 +4089,13 @@ } }, "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", "dev": true, "license": "MIT", "dependencies": { - "hasown": "^2.0.2" + "hasown": "^2.0.3" }, "engines": { "node": ">= 0.4" @@ -4175,14 +4166,15 @@ } }, "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" }, @@ -4219,15 +4211,17 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", "dev": true, "license": "MIT", - "optional": true, "engines": { - "node": ">=0.12.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-number-object": { @@ -4493,9 +4487,9 @@ } }, "node_modules/jsdom/node_modules/lru-cache": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", - "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "version": "11.3.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -4732,6 +4726,9 @@ "arm64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4753,6 +4750,9 @@ "arm64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4774,6 +4774,9 @@ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4795,6 +4798,9 @@ "x64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -4850,16 +4856,6 @@ "url": "https://opencollective.com/parcel" } }, - "node_modules/lightningcss/node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/linkify-html": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/linkify-html/-/linkify-html-4.3.2.tgz", @@ -4969,35 +4965,6 @@ "dev": true, "license": "CC0-1.0" }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -5029,9 +4996,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -5098,6 +5065,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/neostandard/node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/neostandard/node_modules/locate-path": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-8.0.0.tgz", @@ -5167,10 +5147,29 @@ "license": "MIT", "optional": true }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.44", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", + "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", "dev": true, "license": "MIT" }, @@ -5702,6 +5701,30 @@ "node": ">=0.10.0" } }, + "node_modules/resolve": { + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -5723,14 +5746,14 @@ } }, "node_modules/rolldown": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.18.tgz", - "integrity": "sha512-phmyKBpuBdRYDf4hgyynGAYn/rDDe+iZXKVJ7WX5b1zQzpLkP5oJRPGsfJuHdzPMlyyEO/4sPW6yfSx2gf7lVg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0.tgz", + "integrity": "sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.128.0", - "@rolldown/pluginutils": "1.0.0-rc.18" + "@oxc-project/types": "=0.129.0", + "@rolldown/pluginutils": "1.0.0" }, "bin": { "rolldown": "bin/cli.mjs" @@ -5739,27 +5762,27 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.18", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.18", - "@rolldown/binding-darwin-x64": "1.0.0-rc.18", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.18", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.18", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.18", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.18", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.18", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.18", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.18", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.18", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.18", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.18", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.18", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.18" + "@rolldown/binding-android-arm64": "1.0.0", + "@rolldown/binding-darwin-arm64": "1.0.0", + "@rolldown/binding-darwin-x64": "1.0.0", + "@rolldown/binding-freebsd-x64": "1.0.0", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0", + "@rolldown/binding-linux-arm64-gnu": "1.0.0", + "@rolldown/binding-linux-arm64-musl": "1.0.0", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0", + "@rolldown/binding-linux-s390x-gnu": "1.0.0", + "@rolldown/binding-linux-x64-gnu": "1.0.0", + "@rolldown/binding-linux-x64-musl": "1.0.0", + "@rolldown/binding-openharmony-arm64": "1.0.0", + "@rolldown/binding-wasm32-wasi": "1.0.0", + "@rolldown/binding-win32-arm64-msvc": "1.0.0", + "@rolldown/binding-win32-x64-msvc": "1.0.0" } }, "node_modules/rolldown/node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.18", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.18.tgz", - "integrity": "sha512-CUY5Mnhe64xQBGZEEXQ5WyZwsc1JU3vAZLIxtrsBt3LO6UOb+C8GunVKqe9sT8NeWb4lqSaoJtp2xo6GxT1MNw==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0.tgz", + "integrity": "sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==", "dev": true, "license": "MIT" }, @@ -5774,15 +5797,15 @@ } }, "node_modules/safe-array-concat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", - "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, @@ -6019,6 +6042,7 @@ "arm" ], "dev": true, + "libc": "glibc", "license": "MIT", "optional": true, "os": [ @@ -6036,6 +6060,7 @@ "arm64" ], "dev": true, + "libc": "glibc", "license": "MIT", "optional": true, "os": [ @@ -6053,6 +6078,7 @@ "arm" ], "dev": true, + "libc": "musl", "license": "MIT", "optional": true, "os": [ @@ -6070,6 +6096,7 @@ "arm64" ], "dev": true, + "libc": "musl", "license": "MIT", "optional": true, "os": [ @@ -6087,6 +6114,7 @@ "riscv64" ], "dev": true, + "libc": "musl", "license": "MIT", "optional": true, "os": [ @@ -6104,6 +6132,7 @@ "x64" ], "dev": true, + "libc": "musl", "license": "MIT", "optional": true, "os": [ @@ -6121,6 +6150,7 @@ "riscv64" ], "dev": true, + "libc": "glibc", "license": "MIT", "optional": true, "os": [ @@ -6138,6 +6168,7 @@ "x64" ], "dev": true, + "libc": "glibc", "license": "MIT", "optional": true, "os": [ @@ -6336,14 +6367,14 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -6422,6 +6453,20 @@ "dev": true, "license": "MIT" }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -6593,9 +6638,9 @@ } }, "node_modules/sync-message-port": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.1.3.tgz", - "integrity": "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/sync-message-port/-/sync-message-port-1.2.0.tgz", + "integrity": "sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==", "dev": true, "license": "MIT", "engines": { @@ -6603,13 +6648,17 @@ } }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", "dev": true, "license": "MIT", "engines": { "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, "node_modules/tinybench": { @@ -6620,9 +6669,9 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", - "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", "dev": true, "license": "MIT", "engines": { @@ -6666,39 +6715,25 @@ } }, "node_modules/tldts": { - "version": "7.0.17", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.17.tgz", - "integrity": "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==", + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", + "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.0.17" + "tldts-core": "^7.0.30" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.0.17", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.17.tgz", - "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==", + "version": "7.0.30", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz", + "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==", "dev": true, "license": "MIT" }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/tough-cookie": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", @@ -6874,16 +6909,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.59.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.0.tgz", - "integrity": "sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==", + "version": "8.59.3", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.3.tgz", + "integrity": "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.59.0", - "@typescript-eslint/parser": "8.59.0", - "@typescript-eslint/typescript-estree": "8.59.0", - "@typescript-eslint/utils": "8.59.0" + "@typescript-eslint/eslint-plugin": "8.59.3", + "@typescript-eslint/parser": "8.59.3", + "@typescript-eslint/typescript-estree": "8.59.3", + "@typescript-eslint/utils": "8.59.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6995,16 +7030,16 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.0.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.11.tgz", - "integrity": "sha512-Jz1mxtUBR5xTT65VOdJZUUeoyLtqljmFkiUXhPTLZka3RDc9vpi/xXkyrnsdRcm2lIi3l3GPMnAidTsEGIj3Ow==", + "version": "8.0.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.12.tgz", + "integrity": "sha512-w2dDofOWv2QB09ZITZBsvKTVAlYvPR4IAmrY/v0ir9KvLs0xybR7i48wxhM1/oyBWO34wPns+bPGw5ZrZqDpZg==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.14", - "rolldown": "1.0.0-rc.18", + "rolldown": "1.0.0", "tinyglobby": "^0.2.16" }, "bin": { @@ -7083,19 +7118,19 @@ } }, "node_modules/vitest": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", - "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz", + "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.5", - "@vitest/mocker": "4.1.5", - "@vitest/pretty-format": "4.1.5", - "@vitest/runner": "4.1.5", - "@vitest/snapshot": "4.1.5", - "@vitest/spy": "4.1.5", - "@vitest/utils": "4.1.5", + "@vitest/expect": "4.1.6", + "@vitest/mocker": "4.1.6", + "@vitest/pretty-format": "4.1.6", + "@vitest/runner": "4.1.6", + "@vitest/snapshot": "4.1.6", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -7123,12 +7158,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.5", - "@vitest/browser-preview": "4.1.5", - "@vitest/browser-webdriverio": "4.1.5", - "@vitest/coverage-istanbul": "4.1.5", - "@vitest/coverage-v8": "4.1.5", - "@vitest/ui": "4.1.5", + "@vitest/browser-playwright": "4.1.6", + "@vitest/browser-preview": "4.1.6", + "@vitest/browser-webdriverio": "4.1.6", + "@vitest/coverage-istanbul": "4.1.6", + "@vitest/coverage-v8": "4.1.6", + "@vitest/ui": "4.1.6", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -7172,13 +7207,6 @@ } } }, - "node_modules/vitest/node_modules/es-module-lexer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", - "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", - "dev": true, - "license": "MIT" - }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -7311,9 +7339,9 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -7397,9 +7425,9 @@ } }, "node_modules/zod": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", - "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "dev": true, "license": "MIT", "funding": { diff --git a/src/main/frontend/common/RestClient.tsx b/src/main/frontend/common/RestClient.tsx index 36112e0a0..b76e97ab6 100644 --- a/src/main/frontend/common/RestClient.tsx +++ b/src/main/frontend/common/RestClient.tsx @@ -180,6 +180,30 @@ export async function getConsoleSectionRules( } } +export interface ConsoleSectionBoundary { + lineIndex: number; + type: "START" | "END"; + title?: string; +} + +export async function getConsoleSectionBoundaries( + url: string, + nodeId: string, +): Promise { + try { + const response = await fetch( + `${url}stages/consoleSectionBoundaries?nodeId=${encodeURIComponent(nodeId)}`, + ); + if (!response.ok) throw response.statusText; + return await response.json(); + } catch (e) { + console.error( + `Caught error when fetching console section boundaries: '${e}'`, + ); + return []; + } +} + export async function getResourceBundle( resource: string, ): Promise { diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.tsx index a6df584a9..13215ab41 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.tsx +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.tsx @@ -4,10 +4,13 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { BuildStep, + ConsoleSectionBoundary, + getConsoleSectionBoundaries, getConsoleSectionRules, } from "../../../common/RestClient.tsx"; import { ConsoleSectionNodeRenderer } from "./ConsoleSection.tsx"; import { + applyAnnotatorBoundaries, applyRulesToSections, CompiledSectionRule, compileSectionRules, @@ -116,10 +119,32 @@ export default function ConsoleLogStream({ }; }, [currentRunPath]); + const [boundaries, setBoundaries] = useState([]); + useEffect(() => { + let cancelled = false; + getConsoleSectionBoundaries(currentRunPath, stepId) + .then((data) => { + if (!cancelled) { + setBoundaries(data); + } + return data; + }) + .catch(() => {}); + return () => { + cancelled = true; + }; + }, [currentRunPath, stepId]); + const sectionTree = useMemo( () => - applyRulesToSections(parseConsoleSections(logBuffer.lines), sectionRules), - [logBuffer.lines, sectionRules], + applyAnnotatorBoundaries( + applyRulesToSections( + parseConsoleSections(logBuffer.lines), + sectionRules, + ), + boundaries, + ), + [logBuffer.lines, sectionRules, boundaries], ); return ( diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts index a16ed259b..a3d9f9944 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts @@ -1,6 +1,7 @@ /** * @vitest-environment jsdom */ import { + applyAnnotatorBoundaries, applyRulesToSections, compileSectionRules, ConsoleSectionGroup, @@ -505,3 +506,89 @@ describe("applyRulesToSections", () => { expect((result[0] as ConsoleSectionGroup).title).toBe("Terraform Plan"); }); }); + +describe("applyAnnotatorBoundaries", () => { + it("returns nodes unchanged when boundaries is empty", () => { + const lines = ["a", "b", "c"]; + const flat = parseConsoleSections(lines); + const result = applyAnnotatorBoundaries(flat, []); + expect(result).toEqual(flat); + }); + + it("groups lines between START and END boundaries", () => { + const lines = ["line 0", "line 1", "line 2", "line 3", "line 4"]; + const flat = parseConsoleSections(lines); + const result = applyAnnotatorBoundaries(flat, [ + { lineIndex: 1, type: "START", title: "Group A" }, + { lineIndex: 3, type: "END" }, + ]); + + // START (line 1) and END (line 3) are consumed; line 2 is a child. + expect(result).toHaveLength(3); + expect(result[0]).toMatchObject({ kind: "line", index: 0 }); + + const group = result[1] as ConsoleSectionGroup; + expect(group.kind).toBe("group"); + expect(group.title).toBe("Group A"); + expect(group.startIndex).toBe(1); + expect(group.endIndex).toBe(3); + expect(group.children).toHaveLength(1); + expect(group.children[0]).toMatchObject({ kind: "line", index: 2 }); + + expect(result[2]).toMatchObject({ kind: "line", index: 4 }); + }); + + it("auto-closes unclosed annotator group", () => { + const lines = ["line 0", "line 1", "line 2"]; + const flat = parseConsoleSections(lines); + const result = applyAnnotatorBoundaries(flat, [ + { lineIndex: 1, type: "START", title: "Open" }, + ]); + + expect(result).toHaveLength(2); + expect(result[0]).toMatchObject({ kind: "line", index: 0 }); + + const group = result[1] as ConsoleSectionGroup; + expect(group.kind).toBe("group"); + expect(group.title).toBe("Open"); + expect(group.endIndex).toBe(-1); + expect(group.children).toHaveLength(1); + expect(group.children[0]).toMatchObject({ kind: "line", index: 2 }); + }); + + it("passes existing marker groups through unchanged", () => { + const lines = [ + "line 0", + "##[group]Build", + "compiling", + "##[endgroup]", + "line 4", + ]; + const parsed = parseConsoleSections(lines); + const result = applyAnnotatorBoundaries(parsed, [ + { lineIndex: 0, type: "START", title: "Outer" }, + { lineIndex: 4, type: "END" }, + ]); + + // Both START and END boundary lines are consumed (not shown). + expect(result).toHaveLength(1); + const outer = result[0] as ConsoleSectionGroup; + expect(outer.title).toBe("Outer"); + expect(outer.endIndex).toBe(4); + // The marker group should be nested inside the annotator group + const markerGroup = outer.children.find((c) => c.kind === "group"); + expect(markerGroup).toBeDefined(); + expect((markerGroup as ConsoleSectionGroup).title).toBe("Build"); + }); + + it("uses default title when title is missing", () => { + const lines = ["line 0", "line 1"]; + const flat = parseConsoleSections(lines); + const result = applyAnnotatorBoundaries(flat, [ + { lineIndex: 0, type: "START" }, + ]); + + const group = result[0] as ConsoleSectionGroup; + expect(group.title).toBe("Section"); + }); +}); diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts index 13c0fb359..3615cf0e9 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts @@ -67,9 +67,8 @@ const GROUP_MARKER_RE = /##\[group\]|::group::/; */ export function parseConsoleSections(lines: string[]): ConsoleSectionNode[] { const root: ConsoleSectionNode[] = []; - // Stack of open groups: each entry is the children array of the current group. - const stack: { group: ConsoleSectionGroup; parent: ConsoleSectionNode[] }[] = - []; + // Stack of open groups. + const stack: { group: ConsoleSectionGroup }[] = []; function current(): ConsoleSectionNode[] { return stack.length > 0 ? stack[stack.length - 1].group.children : root; @@ -99,7 +98,7 @@ export function parseConsoleSections(lines: string[]): ConsoleSectionNode[] { children: [], }; current().push(group); - stack.push({ group, parent: current() }); + stack.push({ group }); continue; } @@ -188,7 +187,7 @@ export function applyRulesToSections( continue; } - const stripped = node.content.replace(ANSI_RE, "").trimStart(); + const stripped = stripForDetection(node.content); // Check if current line ends an active rule-based group. if (activeRule && activeGroup && activeRule.endPattern.test(stripped)) { @@ -261,3 +260,87 @@ function matchAnyRule( } return null; } + +/** + * Apply server-side annotator boundary events to an existing node tree. + * + * Boundary events reference plain-text line indices from the backend. + * Frontend lines come from progressiveHtml (HTML), so indices may not + * align 1:1 if Jenkins injects extra HTML-only lines. We use a + * best-effort match: the boundary's lineIndex maps to the Nth + * ConsoleSectionLine node (by its `index` field). + * + * Only flat lines are grouped; already-grouped nodes are passed through. + */ +export function applyAnnotatorBoundaries( + nodes: ConsoleSectionNode[], + boundaries: Array<{ + lineIndex: number; + type: "START" | "END"; + title?: string; + }>, +): ConsoleSectionNode[] { + if (boundaries.length === 0) return nodes; + + // Build a set of line indices for quick lookup. + const startMap = new Map(); + const endSet = new Set(); + for (const b of boundaries) { + if (b.type === "START") { + startMap.set(b.lineIndex, b.title ?? "Section"); + } else { + endSet.add(b.lineIndex); + } + } + + const result: ConsoleSectionNode[] = []; + let activeGroup: ConsoleSectionGroup | null = null; + + for (const node of nodes) { + // Pass existing groups through unchanged. + if (node.kind === "group") { + if (activeGroup) { + activeGroup.children.push(node); + } else { + result.push(node); + } + continue; + } + + const lineIdx = node.index; + + // Check end before start so a boundary that closes and opens + // on the same line works correctly. + if (activeGroup && endSet.has(lineIdx)) { + activeGroup.endIndex = lineIdx; + result.push(activeGroup); + activeGroup = null; + continue; + } + + const startTitle = startMap.get(lineIdx); + if (startTitle) { + activeGroup = { + kind: "group", + title: startTitle, + startIndex: lineIdx, + endIndex: -1, + children: [], + }; + continue; + } + + if (activeGroup) { + activeGroup.children.push(node); + } else { + result.push(node); + } + } + + // Close any unclosed annotator group. + if (activeGroup) { + result.push(activeGroup); + } + + return result; +} diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotator.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotator.java index bcb6db517..c96b7308a 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotator.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotator.java @@ -1,6 +1,7 @@ package io.jenkins.plugins.pipelinegraphview.consoleview; import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; import hudson.ExtensionList; import hudson.ExtensionPoint; @@ -65,10 +66,14 @@ public boolean isEnabledByDefault() { *

Called once per line, in order. The annotator may maintain * internal state across calls within a single log stream. * + *

Implementations must never return {@code null}; return + * {@link SectionBoundary#NONE} if this line does not start or end + * a section. + * * @param line the raw console output line (may contain ANSI escapes) * @return a boundary event, or {@link SectionBoundary#NONE} if no transition */ - @CheckForNull + @NonNull public abstract SectionBoundary detect(String line); /** diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionProcessor.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionProcessor.java new file mode 100644 index 000000000..fa7609864 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionProcessor.java @@ -0,0 +1,91 @@ +package io.jenkins.plugins.pipelinegraphview.consoleview; + +import edu.umd.cs.findbugs.annotations.CheckForNull; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Processes console log text through registered {@link ConsoleSectionAnnotator} + * instances and collects section boundary events. + */ +public class ConsoleSectionProcessor { + + private final List annotators; + + public ConsoleSectionProcessor(List annotators) { + // Filter to only enabled annotators. + List enabled = new ArrayList<>(); + for (ConsoleSectionAnnotator a : annotators) { + if (a.isEnabledByDefault()) { + enabled.add(a); + } + } + this.annotators = Collections.unmodifiableList(enabled); + } + + /** + * Process raw log text and return boundary events. + * + * @param logText raw plain-text log output (not HTML) + * @return ordered list of boundary events with line indices + */ + public List process(String logText) { + if (annotators.isEmpty() || logText.isEmpty()) { + return Collections.emptyList(); + } + + for (ConsoleSectionAnnotator a : annotators) { + a.reset(); + } + + String[] lines = logText.split("\n", -1); + List events = new ArrayList<>(); + + for (int i = 0; i < lines.length; i++) { + String line = lines[i]; + for (ConsoleSectionAnnotator annotator : annotators) { + ConsoleSectionAnnotator.SectionBoundary boundary = annotator.detect(line); + if (boundary.getType() != ConsoleSectionAnnotator.SectionBoundary.Type.NONE) { + events.add(new BoundaryEvent( + i, + boundary.getType() == ConsoleSectionAnnotator.SectionBoundary.Type.START ? "START" : "END", + boundary.getTitle())); + // First annotator to match wins for this line. + break; + } + } + } + + return events; + } + + /** + * A single section boundary event at a specific line index. + */ + public static final class BoundaryEvent { + private final int lineIndex; + private final String type; + private final String title; + + public BoundaryEvent(int lineIndex, String type, @CheckForNull String title) { + this.lineIndex = lineIndex; + this.type = type; + this.title = title; + } + + public int getLineIndex() { + return lineIndex; + } + + /** "START" or "END". */ + public String getType() { + return type; + } + + @CheckForNull + public String getTitle() { + return title; + } + } +} diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionRule.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionRule.java index 91a912c49..986348c18 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionRule.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionRule.java @@ -39,13 +39,17 @@ public abstract class ConsoleSectionRule implements ExtensionPoint { public abstract String getDisplayName(); /** - * Java regex pattern that matches the start of a collapsible section. + * ECMAScript-compatible regex pattern that matches the start of a collapsible section. + * These patterns are compiled on the frontend with {@code new RegExp(...)}, so they must + * use ECMAScript regex syntax (avoid Java-only features like possessive quantifiers or + * {@code \p{}} Unicode categories). * The first capture group, if present, is used as the section title. */ public abstract String getStartPattern(); /** - * Java regex pattern that matches the end of a collapsible section. + * ECMAScript-compatible regex pattern that matches the end of a collapsible section. + * See {@link #getStartPattern()} for syntax requirements. */ public abstract String getEndPattern(); diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/MarkerConsoleSectionAnnotator.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/MarkerConsoleSectionAnnotator.java index f99bd2586..389501d32 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/MarkerConsoleSectionAnnotator.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/MarkerConsoleSectionAnnotator.java @@ -1,6 +1,5 @@ package io.jenkins.plugins.pipelinegraphview.consoleview; -import hudson.Extension; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -10,8 +9,11 @@ * *

This is the server-side counterpart to the frontend marker parser * in {@code parseConsoleSections.ts}. Both must agree on the syntax. + * + *

Not registered as an {@code @Extension} because the frontend already + * handles these markers directly. This class exists for third-party code + * that may instantiate it explicitly. */ -@Extension public class MarkerConsoleSectionAnnotator extends ConsoleSectionAnnotator { // Same ANSI stripping as the frontend. diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction.java index e0b502d35..f21ddc26b 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction.java @@ -26,6 +26,7 @@ import io.jenkins.plugins.pipelinegraphview.utils.PipelineStepApi; import io.jenkins.plugins.pipelinegraphview.utils.PipelineStepList; import jakarta.servlet.ServletException; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -407,6 +408,61 @@ public void getConsoleSectionRules(StaplerRequest2 req, StaplerResponse2 rsp) th net.sf.json.JSONArray.fromObject(rules).write(rsp.getWriter()); } + @GET + @WebMethod(name = "consoleSectionBoundaries") + @SuppressWarnings("ResultOfMethodCallIgnored") + @SuppressFBWarnings( + value = "RV_RETURN_VALUE_IGNORED", + justification = "writeLogTo return value not needed; we only care about the buffered output") + public void getConsoleSectionBoundaries(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException { + run.checkPermission(Item.READ); + String nodeId = req.getParameter("nodeId"); + if (nodeId == null) { + rsp.setStatus(400); + rsp.setContentType("application/json;charset=UTF-8"); + rsp.getWriter().write("[]"); + return; + } + + AnnotatedLargeText logText = getLogForNode(nodeId); + if (logText == null) { + rsp.setStatus(200); + rsp.setContentType("application/json;charset=UTF-8"); + rsp.setHeader("Cache-Control", "private, no-store"); + rsp.getWriter().write("[]"); + return; + } + + // Read raw plain text (not HTML) - annotators expect undecorated output. + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + logText.writeLogTo(0L, baos); + String rawLog = baos.toString("UTF-8"); + + ConsoleSectionProcessor processor = new ConsoleSectionProcessor( + ConsoleSectionAnnotator.all().stream().toList()); + List events = processor.process(rawLog); + + rsp.setStatus(200); + rsp.setContentType("application/json;charset=UTF-8"); + if (logText.isComplete()) { + rsp.setHeader("Cache-Control", "private, max-age=300"); + } else { + rsp.setHeader("Cache-Control", "private, no-store"); + } + + List jsonEvents = new ArrayList<>(); + for (ConsoleSectionProcessor.BoundaryEvent event : events) { + JSONObject obj = new JSONObject(); + obj.put("lineIndex", event.getLineIndex()); + obj.put("type", event.getType()); + if (event.getTitle() != null) { + obj.put("title", event.getTitle()); + } + jsonEvents.add(obj); + } + net.sf.json.JSONArray.fromObject(jsonEvents).write(rsp.getWriter()); + } + @GET @WebMethod(name = "tree") public void getTree(StaplerRequest2 req, StaplerResponse2 rsp) throws IOException, ServletException { diff --git a/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotatorTest.java b/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotatorTest.java index 2c77ca681..64f6f7eee 100644 --- a/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotatorTest.java +++ b/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotatorTest.java @@ -12,12 +12,14 @@ class ConsoleSectionAnnotatorTest { @Test - void markerAnnotatorIsDiscovered(JenkinsRule j) { + void markerAnnotatorIsNotAutoDiscovered(JenkinsRule j) { + // MarkerConsoleSectionAnnotator no longer has @Extension because the + // frontend handles markers directly. Verify it does NOT appear in all(). List annotators = ConsoleSectionAnnotator.all().stream().toList(); List ids = annotators.stream().map(ConsoleSectionAnnotator::getId).toList(); - assertThat(ids, hasItem("markers")); + assertThat(ids, not(hasItem("markers"))); } @Test diff --git a/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionProcessorTest.java b/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionProcessorTest.java new file mode 100644 index 000000000..42a51b464 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionProcessorTest.java @@ -0,0 +1,162 @@ +package io.jenkins.plugins.pipelinegraphview.consoleview; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +import java.util.List; +import org.junit.jupiter.api.Test; + +class ConsoleSectionProcessorTest { + + @Test + void emptyLogProducesNoEvents() { + MarkerConsoleSectionAnnotator annotator = new MarkerConsoleSectionAnnotator(); + ConsoleSectionProcessor processor = new ConsoleSectionProcessor(List.of(annotator)); + List events = processor.process(""); + assertThat(events, is(empty())); + } + + @Test + void noAnnotatorsProducesNoEvents() { + ConsoleSectionProcessor processor = new ConsoleSectionProcessor(List.of()); + List events = processor.process("hello\nworld"); + assertThat(events, is(empty())); + } + + @Test + void detectsGroupMarkers() { + MarkerConsoleSectionAnnotator annotator = new MarkerConsoleSectionAnnotator(); + ConsoleSectionProcessor processor = new ConsoleSectionProcessor(List.of(annotator)); + + String log = "line 0\n##[group]Build\ncompiling...\n##[endgroup]\nline 4"; + List events = processor.process(log); + + assertThat(events, hasSize(2)); + + assertThat(events.get(0).getLineIndex(), is(1)); + assertThat(events.get(0).getType(), is("START")); + assertThat(events.get(0).getTitle(), is("Build")); + + assertThat(events.get(1).getLineIndex(), is(3)); + assertThat(events.get(1).getType(), is("END")); + assertThat(events.get(1).getTitle(), nullValue()); + } + + @Test + void detectsGitHubActionsMarkers() { + MarkerConsoleSectionAnnotator annotator = new MarkerConsoleSectionAnnotator(); + ConsoleSectionProcessor processor = new ConsoleSectionProcessor(List.of(annotator)); + + String log = "::group::Test Suite\nrunning tests\n::endgroup::"; + List events = processor.process(log); + + assertThat(events, hasSize(2)); + assertThat(events.get(0).getType(), is("START")); + assertThat(events.get(0).getTitle(), is("Test Suite")); + assertThat(events.get(1).getType(), is("END")); + } + + @Test + void skipsDisabledAnnotators() { + ConsoleSectionAnnotator disabled = new ConsoleSectionAnnotator() { + @Override + public String getId() { + return "disabled"; + } + + @Override + public String getDisplayName() { + return "Disabled"; + } + + @Override + public boolean isEnabledByDefault() { + return false; + } + + @Override + public SectionBoundary detect(String line) { + return SectionBoundary.start("Should not appear"); + } + }; + + ConsoleSectionProcessor processor = new ConsoleSectionProcessor(List.of(disabled)); + List events = processor.process("any line"); + assertThat(events, is(empty())); + } + + @Test + void firstAnnotatorWinsOnSameLine() { + ConsoleSectionAnnotator first = new ConsoleSectionAnnotator() { + @Override + public String getId() { + return "first"; + } + + @Override + public String getDisplayName() { + return "First"; + } + + @Override + public SectionBoundary detect(String line) { + return line.contains("match") ? SectionBoundary.start("First") : SectionBoundary.NONE; + } + }; + + ConsoleSectionAnnotator second = new ConsoleSectionAnnotator() { + @Override + public String getId() { + return "second"; + } + + @Override + public String getDisplayName() { + return "Second"; + } + + @Override + public SectionBoundary detect(String line) { + return line.contains("match") ? SectionBoundary.start("Second") : SectionBoundary.NONE; + } + }; + + ConsoleSectionProcessor processor = new ConsoleSectionProcessor(List.of(first, second)); + List events = processor.process("match"); + + assertThat(events, hasSize(1)); + assertThat(events.get(0).getTitle(), is("First")); + } + + @Test + void resetIsCalledBeforeProcessing() { + int[] resetCount = {0}; + ConsoleSectionAnnotator counting = new ConsoleSectionAnnotator() { + @Override + public String getId() { + return "counting"; + } + + @Override + public String getDisplayName() { + return "Counting"; + } + + @Override + public void reset() { + resetCount[0]++; + } + + @Override + public SectionBoundary detect(String line) { + return SectionBoundary.NONE; + } + }; + + ConsoleSectionProcessor processor = new ConsoleSectionProcessor(List.of(counting)); + processor.process("line 1\nline 2"); + processor.process("line 3"); + + assertThat(resetCount[0], is(2)); + } +} From 9086ec481749fc9ebce73193fef8344dd44f44dd Mon Sep 17 00:00:00 2001 From: Aditya Jalkhare Date: Tue, 12 May 2026 20:14:10 +0530 Subject: [PATCH 16/21] address comments after copilot review --- .../pipeline-console/main/ConsoleSection.tsx | 1 + .../main/parseConsoleSections.ts | 9 +- .../consoleview/ConsoleSectionAnnotator.java | 6 +- .../consoleview/ConsoleSectionProcessor.java | 83 +++++++++++++++---- .../PipelineConsoleViewAction.java | 6 +- 5 files changed, 83 insertions(+), 22 deletions(-) diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.tsx index 9ed5c4751..0b69b494e 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.tsx +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.tsx @@ -88,6 +88,7 @@ export const ConsoleSection = memo(function ConsoleSection({ startByte={startByte} stopTailingLogs={stopTailingLogs} currentRunPath={currentRunPath} + buildStep={buildStep} /> ))}

diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts index 3615cf0e9..a367c509e 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts @@ -141,10 +141,13 @@ export function compileSectionRules( displayName: string; startPattern: string; endPattern: string; + enabledByDefault?: boolean; }>, ): CompiledSectionRule[] { const compiled: CompiledSectionRule[] = []; for (const rule of rules) { + // Skip rules that are explicitly disabled by default. + if (rule.enabledByDefault === false) continue; try { compiled.push({ id: rule.id, @@ -266,9 +269,9 @@ function matchAnyRule( * * Boundary events reference plain-text line indices from the backend. * Frontend lines come from progressiveHtml (HTML), so indices may not - * align 1:1 if Jenkins injects extra HTML-only lines. We use a - * best-effort match: the boundary's lineIndex maps to the Nth - * ConsoleSectionLine node (by its `index` field). + * align 1:1 if Jenkins injects extra HTML-only lines. Boundaries are + * matched only to ConsoleSectionLine nodes whose `index` exactly equals + * the boundary's `lineIndex`; boundaries with no exact match are ignored. * * Only flat lines are grouped; already-grouped nodes are passed through. */ diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotator.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotator.java index c96b7308a..2b49cb4ce 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotator.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotator.java @@ -14,8 +14,10 @@ * titles, dynamic enable/disable conditions). * *

Implementations receive lines one at a time via {@link #detect(String)} - * and return section boundary events. State is maintained per annotator - * instance; a fresh instance is created for each log stream. + * and return section boundary events. Annotator instances from {@code all()} + * are shared singletons; the processor calls {@link #reset()} before each + * log stream and synchronizes access. Implementations should keep state + * in instance fields reset by {@link #reset()}. * *

Example: *

diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionProcessor.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionProcessor.java
index fa7609864..2d6b3627e 100644
--- a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionProcessor.java
+++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionProcessor.java
@@ -1,6 +1,11 @@
 package io.jenkins.plugins.pipelinegraphview.consoleview;
 
 import edu.umd.cs.findbugs.annotations.CheckForNull;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -8,6 +13,10 @@
 /**
  * Processes console log text through registered {@link ConsoleSectionAnnotator}
  * instances and collects section boundary events.
+ *
+ * 

Annotator instances may be shared singletons (e.g. from + * {@code ExtensionList}). All access to annotator state ({@code reset()}, + * {@code detect()}) is synchronized so concurrent requests are safe. */ public class ConsoleSectionProcessor { @@ -35,24 +44,27 @@ public List process(String logText) { return Collections.emptyList(); } - for (ConsoleSectionAnnotator a : annotators) { - a.reset(); - } - String[] lines = logText.split("\n", -1); List events = new ArrayList<>(); - for (int i = 0; i < lines.length; i++) { - String line = lines[i]; - for (ConsoleSectionAnnotator annotator : annotators) { - ConsoleSectionAnnotator.SectionBoundary boundary = annotator.detect(line); - if (boundary.getType() != ConsoleSectionAnnotator.SectionBoundary.Type.NONE) { - events.add(new BoundaryEvent( - i, - boundary.getType() == ConsoleSectionAnnotator.SectionBoundary.Type.START ? "START" : "END", - boundary.getTitle())); - // First annotator to match wins for this line. - break; + synchronized (this) { + for (ConsoleSectionAnnotator a : annotators) { + a.reset(); + } + + for (int i = 0; i < lines.length; i++) { + String line = lines[i]; + for (ConsoleSectionAnnotator annotator : annotators) { + ConsoleSectionAnnotator.SectionBoundary boundary = annotator.detect(line); + if (boundary.getType() != ConsoleSectionAnnotator.SectionBoundary.Type.NONE) { + events.add(new BoundaryEvent( + i, + boundary.getType() == ConsoleSectionAnnotator.SectionBoundary.Type.START + ? "START" + : "END", + boundary.getTitle())); + break; + } } } } @@ -60,6 +72,47 @@ public List process(String logText) { return events; } + /** + * Process log content from an input stream, reading line by line to avoid + * buffering the entire log as a single String. + * + * @param input stream of raw plain-text log output (UTF-8) + * @return ordered list of boundary events with line indices + */ + public List process(InputStream input) throws IOException { + if (annotators.isEmpty()) { + return Collections.emptyList(); + } + + List events = new ArrayList<>(); + synchronized (this) { + for (ConsoleSectionAnnotator a : annotators) { + a.reset(); + } + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { + String line; + int lineIndex = 0; + while ((line = reader.readLine()) != null) { + for (ConsoleSectionAnnotator annotator : annotators) { + ConsoleSectionAnnotator.SectionBoundary boundary = annotator.detect(line); + if (boundary.getType() != ConsoleSectionAnnotator.SectionBoundary.Type.NONE) { + events.add(new BoundaryEvent( + lineIndex, + boundary.getType() == ConsoleSectionAnnotator.SectionBoundary.Type.START + ? "START" + : "END", + boundary.getTitle())); + break; + } + } + lineIndex++; + } + } + } + return events; + } + /** * A single section boundary event at a specific line index. */ diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction.java index f21ddc26b..6019e412a 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction.java @@ -434,13 +434,15 @@ public void getConsoleSectionBoundaries(StaplerRequest2 req, StaplerResponse2 rs } // Read raw plain text (not HTML) - annotators expect undecorated output. + // We buffer into a byte array because writeLogTo requires an OutputStream, + // then stream line-by-line through the processor to avoid a second full copy. ByteArrayOutputStream baos = new ByteArrayOutputStream(); logText.writeLogTo(0L, baos); - String rawLog = baos.toString("UTF-8"); ConsoleSectionProcessor processor = new ConsoleSectionProcessor( ConsoleSectionAnnotator.all().stream().toList()); - List events = processor.process(rawLog); + List events = + processor.process(new java.io.ByteArrayInputStream(baos.toByteArray())); rsp.setStatus(200); rsp.setContentType("application/json;charset=UTF-8"); From b6140c502ab4a9efe1537add491b52ecd6e9d4c1 Mon Sep 17 00:00:00 2001 From: Aditya Jalkhare Date: Sun, 17 May 2026 17:12:35 +0530 Subject: [PATCH 17/21] Address the Copilot review comments - - Introduced logComplete state in ConsoleLogStream to manage log tailing. - Updated dependencies in ConsoleSection to use a div for section titles. - Modified console-section.scss to set title display to inline. - Made ConsoleSectionAnnotator cloneable for per-request isolation. - Streamlined log processing in ConsoleSectionProcessor to handle input streams directly. - Improved PipelineConsoleViewAction to process logs without full buffering. --- .../main/ConsoleLogStream.tsx | 3 +- .../pipeline-console/main/ConsoleSection.tsx | 4 +- .../main/console-section.scss | 1 + .../consoleview/ConsoleSectionAnnotator.java | 23 ++- .../consoleview/ConsoleSectionProcessor.java | 153 +++++++++++++----- .../PipelineConsoleViewAction.java | 15 +- 6 files changed, 139 insertions(+), 60 deletions(-) diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.tsx index 13215ab41..1c744936c 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.tsx +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleLogStream.tsx @@ -120,6 +120,7 @@ export default function ConsoleLogStream({ }, [currentRunPath]); const [boundaries, setBoundaries] = useState([]); + const logComplete = logBuffer.stopTailing ?? false; useEffect(() => { let cancelled = false; getConsoleSectionBoundaries(currentRunPath, stepId) @@ -133,7 +134,7 @@ export default function ConsoleLogStream({ return () => { cancelled = true; }; - }, [currentRunPath, stepId]); + }, [currentRunPath, stepId, logComplete]); const sectionTree = useMemo( () => diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.tsx b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.tsx index 0b69b494e..6bd8403ef 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.tsx +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/ConsoleSection.tsx @@ -67,12 +67,12 @@ export const ConsoleSection = memo(function ConsoleSection({ d="M184 112l144 144-144 144" /> - +

{makeReactChildren( tokenizeANSIString(group.title), `section-title-${group.startIndex}`, )} - +
{group.children.length}{" "} {group.children.length === 1 ? "line" : "lines"} diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/console-section.scss b/src/main/frontend/pipeline-console-view/pipeline-console/main/console-section.scss index d8f5d96b5..5b8185b85 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/console-section.scss +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/console-section.scss @@ -51,6 +51,7 @@ } .pgv-console-section__title { + display: inline; font-weight: var(--font-bold-weight); color: var(--text-color); } diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotator.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotator.java index 2b49cb4ce..d5e4a104a 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotator.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionAnnotator.java @@ -15,9 +15,11 @@ * *

Implementations receive lines one at a time via {@link #detect(String)} * and return section boundary events. Annotator instances from {@code all()} - * are shared singletons; the processor calls {@link #reset()} before each - * log stream and synchronizes access. Implementations should keep state - * in instance fields reset by {@link #reset()}. + * are shared singletons. The processor clones each annotator via + * {@link #clone()} so shared instances are never mutated concurrently, + * then calls {@link #reset()} before processing each log stream. + * Implementations should keep state in instance fields reset by + * {@link #reset()}. * *

Example: *

@@ -43,7 +45,7 @@
  * }
  * 
*/ -public abstract class ConsoleSectionAnnotator implements ExtensionPoint { +public abstract class ConsoleSectionAnnotator implements ExtensionPoint, Cloneable { /** * Unique identifier for this annotator. @@ -85,6 +87,19 @@ public void reset() { // Default no-op; subclasses override as needed. } + /** + * Create a shallow copy of this annotator for per-request isolation. + * Subclasses with complex state should override this. + */ + @Override + public ConsoleSectionAnnotator clone() { + try { + return (ConsoleSectionAnnotator) super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError("ConsoleSectionAnnotator must be Cloneable", e); + } + } + /** * All registered annotators. */ diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionProcessor.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionProcessor.java index 2d6b3627e..ec8afca73 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionProcessor.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/ConsoleSectionProcessor.java @@ -2,9 +2,11 @@ import edu.umd.cs.findbugs.annotations.CheckForNull; import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; @@ -14,23 +16,24 @@ * Processes console log text through registered {@link ConsoleSectionAnnotator} * instances and collects section boundary events. * - *

Annotator instances may be shared singletons (e.g. from - * {@code ExtensionList}). All access to annotator state ({@code reset()}, - * {@code detect()}) is synchronized so concurrent requests are safe. + *

A fresh processor is created per request. It clones each annotator so + * shared singleton instances from {@code ExtensionList} are never mutated + * concurrently. */ public class ConsoleSectionProcessor { private final List annotators; public ConsoleSectionProcessor(List annotators) { - // Filter to only enabled annotators. - List enabled = new ArrayList<>(); + // Filter to only enabled annotators and clone each so shared singletons + // are never mutated by concurrent requests. + List cloned = new ArrayList<>(); for (ConsoleSectionAnnotator a : annotators) { if (a.isEnabledByDefault()) { - enabled.add(a); + cloned.add(a.clone()); } } - this.annotators = Collections.unmodifiableList(enabled); + this.annotators = Collections.unmodifiableList(cloned); } /** @@ -43,22 +46,40 @@ public List process(String logText) { if (annotators.isEmpty() || logText.isEmpty()) { return Collections.emptyList(); } + try { + return process(new java.io.ByteArrayInputStream(logText.getBytes(StandardCharsets.UTF_8))); + } catch (IOException e) { + // ByteArrayInputStream never throws IOException in practice. + return Collections.emptyList(); + } + } - String[] lines = logText.split("\n", -1); - List events = new ArrayList<>(); + /** + * Process log content from an input stream, reading line by line to avoid + * buffering the entire log as a single String. + * + * @param input stream of raw plain-text log output (UTF-8) + * @return ordered list of boundary events with line indices + */ + public List process(InputStream input) throws IOException { + if (annotators.isEmpty()) { + return Collections.emptyList(); + } - synchronized (this) { - for (ConsoleSectionAnnotator a : annotators) { - a.reset(); - } + List events = new ArrayList<>(); + for (ConsoleSectionAnnotator a : annotators) { + a.reset(); + } - for (int i = 0; i < lines.length; i++) { - String line = lines[i]; + try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { + String line; + int lineIndex = 0; + while ((line = reader.readLine()) != null) { for (ConsoleSectionAnnotator annotator : annotators) { ConsoleSectionAnnotator.SectionBoundary boundary = annotator.detect(line); if (boundary.getType() != ConsoleSectionAnnotator.SectionBoundary.Type.NONE) { events.add(new BoundaryEvent( - i, + lineIndex, boundary.getType() == ConsoleSectionAnnotator.SectionBoundary.Type.START ? "START" : "END", @@ -66,51 +87,95 @@ public List process(String logText) { break; } } + lineIndex++; } } - return events; } /** - * Process log content from an input stream, reading line by line to avoid - * buffering the entire log as a single String. + * Creates an OutputStream that processes log lines incrementally as bytes + * are written. Call {@link LineProcessingOutputStream#getEvents()} after + * writing is complete to retrieve the collected boundary events. * - * @param input stream of raw plain-text log output (UTF-8) - * @return ordered list of boundary events with line indices + *

This avoids buffering the entire log in memory - lines are processed + * and discarded as they arrive. */ - public List process(InputStream input) throws IOException { - if (annotators.isEmpty()) { - return Collections.emptyList(); - } + public LineProcessingOutputStream createOutputStream() { + return new LineProcessingOutputStream(); + } - List events = new ArrayList<>(); - synchronized (this) { + /** + * An OutputStream that feeds lines directly into the processor's annotators + * as they are written, avoiding a full in-memory buffer of the log. + */ + public class LineProcessingOutputStream extends OutputStream { + private final ByteArrayOutputStream lineBuffer = new ByteArrayOutputStream(512); + private final List events = new ArrayList<>(); + private int lineIndex = 0; + + private LineProcessingOutputStream() { for (ConsoleSectionAnnotator a : annotators) { a.reset(); } + } - try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) { - String line; - int lineIndex = 0; - while ((line = reader.readLine()) != null) { - for (ConsoleSectionAnnotator annotator : annotators) { - ConsoleSectionAnnotator.SectionBoundary boundary = annotator.detect(line); - if (boundary.getType() != ConsoleSectionAnnotator.SectionBoundary.Type.NONE) { - events.add(new BoundaryEvent( - lineIndex, - boundary.getType() == ConsoleSectionAnnotator.SectionBoundary.Type.START - ? "START" - : "END", - boundary.getTitle())); - break; - } + @Override + public void write(int b) throws IOException { + if (b == '\n') { + processLine(); + } else { + lineBuffer.write(b); + } + } + + @Override + public void write(byte[] buf, int off, int len) throws IOException { + int end = off + len; + for (int i = off; i < end; i++) { + if (buf[i] == '\n') { + // Flush everything before the newline as part of the current line + if (i > off) { + lineBuffer.write(buf, off, i - off); } - lineIndex++; + processLine(); + off = i + 1; } } + // Buffer remaining bytes (partial line) + if (off < end) { + lineBuffer.write(buf, off, end - off); + } + } + + @Override + public void close() throws IOException { + // Process any remaining partial line (no trailing newline) + if (lineBuffer.size() > 0) { + processLine(); + } + } + + private void processLine() { + String line = lineBuffer.toString(StandardCharsets.UTF_8); + lineBuffer.reset(); + for (ConsoleSectionAnnotator annotator : annotators) { + ConsoleSectionAnnotator.SectionBoundary boundary = annotator.detect(line); + if (boundary.getType() != ConsoleSectionAnnotator.SectionBoundary.Type.NONE) { + events.add(new BoundaryEvent( + lineIndex, + boundary.getType() == ConsoleSectionAnnotator.SectionBoundary.Type.START ? "START" : "END", + boundary.getTitle())); + break; + } + } + lineIndex++; + } + + /** Returns the boundary events collected during writing. */ + public List getEvents() { + return Collections.unmodifiableList(events); } - return events; } /** diff --git a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction.java b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction.java index 6019e412a..49f6c4836 100644 --- a/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction.java +++ b/src/main/java/io/jenkins/plugins/pipelinegraphview/consoleview/PipelineConsoleViewAction.java @@ -26,7 +26,6 @@ import io.jenkins.plugins.pipelinegraphview.utils.PipelineStepApi; import io.jenkins.plugins.pipelinegraphview.utils.PipelineStepList; import jakarta.servlet.ServletException; -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -433,16 +432,14 @@ public void getConsoleSectionBoundaries(StaplerRequest2 req, StaplerResponse2 rs return; } - // Read raw plain text (not HTML) - annotators expect undecorated output. - // We buffer into a byte array because writeLogTo requires an OutputStream, - // then stream line-by-line through the processor to avoid a second full copy. - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - logText.writeLogTo(0L, baos); - + // Stream log directly through the processor line-by-line to avoid + // buffering the entire log in memory. ConsoleSectionProcessor processor = new ConsoleSectionProcessor( ConsoleSectionAnnotator.all().stream().toList()); - List events = - processor.process(new java.io.ByteArrayInputStream(baos.toByteArray())); + ConsoleSectionProcessor.LineProcessingOutputStream out = processor.createOutputStream(); + logText.writeLogTo(0L, out); + out.close(); + List events = out.getEvents(); rsp.setStatus(200); rsp.setContentType("application/json;charset=UTF-8"); From 52693d29c18d3e242943191827e8ae8ce319ba23 Mon Sep 17 00:00:00 2001 From: Aditya Jalkhare Date: Tue, 26 May 2026 11:24:33 +0530 Subject: [PATCH 18/21] Rerun CI From 2c759f2c708b706805d66205dba4fb3419c5961e Mon Sep 17 00:00:00 2001 From: Aditya Jalkhare Date: Sat, 30 May 2026 10:25:10 +0530 Subject: [PATCH 19/21] add capability to work when timestamper plugin is enabled --- .../main/parseConsoleSections.spec.ts | 62 ++++++++ .../main/parseConsoleSections.ts | 150 ++++++++---------- 2 files changed, 126 insertions(+), 86 deletions(-) diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts index a3d9f9944..621a93651 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts @@ -240,6 +240,68 @@ describe("parseConsoleSections", () => { }); }); + describe("Timestamper plugin compatibility", () => { + it.each([ + [ + "plain HH:MM:SS with ##[group]", + "11:18:37 ##[group]Compile", + "11:18:37 content", + "11:18:37 ##[endgroup]", + ], + [ + "plain HH:MM:SS with ::group::", + "09:05:12 ::group::Docker Build", + "09:05:12 Step 1/3 : FROM node", + "09:05:13 ::endgroup::", + ], + [ + "full date+time prefix", + "2026-05-30 11:18:37 ##[group]Build", + "2026-05-30 11:18:37 compiling...", + "2026-05-30 11:18:38 ##[endgroup]", + ], + [ + "timestamp with milliseconds", + "11:18:37.456 ##[group]Tests", + "11:18:37.500 running...", + "11:18:38.001 ##[endgroup]", + ], + [ + "HTML-wrapped timestamp", + '11:18:37 ##[group]Build', + '11:18:37 compiling...', + '11:18:37 ##[endgroup]', + ], + [ + "bracketed ISO format", + "[2026-05-30T11:18:37.456Z] ##[group]Deploy", + "[2026-05-30T11:18:37.456Z] deploying...", + "[2026-05-30T11:18:38.001Z] ##[endgroup]", + ], + [ + "bracketed ISO with timezone offset", + "[2026-05-30T11:18:37.456+0530] ::group::Build", + "[2026-05-30T11:18:37.500+0530] compiling...", + "[2026-05-30T11:18:38.001+0530] ::endgroup::", + ], + [ + "real Timestamper: visible clock + hidden ISO (dual prefix)", + '01:08:06 [2026-05-29T19:38:06.669Z] ##[group]Compile All Modules', + '01:08:06 [2026-05-29T19:38:06.669Z] Starting compilation...', + '01:08:06 [2026-05-29T19:38:06.669Z] ##[endgroup]', + ], + ])( + "detects markers with %s", + (_label, startLine, contentLine, endLine) => { + const result = parseConsoleSections([startLine, contentLine, endLine]); + expect(result).toHaveLength(1); + const group = result[0] as ConsoleSectionGroup; + expect(group.kind).toBe("group"); + expect(group.children).toHaveLength(1); + }, + ); + }); + describe("nesting", () => { it("supports nested groups", () => { const lines = [ diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts index a367c509e..3928f58d6 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts @@ -38,6 +38,15 @@ const ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]/g; // progressiveHtml returns HTML; AnsiColor plugin wraps ANSI codes in tags. const HTML_TAG_RE = /<[^>]*>/g; +// Timestamp prefix pattern for the Timestamper plugin. +// After HTML tags are stripped, timestamps remain as plain text. The plugin +// emits BOTH a visible clock time AND a hidden ISO timestamp per line: +// "01:08:06 [2026-05-29T19:38:06.669Z] " +// After HTML stripping: "01:08:06 [2026-05-29T19:38:06.669Z] " +// The trailing + allows matching multiple consecutive timestamp prefixes. +const TIMESTAMP_PREFIX_RE = + /^(?:\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?[A-Z]*[+-]?\d{0,4}\]\s+|(?:\d{4}[-/]\d{2}[-/]\d{2}\s+)?\d{1,2}:\d{2}(?::\d{2})?(?:\.\d+)?\s+)+/; + // Start markers: ##[group]Title or ::group::Title const GROUP_START_RE = /^(?:##\[group\]|::group::)\s*(.*)$/; @@ -50,7 +59,11 @@ const GROUP_END_RE = /^(?:##\[endgroup\]|::endgroup::)\s*$/; * in the output nodes. */ function stripForDetection(line: string): string { - return line.replace(ANSI_RE, "").replace(HTML_TAG_RE, "").trimStart(); + return line + .replace(ANSI_RE, "") + .replace(HTML_TAG_RE, "") + .replace(TIMESTAMP_PREFIX_RE, "") + .trimStart(); } // Pattern matching the group-start marker itself (no capture). @@ -67,28 +80,22 @@ const GROUP_MARKER_RE = /##\[group\]|::group::/; */ export function parseConsoleSections(lines: string[]): ConsoleSectionNode[] { const root: ConsoleSectionNode[] = []; - // Stack of open groups. - const stack: { group: ConsoleSectionGroup }[] = []; - - function current(): ConsoleSectionNode[] { - return stack.length > 0 ? stack[stack.length - 1].group.children : root; - } + const stack: ConsoleSectionGroup[] = []; + const target = () => stack.at(-1)?.children ?? root; for (let i = 0; i < lines.length; i++) { const raw = lines[i]; const stripped = stripForDetection(raw); - // Reject lines that look like shell trace of the echo command. - // e.g. "+ echo ##[group]Build" should not be treated as a marker. + // Reject shell trace lines (e.g. "+ echo ##[group]Build"). if (stripped.startsWith("+ ")) { - current().push({ kind: "line", index: i, content: raw }); + target().push({ kind: "line", index: i, content: raw }); continue; } const startMatch = GROUP_START_RE.exec(stripped); if (startMatch) { - // Strip the marker from the raw line, preserving surrounding HTML/ANSI - // so the title renderer can show colors from the AnsiColor plugin. + // Preserve surrounding HTML/ANSI for colored title rendering. const title = raw.replace(GROUP_MARKER_RE, "").trim() || "Section"; const group: ConsoleSectionGroup = { kind: "group", @@ -97,27 +104,23 @@ export function parseConsoleSections(lines: string[]): ConsoleSectionNode[] { endIndex: -1, children: [], }; - current().push(group); - stack.push({ group }); + target().push(group); + stack.push(group); continue; } if (GROUP_END_RE.test(stripped)) { if (stack.length > 0) { - stack[stack.length - 1].group.endIndex = i; - stack.pop(); - } - // If no open group, ignore the stray endgroup marker (treat as normal line). - else { - current().push({ kind: "line", index: i, content: raw }); + stack.pop()!.endIndex = i; + } else { + target().push({ kind: "line", index: i, content: raw }); } continue; } - current().push({ kind: "line", index: i, content: raw }); + target().push({ kind: "line", index: i, content: raw }); } - // Unclosed groups keep endIndex = -1 (still streaming). return root; } @@ -178,15 +181,30 @@ export function applyRulesToSections( const result: ConsoleSectionNode[] = []; let activeRule: CompiledSectionRule | null = null; let activeGroup: ConsoleSectionGroup | null = null; + const emit = (n: ConsoleSectionNode) => + activeGroup ? activeGroup.children.push(n) : result.push(n); + + function openGroup(title: string, index: number, rule: CompiledSectionRule) { + activeGroup = { + kind: "group", + title, + startIndex: index, + endIndex: -1, + children: [], + }; + activeRule = rule; + } + + function closeGroup(endIndex: number) { + activeGroup!.endIndex = endIndex; + result.push(activeGroup!); + activeGroup = null; + activeRule = null; + } for (const node of nodes) { - // Only process flat lines; pass groups through unchanged. if (node.kind === "group") { - if (activeGroup) { - activeGroup.children.push(node); - } else { - result.push(node); - } + emit(node); continue; } @@ -194,57 +212,30 @@ export function applyRulesToSections( // Check if current line ends an active rule-based group. if (activeRule && activeGroup && activeRule.endPattern.test(stripped)) { - // The end-line starts a new section of the same rule (e.g. next Maven phase). - const startMatch = matchAnyRule(stripped, rules); - if (startMatch) { - activeGroup.endIndex = node.index; - result.push(activeGroup); - activeGroup = { - kind: "group", - title: startMatch.title, - startIndex: node.index, - endIndex: -1, - children: [], - }; - activeRule = startMatch.rule; - continue; + closeGroup(node.index); + // The end-line may also start a new section (e.g. next Maven phase). + const m = matchAnyRule(stripped, rules); + if (m) { + openGroup(m.title, node.index, m.rule); + } else { + result.push(node); } - activeGroup.endIndex = node.index; - result.push(activeGroup); - activeGroup = null; - activeRule = null; - result.push(node); continue; } // Check if a new rule-based group starts. if (!activeRule) { - const startMatch = matchAnyRule(stripped, rules); - if (startMatch) { - activeGroup = { - kind: "group", - title: startMatch.title, - startIndex: node.index, - endIndex: -1, - children: [], - }; - activeRule = startMatch.rule; + const m = matchAnyRule(stripped, rules); + if (m) { + openGroup(m.title, node.index, m.rule); continue; } } - if (activeGroup) { - activeGroup.children.push(node); - } else { - result.push(node); - } - } - - // Close any unclosed rule-based group. - if (activeGroup) { - result.push(activeGroup); + emit(node); } + if (activeGroup) result.push(activeGroup); return result; } @@ -285,7 +276,6 @@ export function applyAnnotatorBoundaries( ): ConsoleSectionNode[] { if (boundaries.length === 0) return nodes; - // Build a set of line indices for quick lookup. const startMap = new Map(); const endSet = new Set(); for (const b of boundaries) { @@ -298,22 +288,18 @@ export function applyAnnotatorBoundaries( const result: ConsoleSectionNode[] = []; let activeGroup: ConsoleSectionGroup | null = null; + const emit = (n: ConsoleSectionNode) => + activeGroup ? activeGroup.children.push(n) : result.push(n); for (const node of nodes) { - // Pass existing groups through unchanged. if (node.kind === "group") { - if (activeGroup) { - activeGroup.children.push(node); - } else { - result.push(node); - } + emit(node); continue; } const lineIdx = node.index; - // Check end before start so a boundary that closes and opens - // on the same line works correctly. + // Check end before start so same-line close+open works. if (activeGroup && endSet.has(lineIdx)) { activeGroup.endIndex = lineIdx; result.push(activeGroup); @@ -333,17 +319,9 @@ export function applyAnnotatorBoundaries( continue; } - if (activeGroup) { - activeGroup.children.push(node); - } else { - result.push(node); - } - } - - // Close any unclosed annotator group. - if (activeGroup) { - result.push(activeGroup); + emit(node); } + if (activeGroup) result.push(activeGroup); return result; } From cd319d98be6ed11f9cc382e22e1162e1a5717420 Mon Sep 17 00:00:00 2001 From: Aditya Jalkhare Date: Sat, 30 May 2026 10:26:20 +0530 Subject: [PATCH 20/21] ran prettier --- .../main/parseConsoleSections.spec.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts index 621a93651..fb5f71c04 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.spec.ts @@ -290,16 +290,13 @@ describe("parseConsoleSections", () => { '01:08:06 [2026-05-29T19:38:06.669Z] Starting compilation...', '01:08:06 [2026-05-29T19:38:06.669Z] ##[endgroup]', ], - ])( - "detects markers with %s", - (_label, startLine, contentLine, endLine) => { - const result = parseConsoleSections([startLine, contentLine, endLine]); - expect(result).toHaveLength(1); - const group = result[0] as ConsoleSectionGroup; - expect(group.kind).toBe("group"); - expect(group.children).toHaveLength(1); - }, - ); + ])("detects markers with %s", (_label, startLine, contentLine, endLine) => { + const result = parseConsoleSections([startLine, contentLine, endLine]); + expect(result).toHaveLength(1); + const group = result[0] as ConsoleSectionGroup; + expect(group.kind).toBe("group"); + expect(group.children).toHaveLength(1); + }); }); describe("nesting", () => { From a45374dad1de916a9fa951dcc680d56326659b51 Mon Sep 17 00:00:00 2001 From: Aditya Jalkhare Date: Sat, 30 May 2026 13:27:07 +0530 Subject: [PATCH 21/21] fix tsc issues --- .../pipeline-console/main/parseConsoleSections.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts index 3928f58d6..53025432b 100644 --- a/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts +++ b/src/main/frontend/pipeline-console-view/pipeline-console/main/parseConsoleSections.ts @@ -81,7 +81,7 @@ const GROUP_MARKER_RE = /##\[group\]|::group::/; export function parseConsoleSections(lines: string[]): ConsoleSectionNode[] { const root: ConsoleSectionNode[] = []; const stack: ConsoleSectionGroup[] = []; - const target = () => stack.at(-1)?.children ?? root; + const target = () => stack[stack.length - 1]?.children ?? root; for (let i = 0; i < lines.length; i++) { const raw = lines[i]; @@ -211,7 +211,8 @@ export function applyRulesToSections( const stripped = stripForDetection(node.content); // Check if current line ends an active rule-based group. - if (activeRule && activeGroup && activeRule.endPattern.test(stripped)) { + const currentRule = activeRule as CompiledSectionRule | null; + if (currentRule && activeGroup && currentRule.endPattern.test(stripped)) { closeGroup(node.index); // The end-line may also start a new section (e.g. next Maven phase). const m = matchAnyRule(stripped, rules);