@next-rsc-debug/devtools 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @next-rsc-debug/devtools
2
+
3
+ React-based DevTools UI for Next RSC Debug.
4
+
5
+ ## Features
6
+
7
+ - Event timeline
8
+ - Request list
9
+ - Fetch inspector
10
+ - Warnings panel
11
+ - Connection status indicator
12
+ - Event details
13
+ - Basic filtering (All, Navigation, RSC, Fetch, Cache, Actions, Errors, Slow only, Errors only)
14
+ - Live SSE updates
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pnpm add @next-rsc-debug/devtools
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ The package ships `"use client"` directives, so you can import it directly into
25
+ an App Router Server Component page. Do **not** wrap it in `next/dynamic` with
26
+ `ssr: false` — that is not permitted inside a Server Component on Next.js 16.
27
+
28
+ ```tsx
29
+ // app/rsc-debug/page.tsx
30
+ import { DevTools } from "@next-rsc-debug/devtools";
31
+ import "@next-rsc-debug/devtools/styles.css";
32
+
33
+ export default function DebugPage() {
34
+ return <DevTools url="/api/debug-events" />;
35
+ }
36
+ ```
37
+
38
+ Pair it with the built-in events endpoint:
39
+
40
+ ```ts
41
+ // app/api/debug-events/route.ts
42
+ export { GET } from "next-rsc-debug/route";
43
+ ```
44
+
45
+ > Avoid naming the page folder with a leading underscore (for example
46
+ > `__next-rsc-debug`). App Router treats underscore-prefixed folders as private
47
+ > and they are not routable, so the page would return 404. Use a path such as
48
+ > `/rsc-debug` instead.
49
+
50
+ ## License
51
+
52
+ MIT
@@ -0,0 +1,7 @@
1
+ export interface DevToolsProps {
2
+ url?: string;
3
+ defaultThreshold?: number;
4
+ }
5
+ export declare function DevTools({ url, defaultThreshold, }: DevToolsProps): import("react").JSX.Element;
6
+ export default DevTools;
7
+ //# sourceMappingURL=DevTools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DevTools.d.ts","sourceRoot":"","sources":["../../src/components/DevTools.tsx"],"names":[],"mappings":"AAeA,MAAM,WAAW,aAAa;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAgB,QAAQ,CAAC,EACvB,GAAgC,EAChC,gBAAsB,GACvB,EAAE,aAAa,+BA2Df;AAED,eAAe,QAAQ,CAAC"}
@@ -0,0 +1,22 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * Main DevTools dashboard.
5
+ */
6
+ import { useState, useMemo } from "react";
7
+ import { useSse } from "../hooks/useSse.js";
8
+ import { Summary } from "./Summary.js";
9
+ import { Timeline } from "./Timeline.js";
10
+ import { FetchInspector } from "./FetchInspector.js";
11
+ import { Warnings } from "./Warnings.js";
12
+ import { FilterControls, filterEvents } from "./Filters.js";
13
+ export function DevTools({ url = "/__next-rsc-debug/events", defaultThreshold = 500, }) {
14
+ const { connected, events, error } = useSse(url);
15
+ const [filter, setFilter] = useState("all");
16
+ const [threshold, setThreshold] = useState(defaultThreshold);
17
+ const [selectedId, setSelectedId] = useState(undefined);
18
+ const filtered = useMemo(() => filterEvents(events, filter, threshold), [events, filter, threshold]);
19
+ const selectedEvent = useMemo(() => filtered.find((e) => e.id === selectedId) ?? null, [filtered, selectedId]);
20
+ return (_jsxs("div", { className: "nrpd-dashboard", children: [_jsxs("header", { className: "nrpd-header", children: [_jsx("h1", { children: "Next RSC Debug" }), _jsx("span", { className: `nrpd-connection ${connected ? "connected" : "disconnected"}`, children: connected ? "● Connected" : "○ Disconnected" }), error && _jsx("span", { className: "nrpd-error", children: error })] }), _jsx(Summary, { events: events, threshold: threshold }), _jsx(FilterControls, { activeFilter: filter, onFilterChange: setFilter, threshold: threshold, onThresholdChange: setThreshold }), _jsxs("div", { className: "nrpd-main", children: [_jsxs("div", { className: "nrpd-panel nrpd-panel-timeline", children: [_jsx("h2", { children: "Timeline" }), _jsx(Timeline, { events: filtered, selectedId: selectedId, onSelect: (event) => setSelectedId(event.id) })] }), _jsxs("div", { className: "nrpd-panel nrpd-panel-inspector", children: [_jsx("h2", { children: "Event details" }), _jsx(FetchInspector, { event: selectedEvent })] })] }), _jsxs("div", { className: "nrpd-panel nrpd-panel-warnings", children: [_jsx("h2", { children: "Warnings" }), _jsx(Warnings, { events: events, threshold: threshold })] })] }));
21
+ }
22
+ export default DevTools;
@@ -0,0 +1,6 @@
1
+ import type { DebugEvent } from "@next-rsc-debug/core";
2
+ export interface FetchInspectorProps {
3
+ event: DebugEvent | null;
4
+ }
5
+ export declare function FetchInspector({ event }: FetchInspectorProps): import("react").JSX.Element;
6
+ //# sourceMappingURL=FetchInspector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FetchInspector.d.ts","sourceRoot":"","sources":["../../src/components/FetchInspector.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;CAC1B;AAOD,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,EAAE,mBAAmB,+BAmE5D"}
@@ -0,0 +1,42 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * Fetch inspector component.
5
+ */
6
+ import { useMemo } from "react";
7
+ export function FetchInspector({ event }) {
8
+ const metadata = (event?.metadata ?? {});
9
+ const rows = useMemo(() => {
10
+ if (!event)
11
+ return [];
12
+ return [
13
+ { label: "Method", value: String(metadata.method ?? "—") },
14
+ { label: "URL", value: String(metadata.url ?? "—") },
15
+ { label: "Status", value: String(metadata.status ?? "—") },
16
+ {
17
+ label: "Duration",
18
+ value: metadata.duration !== undefined ? `${metadata.duration}ms` : "—",
19
+ },
20
+ { label: "Timestamp", value: new Date(event.timestamp).toISOString() },
21
+ { label: "Request ID", value: String(event.requestId ?? "—") },
22
+ { label: "Parent ID", value: String(event.parentId ?? "—") },
23
+ {
24
+ label: "Content length",
25
+ value: metadata.contentLength !== undefined
26
+ ? `${metadata.contentLength} bytes`
27
+ : "—",
28
+ },
29
+ {
30
+ label: "Confidence",
31
+ value: String(metadata.confidence ?? "observed"),
32
+ },
33
+ ];
34
+ }, [event, metadata]);
35
+ if (!event) {
36
+ return (_jsx("div", { className: "nrpd-empty", children: _jsx("span", { children: "Select an event to inspect." }) }));
37
+ }
38
+ const warnings = Array.isArray(metadata.warnings)
39
+ ? metadata.warnings
40
+ : [];
41
+ return (_jsxs("div", { className: "nrpd-inspector", children: [_jsx("h3", { children: "Event details" }), _jsx("table", { className: "nrpd-inspector-table", children: _jsx("tbody", { children: rows.map((row) => (_jsxs("tr", { children: [_jsx("td", { className: "nrpd-inspector-label", children: row.label }), _jsx("td", { className: "nrpd-inspector-value", children: row.value })] }, row.label))) }) }), warnings.length > 0 && (_jsxs("div", { className: "nrpd-warnings", children: [_jsx("h4", { children: "Warnings" }), _jsx("ul", { children: warnings.map((w, i) => (_jsx("li", { children: typeof w === "string" ? w : (w.message ?? "") }, i))) })] }))] }));
42
+ }
@@ -0,0 +1,16 @@
1
+ import type { DebugEvent } from "@next-rsc-debug/core";
2
+ export type FilterType = "all" | "navigation" | "rsc" | "fetch" | "cache" | "action" | "error" | "slow" | "errors";
3
+ export interface FilterOption {
4
+ id: FilterType;
5
+ label: string;
6
+ }
7
+ export declare const FILTER_OPTIONS: FilterOption[];
8
+ export declare function filterEvents(events: DebugEvent[], filter: FilterType, threshold: number): DebugEvent[];
9
+ export interface FilterControlsProps {
10
+ activeFilter: FilterType;
11
+ onFilterChange: (filter: FilterType) => void;
12
+ threshold: number;
13
+ onThresholdChange: (threshold: number) => void;
14
+ }
15
+ export declare function FilterControls({ activeFilter, onFilterChange, threshold, onThresholdChange, }: FilterControlsProps): import("react").JSX.Element;
16
+ //# sourceMappingURL=Filters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Filters.d.ts","sourceRoot":"","sources":["../../src/components/Filters.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAkB,MAAM,sBAAsB,CAAC;AAEvE,MAAM,MAAM,UAAU,GAClB,KAAK,GACL,YAAY,GACZ,KAAK,GACL,OAAO,GACP,OAAO,GACP,QAAQ,GACR,OAAO,GACP,MAAM,GACN,QAAQ,CAAC;AAEb,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,UAAU,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED,eAAO,MAAM,cAAc,EAAE,YAAY,EAUxC,CAAC;AAEF,wBAAgB,YAAY,CAC1B,MAAM,EAAE,UAAU,EAAE,EACpB,MAAM,EAAE,UAAU,EAClB,SAAS,EAAE,MAAM,GAChB,UAAU,EAAE,CAiCd;AAED,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,UAAU,CAAC;IACzB,cAAc,EAAE,CAAC,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAChD;AAED,wBAAgB,cAAc,CAAC,EAC7B,YAAY,EACZ,cAAc,EACd,SAAS,EACT,iBAAiB,GAClB,EAAE,mBAAmB,+BA+BrB"}
@@ -0,0 +1,45 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ export const FILTER_OPTIONS = [
4
+ { id: "all", label: "All" },
5
+ { id: "navigation", label: "Navigation" },
6
+ { id: "rsc", label: "RSC" },
7
+ { id: "fetch", label: "Fetch" },
8
+ { id: "cache", label: "Cache" },
9
+ { id: "action", label: "Actions" },
10
+ { id: "error", label: "Errors" },
11
+ { id: "slow", label: "Slow only" },
12
+ { id: "errors", label: "Errors only" },
13
+ ];
14
+ export function filterEvents(events, filter, threshold) {
15
+ if (filter === "all") {
16
+ return events;
17
+ }
18
+ return events.filter((event) => {
19
+ switch (filter) {
20
+ case "navigation":
21
+ return (event.type === "navigation:start" || event.type === "navigation:end");
22
+ case "rsc":
23
+ return event.type === "rsc:start" || event.type === "rsc:end";
24
+ case "fetch":
25
+ return event.type === "fetch:start" || event.type === "fetch:end";
26
+ case "cache":
27
+ return (event.type === "cache:hit" ||
28
+ event.type === "cache:miss" ||
29
+ event.type === "cache:invalidate");
30
+ case "action":
31
+ return event.type === "action:start" || event.type === "action:end";
32
+ case "error":
33
+ return event.type === "error";
34
+ case "slow":
35
+ return (event.duration ?? 0) >= threshold;
36
+ case "errors":
37
+ return event.type === "error";
38
+ default:
39
+ return true;
40
+ }
41
+ });
42
+ }
43
+ export function FilterControls({ activeFilter, onFilterChange, threshold, onThresholdChange, }) {
44
+ return (_jsxs("div", { className: "nrpd-filters", children: [_jsx("div", { className: "nrpd-filter-group", children: FILTER_OPTIONS.map((opt) => (_jsx("button", { className: `nrpd-filter-btn ${activeFilter === opt.id ? "active" : ""}`, onClick: () => onFilterChange(opt.id), children: opt.label }, opt.id))) }), _jsx("div", { className: "nrpd-threshold", children: _jsxs("label", { children: ["Slow threshold:", _jsx("input", { type: "number", value: threshold, onChange: (e) => onThresholdChange(Number.parseInt(e.target.value, 10) || 500), min: 0, step: 50 }), _jsx("span", { children: "ms" })] }) })] }));
45
+ }
@@ -0,0 +1,7 @@
1
+ import type { DebugEvent } from "@next-rsc-debug/core";
2
+ export interface SummaryProps {
3
+ events: DebugEvent[];
4
+ threshold: number;
5
+ }
6
+ export declare function Summary({ events, threshold }: SummaryProps): import("react").JSX.Element;
7
+ //# sourceMappingURL=Summary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Summary.d.ts","sourceRoot":"","sources":["../../src/components/Summary.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAGvD,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,OAAO,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,YAAY,+BAsC1D"}
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * Summary cards for the DevTools dashboard.
5
+ */
6
+ import { useMemo } from "react";
7
+ import { analyzeEvents } from "@next-rsc-debug/core";
8
+ export function Summary({ events, threshold }) {
9
+ const { warnings } = useMemo(() => analyzeEvents(events, { slowThreshold: threshold }), [events, threshold]);
10
+ const stats = useMemo(() => {
11
+ const fetches = events.filter((e) => e.type === "fetch:start" || e.type === "fetch:end").length;
12
+ const slow = warnings.filter((w) => w.type === "slow-request").length;
13
+ const dupes = warnings.filter((w) => w.type === "duplicate-request").length;
14
+ const errors = events.filter((e) => e.type === "error").length;
15
+ return { total: events.length, fetches, slow, dupes, errors };
16
+ }, [events, warnings]);
17
+ const cards = [
18
+ { label: "Events", value: stats.total, color: "#60a5fa" },
19
+ { label: "Fetches", value: stats.fetches, color: "#34d399" },
20
+ { label: "Slow", value: stats.slow, color: "#fbbf24" },
21
+ { label: "Duplicates", value: stats.dupes, color: "#f87171" },
22
+ { label: "Errors", value: stats.errors, color: "#ef4444" },
23
+ ];
24
+ return (_jsx("div", { className: "nrpd-summary", children: cards.map((card) => (_jsxs("div", { className: "nrpd-summary-card", children: [_jsx("span", { className: "nrpd-summary-dot", style: { background: card.color } }), _jsx("span", { className: "nrpd-summary-value", children: card.value }), _jsx("span", { className: "nrpd-summary-label", children: card.label })] }, card.label))) }));
25
+ }
@@ -0,0 +1,8 @@
1
+ import type { DebugEvent } from "@next-rsc-debug/core";
2
+ export interface TimelineProps {
3
+ events: DebugEvent[];
4
+ selectedId?: string;
5
+ onSelect?: (event: DebugEvent) => void;
6
+ }
7
+ export declare function Timeline({ events, selectedId, onSelect }: TimelineProps): import("react").JSX.Element;
8
+ //# sourceMappingURL=Timeline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Timeline.d.ts","sourceRoot":"","sources":["../../src/components/Timeline.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAUvD,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;CACxC;AA2CD,wBAAgB,QAAQ,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,EAAE,aAAa,+BAyCvE"}
@@ -0,0 +1,63 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * Event timeline component.
5
+ */
6
+ import { useMemo } from "react";
7
+ import { isNavigationEventType, isRscEventType, isFetchEventType, isCacheEventType, isActionEventType, isErrorEventType, } from "@next-rsc-debug/core";
8
+ function getEventIcon(type) {
9
+ if (isNavigationEventType(type))
10
+ return "🧭";
11
+ if (isRscEventType(type))
12
+ return "📦";
13
+ if (isFetchEventType(type))
14
+ return "🌐";
15
+ if (isCacheEventType(type))
16
+ return "💾";
17
+ if (isActionEventType(type))
18
+ return "⚡";
19
+ if (isErrorEventType(type))
20
+ return "❌";
21
+ return "•";
22
+ }
23
+ function getEventLabel(event) {
24
+ switch (event.type) {
25
+ case "navigation:start":
26
+ return `Navigation start`;
27
+ case "navigation:end":
28
+ return `Navigation end`;
29
+ case "rsc:start":
30
+ return `RSC request${event.metadata?.route ? ` → ${event.metadata.route}` : ""}`;
31
+ case "rsc:end":
32
+ return `RSC response${event.metadata?.route ? ` → ${event.metadata.route}` : ""}`;
33
+ case "fetch:start":
34
+ return `fetch ${event.metadata?.method ?? "GET"} ${event.metadata?.url ?? ""}`;
35
+ case "fetch:end":
36
+ return `fetch ${event.metadata?.method ?? "GET"} ${event.metadata?.url ?? ""} ${event.metadata?.status ?? ""}`;
37
+ case "cache:hit":
38
+ return `Cache HIT: ${event.metadata?.key ?? ""}`;
39
+ case "cache:miss":
40
+ return `Cache MISS: ${event.metadata?.key ?? ""}`;
41
+ case "cache:invalidate":
42
+ return `Cache INVALIDATE: ${event.metadata?.key ?? ""}`;
43
+ case "action:start":
44
+ return `Action start: ${event.metadata?.name ?? ""}`;
45
+ case "action:end":
46
+ return `Action end: ${event.metadata?.name ?? ""}`;
47
+ case "error":
48
+ return `Error: ${event.metadata?.message ?? ""}`;
49
+ default:
50
+ return event.type;
51
+ }
52
+ }
53
+ export function Timeline({ events, selectedId, onSelect }) {
54
+ const sorted = useMemo(() => [...events].sort((a, b) => a.timestamp - b.timestamp), [events]);
55
+ if (sorted.length === 0) {
56
+ return (_jsx("div", { className: "nrpd-empty", children: _jsx("span", { children: "No events yet." }) }));
57
+ }
58
+ return (_jsx("div", { className: "nrpd-timeline", children: sorted.map((event) => {
59
+ const isSelected = event.id === selectedId;
60
+ const duration = event.duration !== undefined ? ` (${event.duration}ms)` : "";
61
+ return (_jsxs("div", { className: `nrpd-timeline-item ${isSelected ? "selected" : ""}`, onClick: () => onSelect?.(event), children: [_jsx("span", { className: "nrpd-timeline-icon", children: getEventIcon(event.type) }), _jsxs("span", { className: "nrpd-timeline-label", children: [getEventLabel(event), duration] }), _jsx("span", { className: "nrpd-timestamp", children: new Date(event.timestamp).toLocaleTimeString() })] }, event.id));
62
+ }) }));
63
+ }
@@ -0,0 +1,7 @@
1
+ import type { DebugEvent } from "@next-rsc-debug/core";
2
+ export interface WarningsProps {
3
+ events: DebugEvent[];
4
+ threshold?: number;
5
+ }
6
+ export declare function Warnings({ events, threshold }: WarningsProps): import("react").JSX.Element;
7
+ //# sourceMappingURL=Warnings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Warnings.d.ts","sourceRoot":"","sources":["../../src/components/Warnings.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAW,MAAM,sBAAsB,CAAC;AAGhE,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,wBAAgB,QAAQ,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,aAAa,+BA2B5D"}
@@ -0,0 +1,14 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * Warnings panel.
5
+ */
6
+ import { useMemo } from "react";
7
+ import { analyzeEvents } from "@next-rsc-debug/core";
8
+ export function Warnings({ events, threshold }) {
9
+ const { warnings } = useMemo(() => analyzeEvents(events, { slowThreshold: threshold }), [events, threshold]);
10
+ if (warnings.length === 0) {
11
+ return (_jsx("div", { className: "nrpd-empty", children: _jsx("span", { children: "No warnings detected." }) }));
12
+ }
13
+ return (_jsxs("div", { className: "nrpd-warnings", children: [_jsxs("h3", { children: ["Warnings (", warnings.length, ")"] }), _jsx("ul", { className: "nrpd-warning-list", children: warnings.map((w) => (_jsxs("li", { className: `nrpd-warning nrpd-warning-${w.type}`, children: [_jsx("span", { className: "nrpd-warning-type", children: w.type }), _jsx("span", { className: "nrpd-warning-message", children: w.message })] }, w.id))) })] }));
14
+ }
@@ -0,0 +1,8 @@
1
+ import type { DebugEvent } from "@next-rsc-debug/core";
2
+ export interface SseState {
3
+ connected: boolean;
4
+ events: DebugEvent[];
5
+ error: string | null;
6
+ }
7
+ export declare function useSse(url: string): SseState;
8
+ //# sourceMappingURL=useSse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSse.d.ts","sourceRoot":"","sources":["../../src/hooks/useSse.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAEvD,MAAM,WAAW,QAAQ;IACvB,SAAS,EAAE,OAAO,CAAC;IACnB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,CAiE5C"}
@@ -0,0 +1,64 @@
1
+ "use client";
2
+ /**
3
+ * SSE client hook for live event streaming.
4
+ */
5
+ import { useState, useEffect, useRef } from "react";
6
+ export function useSse(url) {
7
+ const [connected, setConnected] = useState(false);
8
+ const [events, setEvents] = useState([]);
9
+ const [error, setError] = useState(null);
10
+ const esRef = useRef(null);
11
+ const eventsRef = useRef([]);
12
+ useEffect(() => {
13
+ if (typeof window === "undefined") {
14
+ return;
15
+ }
16
+ let es;
17
+ try {
18
+ es = new EventSource(url);
19
+ }
20
+ catch (err) {
21
+ setError(err instanceof Error ? err.message : "Failed to connect");
22
+ return;
23
+ }
24
+ esRef.current = es;
25
+ es.onopen = () => {
26
+ setConnected(true);
27
+ setError(null);
28
+ };
29
+ es.onerror = () => {
30
+ setConnected(false);
31
+ setError("Connection lost");
32
+ };
33
+ es.addEventListener("snapshot", (e) => {
34
+ try {
35
+ const data = JSON.parse(e.data);
36
+ if (data.events) {
37
+ eventsRef.current = data.events;
38
+ setEvents(data.events);
39
+ }
40
+ }
41
+ catch {
42
+ // Ignore parse errors.
43
+ }
44
+ });
45
+ es.addEventListener("event", (e) => {
46
+ try {
47
+ const event = JSON.parse(e.data);
48
+ eventsRef.current = [...eventsRef.current, event];
49
+ setEvents(eventsRef.current);
50
+ }
51
+ catch {
52
+ // Ignore parse errors.
53
+ }
54
+ });
55
+ es.addEventListener("heartbeat", () => {
56
+ // Heartbeat received — connection is alive.
57
+ });
58
+ return () => {
59
+ es.close();
60
+ setConnected(false);
61
+ };
62
+ }, [url]);
63
+ return { connected, events, error };
64
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Public API for @next-rsc-debug/devtools.
3
+ */
4
+ export { DevTools, type DevToolsProps } from "./components/DevTools.js";
5
+ export { Timeline, type TimelineProps } from "./components/Timeline.js";
6
+ export { FetchInspector, type FetchInspectorProps, } from "./components/FetchInspector.js";
7
+ export { Warnings, type WarningsProps } from "./components/Warnings.js";
8
+ export { Summary, type SummaryProps } from "./components/Summary.js";
9
+ export { FilterControls, filterEvents, FILTER_OPTIONS, type FilterType, type FilterControlsProps, } from "./components/Filters.js";
10
+ export { useSse, type SseState } from "./hooks/useSse.js";
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA;;GAEG;AAEH,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,EACL,cAAc,EACd,KAAK,mBAAmB,GACzB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,KAAK,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACxE,OAAO,EAAE,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,EACL,cAAc,EACd,YAAY,EACZ,cAAc,EACd,KAAK,UAAU,EACf,KAAK,mBAAmB,GACzB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,MAAM,EAAE,KAAK,QAAQ,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ "use client";
2
+ /**
3
+ * Public API for @next-rsc-debug/devtools.
4
+ */
5
+ export { DevTools } from "./components/DevTools.js";
6
+ export { Timeline } from "./components/Timeline.js";
7
+ export { FetchInspector, } from "./components/FetchInspector.js";
8
+ export { Warnings } from "./components/Warnings.js";
9
+ export { Summary } from "./components/Summary.js";
10
+ export { FilterControls, filterEvents, FILTER_OPTIONS, } from "./components/Filters.js";
11
+ export { useSse } from "./hooks/useSse.js";
@@ -0,0 +1,250 @@
1
+ /**
2
+ * DevTools dashboard styles.
3
+ */
4
+
5
+ .nrpd-dashboard {
6
+ font-family:
7
+ -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
8
+ background: #0f1115;
9
+ color: #e6e6e6;
10
+ min-height: 100vh;
11
+ padding: 16px;
12
+ font-size: 13px;
13
+ }
14
+
15
+ .nrpd-header {
16
+ display: flex;
17
+ align-items: center;
18
+ gap: 12px;
19
+ padding-bottom: 16px;
20
+ border-bottom: 1px solid #1f2430;
21
+ margin-bottom: 16px;
22
+ }
23
+
24
+ .nrpd-header h1 {
25
+ font-size: 16px;
26
+ margin: 0;
27
+ font-weight: 600;
28
+ color: #f9fafb;
29
+ }
30
+
31
+ .nrpd-connection {
32
+ font-size: 12px;
33
+ padding: 2px 8px;
34
+ border-radius: 10px;
35
+ background: #1f2430;
36
+ }
37
+
38
+ .nrpd-connection.connected {
39
+ color: #34d399;
40
+ }
41
+
42
+ .nrpd-connection.disconnected {
43
+ color: #f87171;
44
+ }
45
+
46
+ .nrpd-error {
47
+ color: #f87171;
48
+ font-size: 12px;
49
+ }
50
+
51
+ .nrpd-summary {
52
+ display: flex;
53
+ gap: 12px;
54
+ margin-bottom: 16px;
55
+ flex-wrap: wrap;
56
+ }
57
+
58
+ .nrpd-summary-card {
59
+ display: flex;
60
+ align-items: center;
61
+ gap: 8px;
62
+ background: #161b22;
63
+ border: 1px solid #1f2430;
64
+ border-radius: 8px;
65
+ padding: 8px 12px;
66
+ min-width: 90px;
67
+ }
68
+
69
+ .nrpd-summary-dot {
70
+ width: 8px;
71
+ height: 8px;
72
+ border-radius: 50%;
73
+ }
74
+
75
+ .nrpd-summary-value {
76
+ font-size: 16px;
77
+ font-weight: 600;
78
+ color: #f9fafb;
79
+ }
80
+
81
+ .nrpd-summary-label {
82
+ font-size: 11px;
83
+ color: #8b949e;
84
+ }
85
+
86
+ .nrpd-filters {
87
+ display: flex;
88
+ align-items: center;
89
+ gap: 12px;
90
+ margin-bottom: 16px;
91
+ flex-wrap: wrap;
92
+ }
93
+
94
+ .nrpd-filter-group {
95
+ display: flex;
96
+ gap: 4px;
97
+ background: #161b22;
98
+ border: 1px solid #1f2430;
99
+ border-radius: 8px;
100
+ padding: 4px;
101
+ }
102
+
103
+ .nrpd-filter-btn {
104
+ background: transparent;
105
+ border: none;
106
+ color: #8b949e;
107
+ padding: 4px 10px;
108
+ border-radius: 6px;
109
+ cursor: pointer;
110
+ font-size: 12px;
111
+ }
112
+
113
+ .nrpd-filter-btn.active {
114
+ background: #2563eb;
115
+ color: #fff;
116
+ }
117
+
118
+ .nrpd-threshold {
119
+ display: flex;
120
+ align-items: center;
121
+ gap: 6px;
122
+ font-size: 12px;
123
+ color: #8b949e;
124
+ }
125
+
126
+ .nrpd-threshold input {
127
+ width: 60px;
128
+ background: #161b22;
129
+ border: 1px solid #1f2430;
130
+ color: #e6e6e6;
131
+ padding: 2px 6px;
132
+ border-radius: 4px;
133
+ }
134
+
135
+ .nrpd-main {
136
+ display: grid;
137
+ grid-template-columns: 1fr 1fr;
138
+ gap: 16px;
139
+ margin-bottom: 16px;
140
+ }
141
+
142
+ .nrpd-panel {
143
+ background: #161b22;
144
+ border: 1px solid #1f2430;
145
+ border-radius: 8px;
146
+ padding: 12px;
147
+ }
148
+
149
+ .nrpd-panel h2 {
150
+ font-size: 13px;
151
+ margin: 0 0 12px 0;
152
+ color: #f9fafb;
153
+ font-weight: 600;
154
+ }
155
+
156
+ .nrpd-timeline {
157
+ max-height: 400px;
158
+ overflow-y: auto;
159
+ }
160
+
161
+ .nrpd-timeline-item {
162
+ display: flex;
163
+ align-items: center;
164
+ gap: 8px;
165
+ padding: 6px 8px;
166
+ border-radius: 4px;
167
+ cursor: pointer;
168
+ border-bottom: 1px solid #1a1f29;
169
+ }
170
+
171
+ .nrpd-timeline-item:hover {
172
+ background: #1a1f29;
173
+ }
174
+
175
+ .nrpd-timeline-item.selected {
176
+ background: #1e3a5f;
177
+ }
178
+
179
+ .nrpd-timeline-icon {
180
+ font-size: 14px;
181
+ }
182
+
183
+ .nrpd-timeline-label {
184
+ flex: 1;
185
+ font-size: 12px;
186
+ }
187
+
188
+ .nrpd-timestamp {
189
+ font-size: 11px;
190
+ color: #6e7681;
191
+ }
192
+
193
+ .nrpd-empty {
194
+ padding: 24px;
195
+ text-align: center;
196
+ color: #6e7681;
197
+ }
198
+
199
+ .nrpd-inspector table {
200
+ width: 100%;
201
+ border-collapse: collapse;
202
+ }
203
+
204
+ .nrpd-inspector-label {
205
+ color: #8b949e;
206
+ padding: 4px 8px;
207
+ border-bottom: 1px solid #1a1f29;
208
+ font-size: 11px;
209
+ text-transform: uppercase;
210
+ }
211
+
212
+ .nrpd-inspector-value {
213
+ padding: 4px 8px;
214
+ border-bottom: 1px solid #1a1f29;
215
+ font-size: 12px;
216
+ word-break: break-all;
217
+ }
218
+
219
+ .nrpd-warning-list {
220
+ list-style: none;
221
+ padding: 0;
222
+ margin: 0;
223
+ }
224
+
225
+ .nrpd-warning {
226
+ display: flex;
227
+ gap: 8px;
228
+ padding: 8px;
229
+ border-radius: 4px;
230
+ background: #1a1f29;
231
+ margin-bottom: 8px;
232
+ font-size: 12px;
233
+ }
234
+
235
+ .nrpd-warning-type {
236
+ font-weight: 600;
237
+ text-transform: uppercase;
238
+ font-size: 10px;
239
+ color: #fbbf24;
240
+ }
241
+
242
+ .nrpd-warning-message {
243
+ flex: 1;
244
+ }
245
+
246
+ @media (max-width: 768px) {
247
+ .nrpd-main {
248
+ grid-template-columns: 1fr;
249
+ }
250
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@next-rsc-debug/devtools",
3
+ "version": "0.1.3",
4
+ "description": "React-based DevTools UI for Next RSC Debug",
5
+ "author": "Samith Mendis",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ },
15
+ "./styles.css": "./dist/styles.css"
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc -p tsconfig.json && node ./scripts/copy-css.mjs",
22
+ "typecheck": "tsc --noEmit -p tsconfig.json",
23
+ "test": "vitest run --config vitest.config.ts",
24
+ "lint": "eslint src --ext .tsx,.ts"
25
+ },
26
+ "dependencies": {
27
+ "@next-rsc-debug/core": "workspace:*"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public",
31
+ "registry": "https://registry.npmjs.org/",
32
+ "executable": false
33
+ },
34
+ "peerDependencies": {
35
+ "react": "^19.0.0",
36
+ "react-dom": "^19.0.0",
37
+ "typescript": "^5.0.0"
38
+ },
39
+ "devDependencies": {
40
+ "@testing-library/jest-dom": "^6.6.0",
41
+ "@testing-library/react": "^16.0.0",
42
+ "jsdom": "^26.1.0",
43
+ "react": "^19.0.0",
44
+ "react-dom": "^19.0.0",
45
+ "typescript": "^5.9.3",
46
+ "vitest": "^3.2.4"
47
+ }
48
+ }