@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 +52 -0
- package/dist/components/DevTools.d.ts +7 -0
- package/dist/components/DevTools.d.ts.map +1 -0
- package/dist/components/DevTools.js +22 -0
- package/dist/components/FetchInspector.d.ts +6 -0
- package/dist/components/FetchInspector.d.ts.map +1 -0
- package/dist/components/FetchInspector.js +42 -0
- package/dist/components/Filters.d.ts +16 -0
- package/dist/components/Filters.d.ts.map +1 -0
- package/dist/components/Filters.js +45 -0
- package/dist/components/Summary.d.ts +7 -0
- package/dist/components/Summary.d.ts.map +1 -0
- package/dist/components/Summary.js +25 -0
- package/dist/components/Timeline.d.ts +8 -0
- package/dist/components/Timeline.d.ts.map +1 -0
- package/dist/components/Timeline.js +63 -0
- package/dist/components/Warnings.d.ts +7 -0
- package/dist/components/Warnings.d.ts.map +1 -0
- package/dist/components/Warnings.js +14 -0
- package/dist/hooks/useSse.d.ts +8 -0
- package/dist/hooks/useSse.d.ts.map +1 -0
- package/dist/hooks/useSse.js +64 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/styles.css +250 -0
- package/package.json +48 -0
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 @@
|
|
|
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 @@
|
|
|
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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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";
|
package/dist/styles.css
ADDED
|
@@ -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
|
+
}
|