rsc-boundary 0.1.0 → 0.2.0
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 +16 -13
- package/dist/components/devtools-client-component-entry.d.ts +7 -0
- package/dist/components/devtools-client-component-entry.d.ts.map +1 -0
- package/dist/components/devtools-client-component-entry.js +19 -0
- package/dist/components/devtools-compare.d.ts +4 -0
- package/dist/components/devtools-compare.d.ts.map +1 -0
- package/dist/components/devtools-compare.js +50 -0
- package/dist/components/devtools-legend-item.d.ts +7 -0
- package/dist/components/devtools-legend-item.d.ts.map +1 -0
- package/dist/components/devtools-legend-item.js +10 -0
- package/dist/components/devtools-panel.d.ts +7 -0
- package/dist/components/devtools-panel.d.ts.map +1 -0
- package/dist/components/devtools-panel.js +84 -0
- package/dist/components/devtools-pill.d.ts +9 -0
- package/dist/components/devtools-pill.d.ts.map +1 -0
- package/dist/components/devtools-pill.js +34 -0
- package/dist/components/devtools-server-region-entry.d.ts +7 -0
- package/dist/components/devtools-server-region-entry.d.ts.map +1 -0
- package/dist/components/devtools-server-region-entry.js +33 -0
- package/dist/{provider.d.ts → components/provider.d.ts} +2 -5
- package/dist/components/provider.d.ts.map +1 -0
- package/dist/components/provider.js +6 -0
- package/dist/{devtools.d.ts → components/rsc-devtools.d.ts} +1 -1
- package/dist/components/rsc-devtools.d.ts.map +1 -0
- package/dist/components/rsc-devtools.js +77 -0
- package/dist/components/server-boundary-marker.d.ts.map +1 -0
- package/dist/{server-boundary-marker.js → components/server-boundary-marker.js} +2 -1
- package/dist/constants.d.ts +6 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +6 -0
- package/dist/fiber-utils.d.ts +3 -3
- package/dist/fiber-utils.d.ts.map +1 -1
- package/dist/fiber-utils.js +27 -38
- package/dist/highlight-caption.d.ts +6 -0
- package/dist/highlight-caption.d.ts.map +1 -0
- package/dist/highlight-caption.js +11 -0
- package/dist/highlight.d.ts +6 -2
- package/dist/highlight.d.ts.map +1 -1
- package/dist/highlight.js +14 -13
- package/dist/host-label.d.ts +5 -0
- package/dist/host-label.d.ts.map +1 -0
- package/dist/host-label.js +10 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/types.d.ts +4 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +13 -3
- package/dist/devtools.d.ts.map +0 -1
- package/dist/devtools.js +0 -286
- package/dist/provider.d.ts.map +0 -1
- package/dist/provider.js +0 -6
- package/dist/server-boundary-marker.d.ts.map +0 -1
- /package/dist/{server-boundary-marker.d.ts → components/server-boundary-marker.d.ts} +0 -0
package/README.md
CHANGED
|
@@ -39,13 +39,7 @@ A small floating pill appears in the bottom-left corner of your page during deve
|
|
|
39
39
|
- **Labels** on each region showing the component name / host tag and provenance
|
|
40
40
|
- **Panel** listing client components and server regions with explicit vs heuristic badges
|
|
41
41
|
|
|
42
|
-
In production builds, `RscBoundaryProvider` renders only `{children}` —
|
|
43
|
-
|
|
44
|
-
To force devtools in production (e.g. a documentation site), pass `enabled`:
|
|
45
|
-
|
|
46
|
-
```tsx
|
|
47
|
-
<RscBoundaryProvider enabled>{children}</RscBoundaryProvider>
|
|
48
|
-
```
|
|
42
|
+
In production builds, `RscBoundaryProvider` renders only `{children}` — no devtools UI, no extra DOM nodes, and no scanning work. Devtools run only in development (`NODE_ENV === "development"`).
|
|
49
43
|
|
|
50
44
|
The package also exports `RscDevtools` for advanced wiring, and optional `RscServerBoundaryMarker` / `SERVER_BOUNDARY_DATA_ATTR` for explicit server regions; most apps should rely on the provider only.
|
|
51
45
|
|
|
@@ -55,7 +49,7 @@ React Server Components are resolved on the server and sent to the client as pre
|
|
|
55
49
|
|
|
56
50
|
When you toggle the devtools on, RSC Boundary walks the React fiber tree (via the `__reactFiber$*` property that React attaches to DOM elements) and:
|
|
57
51
|
|
|
58
|
-
1. Finds every
|
|
52
|
+
1. Finds every user component fiber: function, class, `forwardRef`, and `memo` (including simple memo) work tags
|
|
59
53
|
2. Filters out Next.js framework internals (LayoutRouter, ErrorBoundary, etc.)
|
|
60
54
|
3. Maps each remaining user-defined component to its root DOM node(s) — these are your **client component boundaries**
|
|
61
55
|
4. Collects **explicit** regions: elements with `data-rsc-boundary-server` (e.g. `RscServerBoundaryMarker`)
|
|
@@ -68,14 +62,23 @@ A `MutationObserver` watches for DOM changes (route navigation, lazy loading) an
|
|
|
68
62
|
```
|
|
69
63
|
packages/rsc-boundary/src/
|
|
70
64
|
├── index.ts # Public API
|
|
71
|
-
├── constants.ts # data attribute
|
|
72
|
-
├── provider.tsx # Server component — children + <RscDevtools /> in dev
|
|
73
|
-
├── server-boundary-marker.tsx # Optional explicit server region wrapper
|
|
74
|
-
├── devtools.tsx # "use client" — pill, panel, scan trigger
|
|
65
|
+
├── constants.ts # data attribute names (markers, devtools, highlights)
|
|
75
66
|
├── fiber-utils.ts # Fiber walk + server region detection
|
|
76
67
|
├── highlight.ts # Outlines, labels, MutationObserver
|
|
68
|
+
├── highlight-caption.ts # Label text for highlighted regions
|
|
69
|
+
├── host-label.ts # Fallback labels from host DOM
|
|
77
70
|
├── styles.ts
|
|
78
|
-
|
|
71
|
+
├── types.ts
|
|
72
|
+
└── components/
|
|
73
|
+
├── provider.tsx # Server component — children + <RscDevtools /> in dev
|
|
74
|
+
├── rsc-devtools.tsx # "use client" — scan trigger, highlights, observer wiring
|
|
75
|
+
├── devtools-pill.tsx # Floating toggle
|
|
76
|
+
├── devtools-panel.tsx # Side panel + lists
|
|
77
|
+
├── devtools-compare.ts # Stable list diffing for panel updates
|
|
78
|
+
├── devtools-legend-item.tsx
|
|
79
|
+
├── devtools-client-component-entry.tsx
|
|
80
|
+
├── devtools-server-region-entry.tsx
|
|
81
|
+
└── server-boundary-marker.tsx # Optional explicit server region wrapper
|
|
79
82
|
```
|
|
80
83
|
|
|
81
84
|
## Limitations
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ClientComponentInfo } from "../types";
|
|
2
|
+
interface ClientComponentEntryProps {
|
|
3
|
+
component: ClientComponentInfo;
|
|
4
|
+
}
|
|
5
|
+
export declare function ClientComponentEntry({ component }: ClientComponentEntryProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=devtools-client-component-entry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devtools-client-component-entry.d.ts","sourceRoot":"","sources":["../../src/components/devtools-client-component-entry.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAGpD,UAAU,yBAAyB;IACjC,SAAS,EAAE,mBAAmB,CAAC;CAChC;AAED,wBAAgB,oBAAoB,CAAC,EAAE,SAAS,EAAE,EAAE,yBAAyB,2CAgC5E"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { COLORS } from "../styles";
|
|
3
|
+
export function ClientComponentEntry({ component }) {
|
|
4
|
+
return (_jsxs("div", { style: {
|
|
5
|
+
display: "flex",
|
|
6
|
+
alignItems: "center",
|
|
7
|
+
gap: 6,
|
|
8
|
+
padding: "3px 6px",
|
|
9
|
+
borderRadius: 4,
|
|
10
|
+
background: "rgba(249, 115, 22, 0.1)",
|
|
11
|
+
fontSize: 11,
|
|
12
|
+
}, children: [_jsx("span", { style: {
|
|
13
|
+
width: 6,
|
|
14
|
+
height: 6,
|
|
15
|
+
borderRadius: "50%",
|
|
16
|
+
background: COLORS.client.outline,
|
|
17
|
+
flexShrink: 0,
|
|
18
|
+
} }), _jsx("span", { style: { color: "rgba(255,255,255,0.9)" }, children: `<${component.name} />` }), component.domNodes.length > 1 && (_jsxs("span", { style: { color: "rgba(255,255,255,0.4)", marginLeft: "auto" }, children: [component.domNodes.length, " nodes"] }))] }));
|
|
19
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ClientComponentInfo, ServerRegionInfo } from "../types";
|
|
2
|
+
export declare function clientComponentListEqual(a: ClientComponentInfo[], b: ClientComponentInfo[]): boolean;
|
|
3
|
+
export declare function serverRegionsEqual(a: ServerRegionInfo[], b: ServerRegionInfo[]): boolean;
|
|
4
|
+
//# sourceMappingURL=devtools-compare.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devtools-compare.d.ts","sourceRoot":"","sources":["../../src/components/devtools-compare.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAEtE,wBAAgB,wBAAwB,CACtC,CAAC,EAAE,mBAAmB,EAAE,EACxB,CAAC,EAAE,mBAAmB,EAAE,GACvB,OAAO,CA2BT;AAED,wBAAgB,kBAAkB,CAChC,CAAC,EAAE,gBAAgB,EAAE,EACrB,CAAC,EAAE,gBAAgB,EAAE,GACpB,OAAO,CAqBT"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export function clientComponentListEqual(a, b) {
|
|
2
|
+
if (a.length !== b.length) {
|
|
3
|
+
return false;
|
|
4
|
+
}
|
|
5
|
+
for (let i = 0; i < a.length; i++) {
|
|
6
|
+
const ai = a[i];
|
|
7
|
+
const bi = b[i];
|
|
8
|
+
if (ai === undefined || bi === undefined) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
if (ai.name !== bi.name) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const na = ai.domNodes;
|
|
15
|
+
const nb = bi.domNodes;
|
|
16
|
+
if (na.length !== nb.length) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
for (let j = 0; j < na.length; j++) {
|
|
20
|
+
const naJ = na[j];
|
|
21
|
+
const nbJ = nb[j];
|
|
22
|
+
if (naJ !== nbJ) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
export function serverRegionsEqual(a, b) {
|
|
30
|
+
if (a.length !== b.length) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
for (let i = 0; i < a.length; i++) {
|
|
34
|
+
const ai = a[i];
|
|
35
|
+
const bi = b[i];
|
|
36
|
+
if (ai === undefined || bi === undefined) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
if (ai.element !== bi.element) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (ai.source !== bi.source) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (ai.displayLabel !== bi.displayLabel) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devtools-legend-item.d.ts","sourceRoot":"","sources":["../../src/components/devtools-legend-item.tsx"],"names":[],"mappings":"AAAA,UAAU,eAAe;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,UAAU,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,eAAe,2CAe3D"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export function LegendItem({ color, label }) {
|
|
3
|
+
return (_jsxs("span", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [_jsx("span", { style: {
|
|
4
|
+
display: "inline-block",
|
|
5
|
+
width: 10,
|
|
6
|
+
height: 10,
|
|
7
|
+
borderRadius: 2,
|
|
8
|
+
border: `2px dashed ${color}`,
|
|
9
|
+
} }), label] }));
|
|
10
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ClientComponentInfo, ServerRegionInfo } from "../types";
|
|
2
|
+
export interface PanelProps {
|
|
3
|
+
clientComponents: ClientComponentInfo[];
|
|
4
|
+
serverRegions: ServerRegionInfo[];
|
|
5
|
+
}
|
|
6
|
+
export declare function Panel({ clientComponents, serverRegions }: PanelProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
//# sourceMappingURL=devtools-panel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devtools-panel.d.ts","sourceRoot":"","sources":["../../src/components/devtools-panel.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAQtE,MAAM,WAAW,UAAU;IACzB,gBAAgB,EAAE,mBAAmB,EAAE,CAAC;IACxC,aAAa,EAAE,gBAAgB,EAAE,CAAC;CACnC;AAED,wBAAgB,KAAK,CAAC,EAAE,gBAAgB,EAAE,aAAa,EAAE,EAAE,UAAU,2CAqNpE"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect, useRef, } from "react";
|
|
3
|
+
import { RSC_DEVTOOLS_DATA_ATTR } from "../constants";
|
|
4
|
+
import { COLORS, PANEL_STYLES, applyStyles } from "../styles";
|
|
5
|
+
import { LegendItem } from "./devtools-legend-item";
|
|
6
|
+
import { ClientComponentEntry } from "./devtools-client-component-entry";
|
|
7
|
+
import { ServerRegionEntry } from "./devtools-server-region-entry";
|
|
8
|
+
export function Panel({ clientComponents, serverRegions }) {
|
|
9
|
+
const panelRef = useRef(null);
|
|
10
|
+
const [tab, setTab] = useState("client");
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!panelRef.current)
|
|
13
|
+
return;
|
|
14
|
+
applyStyles(panelRef.current, PANEL_STYLES);
|
|
15
|
+
}, []);
|
|
16
|
+
const tabBase = {
|
|
17
|
+
flex: 1,
|
|
18
|
+
display: "flex",
|
|
19
|
+
flexDirection: "column",
|
|
20
|
+
alignItems: "center",
|
|
21
|
+
justifyContent: "center",
|
|
22
|
+
gap: 2,
|
|
23
|
+
minHeight: 44,
|
|
24
|
+
padding: "6px 6px 8px",
|
|
25
|
+
border: "none",
|
|
26
|
+
borderRadius: "4px 4px 0 0",
|
|
27
|
+
background: "transparent",
|
|
28
|
+
cursor: "pointer",
|
|
29
|
+
fontFamily: "inherit",
|
|
30
|
+
};
|
|
31
|
+
const tabLine1 = {
|
|
32
|
+
fontSize: 14,
|
|
33
|
+
fontWeight: 600,
|
|
34
|
+
lineHeight: 1.15,
|
|
35
|
+
fontVariantNumeric: "tabular-nums",
|
|
36
|
+
};
|
|
37
|
+
const tabLine2 = {
|
|
38
|
+
fontSize: 10,
|
|
39
|
+
fontWeight: 500,
|
|
40
|
+
lineHeight: 1.2,
|
|
41
|
+
textAlign: "center",
|
|
42
|
+
whiteSpace: "nowrap",
|
|
43
|
+
};
|
|
44
|
+
return (_jsxs("div", { ref: panelRef, [RSC_DEVTOOLS_DATA_ATTR]: "", children: [_jsx("div", { style: {
|
|
45
|
+
marginBottom: 10,
|
|
46
|
+
paddingBottom: 8,
|
|
47
|
+
borderBottom: "1px solid rgba(255,255,255,0.1)",
|
|
48
|
+
fontSize: 13,
|
|
49
|
+
fontWeight: 600,
|
|
50
|
+
}, children: "RSC Boundaries" }), _jsxs("div", { style: {
|
|
51
|
+
display: "flex",
|
|
52
|
+
flexDirection: "column",
|
|
53
|
+
gap: 6,
|
|
54
|
+
marginBottom: 10,
|
|
55
|
+
fontSize: 11,
|
|
56
|
+
}, children: [_jsxs("div", { style: { display: "flex", gap: 12 }, children: [_jsx(LegendItem, { color: COLORS.server.outline, label: "Server" }), _jsx(LegendItem, { color: COLORS.client.outline, label: "Client" })] }), _jsxs("div", { style: {
|
|
57
|
+
color: "rgba(255,255,255,0.45)",
|
|
58
|
+
fontSize: 10,
|
|
59
|
+
lineHeight: 1.35,
|
|
60
|
+
}, children: ["Server:", " ", _jsx("span", { style: { color: "rgba(255,255,255,0.65)" }, children: "explicit" }), " ", "(marker) vs", " ", _jsx("span", { style: { color: "rgba(255,255,255,0.65)" }, children: "~" }), " ", "(heuristic)."] })] }), _jsxs("div", { role: "tablist", "aria-label": "Boundary list scope", style: {
|
|
61
|
+
display: "flex",
|
|
62
|
+
gap: 0,
|
|
63
|
+
marginBottom: 10,
|
|
64
|
+
borderBottom: "1px solid rgba(255,255,255,0.12)",
|
|
65
|
+
}, children: [_jsxs("button", { type: "button", role: "tab", "aria-selected": tab === "client", id: "rsc-panel-tab-client", "aria-controls": "rsc-panel-client", "aria-label": `${clientComponents.length} client component${clientComponents.length !== 1 ? "s" : ""}`, onClick: () => setTab("client"), style: {
|
|
66
|
+
...tabBase,
|
|
67
|
+
color: tab === "client"
|
|
68
|
+
? "rgba(255,255,255,0.95)"
|
|
69
|
+
: "rgba(255,255,255,0.55)",
|
|
70
|
+
borderBottom: tab === "client"
|
|
71
|
+
? `2px solid ${COLORS.client.outline}`
|
|
72
|
+
: "2px solid transparent",
|
|
73
|
+
marginBottom: -1,
|
|
74
|
+
}, children: [_jsx("span", { style: tabLine1, children: clientComponents.length }), _jsxs("span", { style: tabLine2, children: ["client component", clientComponents.length !== 1 ? "s" : ""] })] }), _jsxs("button", { type: "button", role: "tab", "aria-selected": tab === "server", id: "rsc-panel-tab-server", "aria-controls": "rsc-panel-server", "aria-label": `${serverRegions.length} server region${serverRegions.length !== 1 ? "s" : ""}`, onClick: () => setTab("server"), style: {
|
|
75
|
+
...tabBase,
|
|
76
|
+
color: tab === "server"
|
|
77
|
+
? "rgba(255,255,255,0.95)"
|
|
78
|
+
: "rgba(255,255,255,0.55)",
|
|
79
|
+
borderBottom: tab === "server"
|
|
80
|
+
? `2px solid ${COLORS.server.outline}`
|
|
81
|
+
: "2px solid transparent",
|
|
82
|
+
marginBottom: -1,
|
|
83
|
+
}, children: [_jsx("span", { style: tabLine1, children: serverRegions.length }), _jsxs("span", { style: tabLine2, children: ["server region", serverRegions.length !== 1 ? "s" : ""] })] })] }), tab === "client" && (_jsx("div", { id: "rsc-panel-client", role: "tabpanel", "aria-labelledby": "rsc-panel-tab-client", children: clientComponents.length > 0 ? (_jsx("div", { style: { display: "flex", flexDirection: "column", gap: 4 }, children: clientComponents.map((comp, i) => (_jsx(ClientComponentEntry, { component: comp }, `${comp.name}-${i}`))) })) : (_jsx("div", { style: { color: "rgba(255,255,255,0.4)", fontSize: 11 }, children: serverRegions.length > 0 ? (_jsxs(_Fragment, { children: ["No client components in this view.", _jsx("br", {}), _jsx("span", { style: { color: "rgba(255,255,255,0.35)" }, children: "Switch to the server tab to see server regions." })] })) : ("No regions detected.") })) })), tab === "server" && (_jsx("div", { id: "rsc-panel-server", role: "tabpanel", "aria-labelledby": "rsc-panel-tab-server", children: serverRegions.length > 0 ? (_jsx("div", { style: { display: "flex", flexDirection: "column", gap: 4 }, children: serverRegions.map((region, i) => (_jsx(ServerRegionEntry, { region: region }, `server-region-${i}`))) })) : (_jsx("div", { style: { color: "rgba(255,255,255,0.4)", fontSize: 11 }, children: clientComponents.length > 0 ? (_jsxs(_Fragment, { children: ["No server regions in this view (everything may sit under client boundaries).", _jsx("br", {}), _jsx("span", { style: { color: "rgba(255,255,255,0.35)" }, children: "Switch to the client tab or add an explicit server marker." })] })) : ("No regions detected.") })) }))] }));
|
|
84
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type MouseEvent } from "react";
|
|
2
|
+
export interface PillProps {
|
|
3
|
+
active: boolean;
|
|
4
|
+
onToggle: () => void;
|
|
5
|
+
onPanelToggle: (e: MouseEvent) => void;
|
|
6
|
+
clientCount: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function Pill({ active, onToggle, onPanelToggle, clientCount, }: PillProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
//# sourceMappingURL=devtools-pill.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devtools-pill.d.ts","sourceRoot":"","sources":["../../src/components/devtools-pill.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,UAAU,EAAE,MAAM,OAAO,CAAC;AAU3D,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,aAAa,EAAE,CAAC,CAAC,EAAE,UAAU,KAAK,IAAI,CAAC;IACvC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,IAAI,CAAC,EACnB,MAAM,EACN,QAAQ,EACR,aAAa,EACb,WAAW,GACZ,EAAE,SAAS,2CA0DX"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { RSC_DEVTOOLS_DATA_ATTR } from "../constants";
|
|
4
|
+
import { COLORS, PILL_STYLES, PILL_ACTIVE_STYLES, applyStyles, } from "../styles";
|
|
5
|
+
export function Pill({ active, onToggle, onPanelToggle, clientCount, }) {
|
|
6
|
+
const pillRef = useRef(null);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (!pillRef.current)
|
|
9
|
+
return;
|
|
10
|
+
applyStyles(pillRef.current, PILL_STYLES);
|
|
11
|
+
if (active) {
|
|
12
|
+
applyStyles(pillRef.current, PILL_ACTIVE_STYLES);
|
|
13
|
+
}
|
|
14
|
+
}, [active]);
|
|
15
|
+
return (_jsxs("button", { ref: pillRef, type: "button", onClick: onToggle, [RSC_DEVTOOLS_DATA_ATTR]: "", "aria-label": "Toggle RSC Boundary highlighting", title: "Toggle RSC Boundary highlighting", children: [_jsx("span", { style: {
|
|
16
|
+
display: "inline-block",
|
|
17
|
+
width: 8,
|
|
18
|
+
height: 8,
|
|
19
|
+
borderRadius: "50%",
|
|
20
|
+
background: active ? COLORS.client.outline : "rgba(255,255,255,0.4)",
|
|
21
|
+
transition: "background 150ms ease",
|
|
22
|
+
} }), _jsx("span", { children: "RSC" }), active && (_jsx("span", { role: "button", tabIndex: 0, onClick: onPanelToggle, onKeyDown: (e) => {
|
|
23
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
24
|
+
onPanelToggle(e);
|
|
25
|
+
}
|
|
26
|
+
}, [RSC_DEVTOOLS_DATA_ATTR]: "", style: {
|
|
27
|
+
color: "rgba(255,255,255,0.7)",
|
|
28
|
+
cursor: "pointer",
|
|
29
|
+
padding: "0 0 0 4px",
|
|
30
|
+
fontSize: "12px",
|
|
31
|
+
fontFamily: "inherit",
|
|
32
|
+
lineHeight: "1",
|
|
33
|
+
}, "aria-label": "Toggle component panel", title: "Toggle component panel", children: clientCount > 0 ? `(${clientCount})` : "···" }))] }));
|
|
34
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ServerRegionInfo } from "../types";
|
|
2
|
+
interface ServerRegionEntryProps {
|
|
3
|
+
region: ServerRegionInfo;
|
|
4
|
+
}
|
|
5
|
+
export declare function ServerRegionEntry({ region }: ServerRegionEntryProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=devtools-server-region-entry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devtools-server-region-entry.d.ts","sourceRoot":"","sources":["../../src/components/devtools-server-region-entry.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAGjD,UAAU,sBAAsB;IAC9B,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAED,wBAAgB,iBAAiB,CAAC,EAAE,MAAM,EAAE,EAAE,sBAAsB,2CAiDnE"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { COLORS } from "../styles";
|
|
3
|
+
export function ServerRegionEntry({ region }) {
|
|
4
|
+
const provenance = region.source === "explicit" ? "explicit" : "~";
|
|
5
|
+
return (_jsxs("div", { style: {
|
|
6
|
+
display: "flex",
|
|
7
|
+
alignItems: "center",
|
|
8
|
+
gap: 6,
|
|
9
|
+
padding: "3px 6px",
|
|
10
|
+
borderRadius: 4,
|
|
11
|
+
background: "rgba(59, 130, 246, 0.12)",
|
|
12
|
+
fontSize: 11,
|
|
13
|
+
}, children: [_jsx("span", { style: {
|
|
14
|
+
width: 6,
|
|
15
|
+
height: 6,
|
|
16
|
+
borderRadius: "50%",
|
|
17
|
+
background: COLORS.server.outline,
|
|
18
|
+
flexShrink: 0,
|
|
19
|
+
} }), _jsx("span", { style: {
|
|
20
|
+
minWidth: 52,
|
|
21
|
+
flexShrink: 0,
|
|
22
|
+
fontSize: 9,
|
|
23
|
+
fontWeight: 600,
|
|
24
|
+
letterSpacing: "0.02em",
|
|
25
|
+
textTransform: "uppercase",
|
|
26
|
+
color: region.source === "explicit"
|
|
27
|
+
? "rgba(147, 197, 253, 0.95)"
|
|
28
|
+
: "rgba(255,255,255,0.45)",
|
|
29
|
+
}, children: provenance }), _jsx("span", { style: {
|
|
30
|
+
color: "rgba(255,255,255,0.9)",
|
|
31
|
+
fontFamily: "ui-monospace, monospace",
|
|
32
|
+
}, children: region.displayLabel })] }));
|
|
33
|
+
}
|
|
@@ -19,15 +19,12 @@
|
|
|
19
19
|
* ```
|
|
20
20
|
*
|
|
21
21
|
* In development, it renders `{children}` plus the `<RscDevtools />` floating
|
|
22
|
-
* overlay. In production, it renders only `{children}` — zero runtime cost
|
|
23
|
-
* unless you pass `enabled` (e.g. for a documentation site).
|
|
22
|
+
* overlay. In production, it renders only `{children}` — zero runtime cost.
|
|
24
23
|
*/
|
|
25
24
|
import type { ReactNode } from "react";
|
|
26
25
|
interface RscBoundaryProviderProps {
|
|
27
26
|
children: ReactNode;
|
|
28
|
-
/** When `true`, always mount devtools (including production). When omitted, devtools run only in development. */
|
|
29
|
-
enabled?: boolean;
|
|
30
27
|
}
|
|
31
|
-
export declare function RscBoundaryProvider({ children
|
|
28
|
+
export declare function RscBoundaryProvider({ children }: RscBoundaryProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
32
29
|
export {};
|
|
33
30
|
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/components/provider.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,UAAU,wBAAwB;IAChC,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,wBAAgB,mBAAmB,CAAC,EAAE,QAAQ,EAAE,EAAE,wBAAwB,2CASzE"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { RscDevtools } from "./rsc-devtools";
|
|
3
|
+
export function RscBoundaryProvider({ children }) {
|
|
4
|
+
const isDev = process.env.NODE_ENV === "development";
|
|
5
|
+
return (_jsxs(_Fragment, { children: [children, isDev ? _jsx(RscDevtools, {}) : null] }));
|
|
6
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export declare function RscDevtools(): import("react/jsx-runtime").JSX.Element;
|
|
2
|
-
//# sourceMappingURL=devtools.d.ts.map
|
|
2
|
+
//# sourceMappingURL=rsc-devtools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rsc-devtools.d.ts","sourceRoot":"","sources":["../../src/components/rsc-devtools.tsx"],"names":[],"mappings":"AAmCA,wBAAgB,WAAW,4CAkF1B"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* RscDevtools — floating overlay for visualizing RSC boundaries.
|
|
5
|
+
*
|
|
6
|
+
* Renders a small pill-shaped toggle (bottom-left, visually complementing
|
|
7
|
+
* the Next.js dev indicator) that, when activated, scans the React fiber
|
|
8
|
+
* tree to find all client components, highlights them with orange outlines,
|
|
9
|
+
* and shows server regions in blue.
|
|
10
|
+
*
|
|
11
|
+
* A companion panel lists detected components with counts and a legend.
|
|
12
|
+
*
|
|
13
|
+
* This component is client-only ("use client"). Mounting is controlled by
|
|
14
|
+
* `RscBoundaryProvider` (development only).
|
|
15
|
+
*/
|
|
16
|
+
import { useState, useCallback, useEffect, useRef, } from "react";
|
|
17
|
+
import { scanFiberTree, getServerRegions } from "../fiber-utils";
|
|
18
|
+
import { applyHighlights, removeHighlights, observeDomChanges, } from "../highlight";
|
|
19
|
+
import { clientComponentListEqual, serverRegionsEqual } from "./devtools-compare";
|
|
20
|
+
import { Panel } from "./devtools-panel";
|
|
21
|
+
import { Pill } from "./devtools-pill";
|
|
22
|
+
export function RscDevtools() {
|
|
23
|
+
const [active, setActive] = useState(false);
|
|
24
|
+
const [panelOpen, setPanelOpen] = useState(false);
|
|
25
|
+
const [clientComponents, setClientComponents] = useState([]);
|
|
26
|
+
const [serverRegions, setServerRegions] = useState([]);
|
|
27
|
+
const cleanupRef = useRef(null);
|
|
28
|
+
const scan = useCallback(() => {
|
|
29
|
+
const nextClientComponents = scanFiberTree();
|
|
30
|
+
const nextServerRegions = getServerRegions(nextClientComponents);
|
|
31
|
+
applyHighlights(nextClientComponents, nextServerRegions);
|
|
32
|
+
setClientComponents((prev) => clientComponentListEqual(prev, nextClientComponents)
|
|
33
|
+
? prev
|
|
34
|
+
: nextClientComponents);
|
|
35
|
+
setServerRegions((prev) => serverRegionsEqual(prev, nextServerRegions) ? prev : nextServerRegions);
|
|
36
|
+
}, []);
|
|
37
|
+
const activate = useCallback(() => {
|
|
38
|
+
// Delay scan slightly to let React finish any pending renders
|
|
39
|
+
requestAnimationFrame(() => {
|
|
40
|
+
scan();
|
|
41
|
+
cleanupRef.current = observeDomChanges(scan);
|
|
42
|
+
});
|
|
43
|
+
}, [scan]);
|
|
44
|
+
const deactivate = useCallback(() => {
|
|
45
|
+
removeHighlights();
|
|
46
|
+
cleanupRef.current?.();
|
|
47
|
+
cleanupRef.current = null;
|
|
48
|
+
setClientComponents([]);
|
|
49
|
+
setServerRegions([]);
|
|
50
|
+
}, []);
|
|
51
|
+
const handleToggle = useCallback(() => {
|
|
52
|
+
setActive((prev) => {
|
|
53
|
+
const next = !prev;
|
|
54
|
+
if (next) {
|
|
55
|
+
activate();
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
deactivate();
|
|
59
|
+
setPanelOpen(false);
|
|
60
|
+
}
|
|
61
|
+
return next;
|
|
62
|
+
});
|
|
63
|
+
}, [activate, deactivate]);
|
|
64
|
+
const handlePanelToggle = useCallback((e) => {
|
|
65
|
+
e.stopPropagation();
|
|
66
|
+
if (active) {
|
|
67
|
+
setPanelOpen((prev) => !prev);
|
|
68
|
+
}
|
|
69
|
+
}, [active]);
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
return () => {
|
|
72
|
+
removeHighlights();
|
|
73
|
+
cleanupRef.current?.();
|
|
74
|
+
};
|
|
75
|
+
}, []);
|
|
76
|
+
return (_jsxs(_Fragment, { children: [panelOpen && active && (_jsx(Panel, { clientComponents: clientComponents, serverRegions: serverRegions })), _jsx(Pill, { active: active, onToggle: handleToggle, onPanelToggle: handlePanelToggle, clientCount: clientComponents.length })] }));
|
|
77
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-boundary-marker.d.ts","sourceRoot":"","sources":["../../src/components/server-boundary-marker.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIvC,UAAU,4BAA4B;IACpC,QAAQ,EAAE,SAAS,CAAC;IACpB,4DAA4D;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,EACtC,QAAQ,EACR,KAAK,EACL,SAAS,GACV,EAAE,4BAA4B,2CAS9B"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { SERVER_BOUNDARY_DATA_ATTR } from "../constants";
|
|
2
3
|
/**
|
|
3
4
|
* Server Component wrapper that opts a subtree into **explicit** server-region
|
|
4
5
|
* detection. Add `label` so the devtools panel shows a stable name.
|
|
@@ -6,5 +7,5 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
6
7
|
* Heuristic detection still runs elsewhere; this marker is optional.
|
|
7
8
|
*/
|
|
8
9
|
export function RscServerBoundaryMarker({ children, label, className, }) {
|
|
9
|
-
return (_jsx("div", { className: className,
|
|
10
|
+
return (_jsx("div", { className: className, [SERVER_BOUNDARY_DATA_ATTR]: label ?? "", children: children }));
|
|
10
11
|
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
/** Optional explicit server region marker (hybrid / higher-accuracy mode). */
|
|
2
2
|
export declare const SERVER_BOUNDARY_DATA_ATTR = "data-rsc-boundary-server";
|
|
3
|
+
/** Marks nodes that belong to the RSC Boundary devtools UI (excluded from scans). */
|
|
4
|
+
export declare const RSC_DEVTOOLS_DATA_ATTR = "data-rsc-devtools";
|
|
5
|
+
/** Outline target for client or server highlight overlays. */
|
|
6
|
+
export declare const RSC_HIGHLIGHT_DATA_ATTR = "data-rsc-highlight";
|
|
7
|
+
/** Floating caption node inside a highlighted region. */
|
|
8
|
+
export declare const RSC_LABEL_DATA_ATTR = "data-rsc-label";
|
|
3
9
|
//# sourceMappingURL=constants.d.ts.map
|
package/dist/constants.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,eAAO,MAAM,yBAAyB,6BAA6B,CAAC"}
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,eAAO,MAAM,yBAAyB,6BAA6B,CAAC;AAEpE,qFAAqF;AACrF,eAAO,MAAM,sBAAsB,sBAAsB,CAAC;AAE1D,8DAA8D;AAC9D,eAAO,MAAM,uBAAuB,uBAAuB,CAAC;AAE5D,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,mBAAmB,CAAC"}
|
package/dist/constants.js
CHANGED
|
@@ -1,2 +1,8 @@
|
|
|
1
1
|
/** Optional explicit server region marker (hybrid / higher-accuracy mode). */
|
|
2
2
|
export const SERVER_BOUNDARY_DATA_ATTR = "data-rsc-boundary-server";
|
|
3
|
+
/** Marks nodes that belong to the RSC Boundary devtools UI (excluded from scans). */
|
|
4
|
+
export const RSC_DEVTOOLS_DATA_ATTR = "data-rsc-devtools";
|
|
5
|
+
/** Outline target for client or server highlight overlays. */
|
|
6
|
+
export const RSC_HIGHLIGHT_DATA_ATTR = "data-rsc-highlight";
|
|
7
|
+
/** Floating caption node inside a highlighted region. */
|
|
8
|
+
export const RSC_LABEL_DATA_ATTR = "data-rsc-label";
|
package/dist/fiber-utils.d.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* This approach mirrors what React DevTools does internally — it accesses the
|
|
15
15
|
* __reactFiber$* property that React attaches to DOM elements during hydration.
|
|
16
16
|
*/
|
|
17
|
-
import type {
|
|
17
|
+
import type { ClientComponentInfo, ServerRegionInfo } from "./types";
|
|
18
18
|
/**
|
|
19
19
|
* Scan the React fiber tree and return all user-defined client components
|
|
20
20
|
* with their root DOM nodes.
|
|
@@ -22,10 +22,10 @@ import type { ComponentInfo, ServerRegionInfo } from "./types";
|
|
|
22
22
|
* Server regions combine explicit markers with heuristic DOM outside client
|
|
23
23
|
* subtrees (see getServerRegions).
|
|
24
24
|
*/
|
|
25
|
-
export declare function scanFiberTree():
|
|
25
|
+
export declare function scanFiberTree(): ClientComponentInfo[];
|
|
26
26
|
/**
|
|
27
27
|
* Collect server-rendered regions: optional explicit markers plus heuristic
|
|
28
28
|
* nested regions outside client component DOM subtrees.
|
|
29
29
|
*/
|
|
30
|
-
export declare function getServerRegions(clientComponents:
|
|
30
|
+
export declare function getServerRegions(clientComponents: ClientComponentInfo[], container?: HTMLElement): ServerRegionInfo[];
|
|
31
31
|
//# sourceMappingURL=fiber-utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fiber-utils.d.ts","sourceRoot":"","sources":["../src/fiber-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;
|
|
1
|
+
{"version":3,"file":"fiber-utils.d.ts","sourceRoot":"","sources":["../src/fiber-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAqNrE;;;;;;GAMG;AACH,wBAAgB,aAAa,IAAI,mBAAmB,EAAE,CAyBrD;AAwID;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,gBAAgB,EAAE,mBAAmB,EAAE,EACvC,SAAS,CAAC,EAAE,WAAW,GACtB,gBAAgB,EAAE,CAepB"}
|