rsc-boundary 0.1.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/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +2 -0
- package/dist/devtools.d.ts +2 -0
- package/dist/devtools.d.ts.map +1 -0
- package/dist/devtools.js +286 -0
- package/dist/fiber-utils.d.ts +31 -0
- package/dist/fiber-utils.d.ts.map +1 -0
- package/dist/fiber-utils.js +358 -0
- package/dist/highlight.d.ts +30 -0
- package/dist/highlight.d.ts.map +1 -0
- package/dist/highlight.js +125 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/provider.d.ts +33 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +6 -0
- package/dist/server-boundary-marker.d.ts +16 -0
- package/dist/server-boundary-marker.d.ts.map +1 -0
- package/dist/server-boundary-marker.js +10 -0
- package/dist/styles.d.ts +23 -0
- package/dist/styles.d.ts.map +1 -0
- package/dist/styles.js +82 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) RSC Boundary contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# RSC Boundary
|
|
2
|
+
|
|
3
|
+
Visualize the boundary between React Server Components and Client Components in Next.js (App Router). Wrap your root layout with a single provider and get automatic, zero-config highlighting of every client component boundary in your app.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add rsc-boundary
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
In your root layout:
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { RscBoundaryProvider } from "rsc-boundary";
|
|
15
|
+
|
|
16
|
+
export default function RootLayout({
|
|
17
|
+
children,
|
|
18
|
+
}: {
|
|
19
|
+
children: React.ReactNode;
|
|
20
|
+
}) {
|
|
21
|
+
return (
|
|
22
|
+
<html lang="en">
|
|
23
|
+
<body>
|
|
24
|
+
<RscBoundaryProvider>{children}</RscBoundaryProvider>
|
|
25
|
+
</body>
|
|
26
|
+
</html>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
That's it. No changes needed to any other component.
|
|
32
|
+
|
|
33
|
+
## What it does
|
|
34
|
+
|
|
35
|
+
A small floating pill appears in the bottom-left corner of your page during development. Click it to toggle boundary highlighting:
|
|
36
|
+
|
|
37
|
+
- **Orange dashed outlines** around client component subtrees (`"use client"`)
|
|
38
|
+
- **Blue dashed outlines** around server-rendered regions (heuristic **~** or optional **explicit** markers)
|
|
39
|
+
- **Labels** on each region showing the component name / host tag and provenance
|
|
40
|
+
- **Panel** listing client components and server regions with explicit vs heuristic badges
|
|
41
|
+
|
|
42
|
+
In production builds, `RscBoundaryProvider` renders only `{children}` — zero runtime cost, no extra DOM nodes, completely tree-shaken.
|
|
43
|
+
|
|
44
|
+
To force devtools in production (e.g. a documentation site), pass `enabled`:
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
<RscBoundaryProvider enabled>{children}</RscBoundaryProvider>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
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
|
+
|
|
52
|
+
## How it works
|
|
53
|
+
|
|
54
|
+
React Server Components are resolved on the server and sent to the client as pre-rendered HTML. They have **no fibers** in the client-side React tree. Client Components are hydrated and **do** have fibers.
|
|
55
|
+
|
|
56
|
+
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
|
+
|
|
58
|
+
1. Finds every `FunctionComponent`, `ClassComponent`, `ForwardRef`, and `MemoComponent` fiber
|
|
59
|
+
2. Filters out Next.js framework internals (LayoutRouter, ErrorBoundary, etc.)
|
|
60
|
+
3. Maps each remaining user-defined component to its root DOM node(s) — these are your **client component boundaries**
|
|
61
|
+
4. Collects **explicit** regions: elements with `data-rsc-boundary-server` (e.g. `RscServerBoundaryMarker`)
|
|
62
|
+
5. Derives **heuristic** regions by walking the app root: nodes outside every client subtree, minus wrappers that strictly contain a client root — including **nested** server islands, not only top-level siblings
|
|
63
|
+
|
|
64
|
+
A `MutationObserver` watches for DOM changes (route navigation, lazy loading) and re-scans automatically.
|
|
65
|
+
|
|
66
|
+
## Architecture
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
packages/rsc-boundary/src/
|
|
70
|
+
├── index.ts # Public API
|
|
71
|
+
├── constants.ts # data attribute name for explicit markers
|
|
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
|
|
75
|
+
├── fiber-utils.ts # Fiber walk + server region detection
|
|
76
|
+
├── highlight.ts # Outlines, labels, MutationObserver
|
|
77
|
+
├── styles.ts
|
|
78
|
+
└── types.ts
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Limitations
|
|
82
|
+
|
|
83
|
+
- **Uses React internals** (`__reactFiber$*`): same approach React DevTools uses. Dev-only, so stability risk is low.
|
|
84
|
+
- **Heuristic server names**: regions outside client subtrees are labeled by host tag / id unless you add `data-rsc-boundary-server` or `RscServerBoundaryMarker` for an explicit label.
|
|
85
|
+
- **Slots inside client trees**: DOM passed as children into a client component is still under that client root for highlighting; use explicit markers if you need a named server region there.
|
|
86
|
+
- **Next.js internal filtering**: maintains a list of known Next.js internal component names to exclude. May need updates when Next.js adds or renames internals.
|
|
87
|
+
|
|
88
|
+
## Requirements
|
|
89
|
+
|
|
90
|
+
- Next.js 16+ (App Router)
|
|
91
|
+
- React 19+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,eAAO,MAAM,yBAAyB,6BAA6B,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devtools.d.ts","sourceRoot":"","sources":["../src/devtools.tsx"],"names":[],"mappings":"AA0FA,wBAAgB,WAAW,4CAE1B"}
|
package/dist/devtools.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
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 by default, or when `enabled` is set).
|
|
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 { COLORS, PILL_STYLES, PILL_ACTIVE_STYLES, PANEL_STYLES, applyStyles, } from "./styles";
|
|
20
|
+
function componentListEqual(a, b) {
|
|
21
|
+
if (a.length !== b.length) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
for (let i = 0; i < a.length; i++) {
|
|
25
|
+
const ai = a[i];
|
|
26
|
+
const bi = b[i];
|
|
27
|
+
if (ai === undefined || bi === undefined) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
if (ai.name !== bi.name) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const na = ai.domNodes;
|
|
34
|
+
const nb = bi.domNodes;
|
|
35
|
+
if (na.length !== nb.length) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
for (let j = 0; j < na.length; j++) {
|
|
39
|
+
const naJ = na[j];
|
|
40
|
+
const nbJ = nb[j];
|
|
41
|
+
if (naJ !== nbJ) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
function serverRegionsEqual(a, b) {
|
|
49
|
+
if (a.length !== b.length) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
for (let i = 0; i < a.length; i++) {
|
|
53
|
+
const ai = a[i];
|
|
54
|
+
const bi = b[i];
|
|
55
|
+
if (ai === undefined || bi === undefined) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (ai.element !== bi.element) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
if (ai.source !== bi.source) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
if (ai.displayLabel !== bi.displayLabel) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
export function RscDevtools() {
|
|
71
|
+
return _jsx(RscDevtoolsInner, {});
|
|
72
|
+
}
|
|
73
|
+
function RscDevtoolsInner() {
|
|
74
|
+
const [active, setActive] = useState(false);
|
|
75
|
+
const [panelOpen, setPanelOpen] = useState(false);
|
|
76
|
+
const [components, setComponents] = useState([]);
|
|
77
|
+
const [serverRegions, setServerRegions] = useState([]);
|
|
78
|
+
const cleanupRef = useRef(null);
|
|
79
|
+
const scan = useCallback(() => {
|
|
80
|
+
const clientComponents = scanFiberTree();
|
|
81
|
+
const nextServerRegions = getServerRegions(clientComponents);
|
|
82
|
+
applyHighlights(clientComponents, nextServerRegions);
|
|
83
|
+
setComponents((prev) => componentListEqual(prev, clientComponents) ? prev : clientComponents);
|
|
84
|
+
setServerRegions((prev) => serverRegionsEqual(prev, nextServerRegions) ? prev : nextServerRegions);
|
|
85
|
+
}, []);
|
|
86
|
+
const activate = useCallback(() => {
|
|
87
|
+
// Delay scan slightly to let React finish any pending renders
|
|
88
|
+
requestAnimationFrame(() => {
|
|
89
|
+
scan();
|
|
90
|
+
cleanupRef.current = observeDomChanges(scan);
|
|
91
|
+
});
|
|
92
|
+
}, [scan]);
|
|
93
|
+
const deactivate = useCallback(() => {
|
|
94
|
+
removeHighlights();
|
|
95
|
+
cleanupRef.current?.();
|
|
96
|
+
cleanupRef.current = null;
|
|
97
|
+
setComponents([]);
|
|
98
|
+
setServerRegions([]);
|
|
99
|
+
}, []);
|
|
100
|
+
const handleToggle = useCallback(() => {
|
|
101
|
+
setActive((prev) => {
|
|
102
|
+
const next = !prev;
|
|
103
|
+
if (next) {
|
|
104
|
+
activate();
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
deactivate();
|
|
108
|
+
setPanelOpen(false);
|
|
109
|
+
}
|
|
110
|
+
return next;
|
|
111
|
+
});
|
|
112
|
+
}, [activate, deactivate]);
|
|
113
|
+
const handlePanelToggle = useCallback((e) => {
|
|
114
|
+
e.stopPropagation();
|
|
115
|
+
if (active) {
|
|
116
|
+
setPanelOpen((prev) => !prev);
|
|
117
|
+
}
|
|
118
|
+
}, [active]);
|
|
119
|
+
useEffect(() => {
|
|
120
|
+
return () => {
|
|
121
|
+
removeHighlights();
|
|
122
|
+
cleanupRef.current?.();
|
|
123
|
+
};
|
|
124
|
+
}, []);
|
|
125
|
+
return (_jsxs(_Fragment, { children: [panelOpen && active && (_jsx(Panel, { components: components, serverRegions: serverRegions })), _jsx(Pill, { active: active, onToggle: handleToggle, onPanelToggle: handlePanelToggle, clientCount: components.length })] }));
|
|
126
|
+
}
|
|
127
|
+
function Pill({ active, onToggle, onPanelToggle, clientCount }) {
|
|
128
|
+
const pillRef = useRef(null);
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (!pillRef.current)
|
|
131
|
+
return;
|
|
132
|
+
applyStyles(pillRef.current, PILL_STYLES);
|
|
133
|
+
if (active) {
|
|
134
|
+
applyStyles(pillRef.current, PILL_ACTIVE_STYLES);
|
|
135
|
+
}
|
|
136
|
+
}, [active]);
|
|
137
|
+
return (_jsxs("button", { ref: pillRef, type: "button", onClick: onToggle, "data-rsc-devtools": "", "aria-label": "Toggle RSC Boundary highlighting", title: "Toggle RSC Boundary highlighting", children: [_jsx("span", { style: {
|
|
138
|
+
display: "inline-block",
|
|
139
|
+
width: 8,
|
|
140
|
+
height: 8,
|
|
141
|
+
borderRadius: "50%",
|
|
142
|
+
background: active ? COLORS.client.outline : "rgba(255,255,255,0.4)",
|
|
143
|
+
transition: "background 150ms ease",
|
|
144
|
+
} }), _jsx("span", { children: "RSC" }), active && (_jsx("span", { role: "button", tabIndex: 0, onClick: onPanelToggle, onKeyDown: (e) => {
|
|
145
|
+
if (e.key === "Enter" || e.key === " ") {
|
|
146
|
+
onPanelToggle(e);
|
|
147
|
+
}
|
|
148
|
+
}, "data-rsc-devtools": "", style: {
|
|
149
|
+
color: "rgba(255,255,255,0.7)",
|
|
150
|
+
cursor: "pointer",
|
|
151
|
+
padding: "0 0 0 4px",
|
|
152
|
+
fontSize: "12px",
|
|
153
|
+
fontFamily: "inherit",
|
|
154
|
+
lineHeight: "1",
|
|
155
|
+
}, "aria-label": "Toggle component panel", title: "Toggle component panel", children: clientCount > 0 ? `(${clientCount})` : "···" }))] }));
|
|
156
|
+
}
|
|
157
|
+
function Panel({ components, serverRegions }) {
|
|
158
|
+
const panelRef = useRef(null);
|
|
159
|
+
const [tab, setTab] = useState("client");
|
|
160
|
+
useEffect(() => {
|
|
161
|
+
if (!panelRef.current)
|
|
162
|
+
return;
|
|
163
|
+
applyStyles(panelRef.current, PANEL_STYLES);
|
|
164
|
+
}, []);
|
|
165
|
+
const tabBase = {
|
|
166
|
+
flex: 1,
|
|
167
|
+
display: "flex",
|
|
168
|
+
flexDirection: "column",
|
|
169
|
+
alignItems: "center",
|
|
170
|
+
justifyContent: "center",
|
|
171
|
+
gap: 2,
|
|
172
|
+
minHeight: 44,
|
|
173
|
+
padding: "6px 6px 8px",
|
|
174
|
+
border: "none",
|
|
175
|
+
borderRadius: "4px 4px 0 0",
|
|
176
|
+
background: "transparent",
|
|
177
|
+
cursor: "pointer",
|
|
178
|
+
fontFamily: "inherit",
|
|
179
|
+
};
|
|
180
|
+
const tabLine1 = {
|
|
181
|
+
fontSize: 14,
|
|
182
|
+
fontWeight: 600,
|
|
183
|
+
lineHeight: 1.15,
|
|
184
|
+
fontVariantNumeric: "tabular-nums",
|
|
185
|
+
};
|
|
186
|
+
const tabLine2 = {
|
|
187
|
+
fontSize: 10,
|
|
188
|
+
fontWeight: 500,
|
|
189
|
+
lineHeight: 1.2,
|
|
190
|
+
textAlign: "center",
|
|
191
|
+
whiteSpace: "nowrap",
|
|
192
|
+
};
|
|
193
|
+
return (_jsxs("div", { ref: panelRef, "data-rsc-devtools": "", children: [_jsx("div", { style: {
|
|
194
|
+
marginBottom: 10,
|
|
195
|
+
paddingBottom: 8,
|
|
196
|
+
borderBottom: "1px solid rgba(255,255,255,0.1)",
|
|
197
|
+
fontSize: 13,
|
|
198
|
+
fontWeight: 600,
|
|
199
|
+
}, children: "RSC Boundaries" }), _jsxs("div", { style: {
|
|
200
|
+
display: "flex",
|
|
201
|
+
flexDirection: "column",
|
|
202
|
+
gap: 6,
|
|
203
|
+
marginBottom: 10,
|
|
204
|
+
fontSize: 11,
|
|
205
|
+
}, 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: { color: "rgba(255,255,255,0.45)", fontSize: 10, lineHeight: 1.35 }, 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: {
|
|
206
|
+
display: "flex",
|
|
207
|
+
gap: 0,
|
|
208
|
+
marginBottom: 10,
|
|
209
|
+
borderBottom: "1px solid rgba(255,255,255,0.12)",
|
|
210
|
+
}, children: [_jsxs("button", { type: "button", role: "tab", "aria-selected": tab === "client", id: "rsc-panel-tab-client", "aria-controls": "rsc-panel-client", "aria-label": `${components.length} client component${components.length !== 1 ? "s" : ""}`, onClick: () => setTab("client"), style: {
|
|
211
|
+
...tabBase,
|
|
212
|
+
color: tab === "client"
|
|
213
|
+
? "rgba(255,255,255,0.95)"
|
|
214
|
+
: "rgba(255,255,255,0.55)",
|
|
215
|
+
borderBottom: tab === "client"
|
|
216
|
+
? `2px solid ${COLORS.client.outline}`
|
|
217
|
+
: "2px solid transparent",
|
|
218
|
+
marginBottom: -1,
|
|
219
|
+
}, children: [_jsx("span", { style: tabLine1, children: components.length }), _jsxs("span", { style: tabLine2, children: ["client component", components.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: {
|
|
220
|
+
...tabBase,
|
|
221
|
+
color: tab === "server"
|
|
222
|
+
? "rgba(255,255,255,0.95)"
|
|
223
|
+
: "rgba(255,255,255,0.55)",
|
|
224
|
+
borderBottom: tab === "server"
|
|
225
|
+
? `2px solid ${COLORS.server.outline}`
|
|
226
|
+
: "2px solid transparent",
|
|
227
|
+
marginBottom: -1,
|
|
228
|
+
}, 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: components.length > 0 ? (_jsx("div", { style: { display: "flex", flexDirection: "column", gap: 4 }, children: components.map((comp, i) => (_jsx(ComponentEntry, { 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: components.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.") })) }))] }));
|
|
229
|
+
}
|
|
230
|
+
function LegendItem({ color, label }) {
|
|
231
|
+
return (_jsxs("span", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [_jsx("span", { style: {
|
|
232
|
+
display: "inline-block",
|
|
233
|
+
width: 10,
|
|
234
|
+
height: 10,
|
|
235
|
+
borderRadius: 2,
|
|
236
|
+
border: `2px dashed ${color}`,
|
|
237
|
+
} }), label] }));
|
|
238
|
+
}
|
|
239
|
+
function ServerRegionEntry({ region }) {
|
|
240
|
+
const provenance = region.source === "explicit" ? "explicit" : "~";
|
|
241
|
+
return (_jsxs("div", { style: {
|
|
242
|
+
display: "flex",
|
|
243
|
+
alignItems: "center",
|
|
244
|
+
gap: 6,
|
|
245
|
+
padding: "3px 6px",
|
|
246
|
+
borderRadius: 4,
|
|
247
|
+
background: "rgba(59, 130, 246, 0.12)",
|
|
248
|
+
fontSize: 11,
|
|
249
|
+
}, children: [_jsx("span", { style: {
|
|
250
|
+
width: 6,
|
|
251
|
+
height: 6,
|
|
252
|
+
borderRadius: "50%",
|
|
253
|
+
background: COLORS.server.outline,
|
|
254
|
+
flexShrink: 0,
|
|
255
|
+
} }), _jsx("span", { style: {
|
|
256
|
+
minWidth: 52,
|
|
257
|
+
flexShrink: 0,
|
|
258
|
+
fontSize: 9,
|
|
259
|
+
fontWeight: 600,
|
|
260
|
+
letterSpacing: "0.02em",
|
|
261
|
+
textTransform: "uppercase",
|
|
262
|
+
color: region.source === "explicit"
|
|
263
|
+
? "rgba(147, 197, 253, 0.95)"
|
|
264
|
+
: "rgba(255,255,255,0.45)",
|
|
265
|
+
}, children: provenance }), _jsx("span", { style: {
|
|
266
|
+
color: "rgba(255,255,255,0.9)",
|
|
267
|
+
fontFamily: "ui-monospace, monospace",
|
|
268
|
+
}, children: region.displayLabel })] }));
|
|
269
|
+
}
|
|
270
|
+
function ComponentEntry({ component }) {
|
|
271
|
+
return (_jsxs("div", { style: {
|
|
272
|
+
display: "flex",
|
|
273
|
+
alignItems: "center",
|
|
274
|
+
gap: 6,
|
|
275
|
+
padding: "3px 6px",
|
|
276
|
+
borderRadius: 4,
|
|
277
|
+
background: "rgba(249, 115, 22, 0.1)",
|
|
278
|
+
fontSize: 11,
|
|
279
|
+
}, children: [_jsx("span", { style: {
|
|
280
|
+
width: 6,
|
|
281
|
+
height: 6,
|
|
282
|
+
borderRadius: "50%",
|
|
283
|
+
background: COLORS.client.outline,
|
|
284
|
+
flexShrink: 0,
|
|
285
|
+
} }), _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"] }))] }));
|
|
286
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React fiber tree utilities for automatic RSC Boundary detection.
|
|
3
|
+
*
|
|
4
|
+
* How it works:
|
|
5
|
+
* - React Server Components are resolved on the server and sent as pre-rendered
|
|
6
|
+
* HTML. They have NO corresponding fibers in the client-side React tree.
|
|
7
|
+
* - Client Components ("use client") are hydrated on the client and DO have
|
|
8
|
+
* fibers (FunctionComponent, ForwardRef, MemoComponent, etc.).
|
|
9
|
+
*
|
|
10
|
+
* By walking the fiber tree after hydration, we find every user-defined client
|
|
11
|
+
* component and map it to its root DOM node(s). Server regions are explicit
|
|
12
|
+
* markers plus heuristic DOM outside those client subtrees (see getServerRegions).
|
|
13
|
+
*
|
|
14
|
+
* This approach mirrors what React DevTools does internally — it accesses the
|
|
15
|
+
* __reactFiber$* property that React attaches to DOM elements during hydration.
|
|
16
|
+
*/
|
|
17
|
+
import type { ComponentInfo, ServerRegionInfo } from "./types";
|
|
18
|
+
/**
|
|
19
|
+
* Scan the React fiber tree and return all user-defined client components
|
|
20
|
+
* with their root DOM nodes.
|
|
21
|
+
*
|
|
22
|
+
* Server regions combine explicit markers with heuristic DOM outside client
|
|
23
|
+
* subtrees (see getServerRegions).
|
|
24
|
+
*/
|
|
25
|
+
export declare function scanFiberTree(): ComponentInfo[];
|
|
26
|
+
/**
|
|
27
|
+
* Collect server-rendered regions: optional explicit markers plus heuristic
|
|
28
|
+
* nested regions outside client component DOM subtrees.
|
|
29
|
+
*/
|
|
30
|
+
export declare function getServerRegions(clientComponents: ComponentInfo[], container?: HTMLElement): ServerRegionInfo[];
|
|
31
|
+
//# sourceMappingURL=fiber-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fiber-utils.d.ts","sourceRoot":"","sources":["../src/fiber-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAsM/D;;;;;;GAMG;AACH,wBAAgB,aAAa,IAAI,aAAa,EAAE,CAyB/C;AAoJD;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,gBAAgB,EAAE,aAAa,EAAE,EACjC,SAAS,CAAC,EAAE,WAAW,GACtB,gBAAgB,EAAE,CAepB"}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React fiber tree utilities for automatic RSC Boundary detection.
|
|
3
|
+
*
|
|
4
|
+
* How it works:
|
|
5
|
+
* - React Server Components are resolved on the server and sent as pre-rendered
|
|
6
|
+
* HTML. They have NO corresponding fibers in the client-side React tree.
|
|
7
|
+
* - Client Components ("use client") are hydrated on the client and DO have
|
|
8
|
+
* fibers (FunctionComponent, ForwardRef, MemoComponent, etc.).
|
|
9
|
+
*
|
|
10
|
+
* By walking the fiber tree after hydration, we find every user-defined client
|
|
11
|
+
* component and map it to its root DOM node(s). Server regions are explicit
|
|
12
|
+
* markers plus heuristic DOM outside those client subtrees (see getServerRegions).
|
|
13
|
+
*
|
|
14
|
+
* This approach mirrors what React DevTools does internally — it accesses the
|
|
15
|
+
* __reactFiber$* property that React attaches to DOM elements during hydration.
|
|
16
|
+
*/
|
|
17
|
+
import { SERVER_BOUNDARY_DATA_ATTR } from "./constants";
|
|
18
|
+
// React fiber work tags (numeric constants from React source)
|
|
19
|
+
const FUNCTION_COMPONENT = 0;
|
|
20
|
+
const CLASS_COMPONENT = 1;
|
|
21
|
+
const HOST_COMPONENT = 5;
|
|
22
|
+
const FORWARD_REF = 11;
|
|
23
|
+
const MEMO_COMPONENT = 14;
|
|
24
|
+
const SIMPLE_MEMO_COMPONENT = 15;
|
|
25
|
+
const COMPONENT_TAGS = new Set([
|
|
26
|
+
FUNCTION_COMPONENT,
|
|
27
|
+
CLASS_COMPONENT,
|
|
28
|
+
FORWARD_REF,
|
|
29
|
+
MEMO_COMPONENT,
|
|
30
|
+
SIMPLE_MEMO_COMPONENT,
|
|
31
|
+
]);
|
|
32
|
+
/**
|
|
33
|
+
* Next.js internal client component names to filter out.
|
|
34
|
+
* These are framework components that appear in the fiber tree but are not
|
|
35
|
+
* user-authored — highlighting them would add noise rather than insight.
|
|
36
|
+
*/
|
|
37
|
+
const NEXT_INTERNALS = new Set([
|
|
38
|
+
"AppRouter",
|
|
39
|
+
"HotReload",
|
|
40
|
+
"Router",
|
|
41
|
+
"LayoutRouter",
|
|
42
|
+
"InnerLayoutRouter",
|
|
43
|
+
"OuterLayoutRouter",
|
|
44
|
+
"RenderFromTemplateContext",
|
|
45
|
+
"ErrorBoundary",
|
|
46
|
+
"ErrorBoundaryHandler",
|
|
47
|
+
"GlobalError",
|
|
48
|
+
"RedirectBoundary",
|
|
49
|
+
"RedirectErrorBoundary",
|
|
50
|
+
"NotFoundBoundary",
|
|
51
|
+
"NotFoundErrorBoundary",
|
|
52
|
+
"LoadingBoundary",
|
|
53
|
+
"ScrollAndFocusHandler",
|
|
54
|
+
"InnerScrollAndFocusHandler",
|
|
55
|
+
"RootLayoutBoundary",
|
|
56
|
+
"RootErrorBoundary",
|
|
57
|
+
"ReactDevOverlay",
|
|
58
|
+
"DevToolsIndicator",
|
|
59
|
+
"DevRootNotFoundBoundary",
|
|
60
|
+
"MetadataBoundary",
|
|
61
|
+
"ViewportBoundary",
|
|
62
|
+
"OutletBoundary",
|
|
63
|
+
"HTTPAccessFallbackBoundary",
|
|
64
|
+
"HTTPAccessErrorBoundary",
|
|
65
|
+
// Our own devtools root components (sub-components filtered via DOM check)
|
|
66
|
+
"RscDevtools",
|
|
67
|
+
"RscDevtoolsInner",
|
|
68
|
+
]);
|
|
69
|
+
/**
|
|
70
|
+
* Find the __reactFiber$* property key on a DOM element.
|
|
71
|
+
* React attaches a randomly-suffixed key per render root.
|
|
72
|
+
*/
|
|
73
|
+
function getReactFiberKey(element) {
|
|
74
|
+
for (const key of Object.keys(element)) {
|
|
75
|
+
if (key.startsWith("__reactFiber$"))
|
|
76
|
+
return key;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get the React fiber attached to a DOM element, if any.
|
|
82
|
+
*/
|
|
83
|
+
function getFiber(element) {
|
|
84
|
+
const key = getReactFiberKey(element);
|
|
85
|
+
if (!key)
|
|
86
|
+
return null;
|
|
87
|
+
return element[key] ?? null;
|
|
88
|
+
}
|
|
89
|
+
function isFiber(value) {
|
|
90
|
+
return (typeof value === "object" &&
|
|
91
|
+
value !== null &&
|
|
92
|
+
"tag" in value &&
|
|
93
|
+
"child" in value &&
|
|
94
|
+
"sibling" in value);
|
|
95
|
+
}
|
|
96
|
+
function getComponentName(fiber) {
|
|
97
|
+
const type = fiber.type;
|
|
98
|
+
if (!type)
|
|
99
|
+
return null;
|
|
100
|
+
if (typeof type === "function") {
|
|
101
|
+
return type.displayName ??
|
|
102
|
+
type.name ?? null;
|
|
103
|
+
}
|
|
104
|
+
if (typeof type === "object" && type !== null) {
|
|
105
|
+
// ForwardRef wraps a render function
|
|
106
|
+
if ("render" in type && typeof type.render === "function") {
|
|
107
|
+
return type.render.displayName ??
|
|
108
|
+
type.render.name ?? null;
|
|
109
|
+
}
|
|
110
|
+
// MemoComponent wraps a type
|
|
111
|
+
if ("type" in type) {
|
|
112
|
+
const inner = type.type;
|
|
113
|
+
if (typeof inner === "function") {
|
|
114
|
+
return inner.displayName ??
|
|
115
|
+
inner.name ?? null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return type.displayName ?? type.name ?? null;
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
function isUserComponent(fiber) {
|
|
123
|
+
if (!COMPONENT_TAGS.has(fiber.tag))
|
|
124
|
+
return false;
|
|
125
|
+
const name = getComponentName(fiber);
|
|
126
|
+
if (!name)
|
|
127
|
+
return false;
|
|
128
|
+
if (NEXT_INTERNALS.has(name))
|
|
129
|
+
return false;
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Walk down from a component fiber to find its root DOM node(s).
|
|
134
|
+
* Stops at the first HostComponent layer — does not recurse into
|
|
135
|
+
* child components.
|
|
136
|
+
*/
|
|
137
|
+
function getRootDomNodes(fiber) {
|
|
138
|
+
const nodes = [];
|
|
139
|
+
function collect(f) {
|
|
140
|
+
if (!f)
|
|
141
|
+
return;
|
|
142
|
+
if (f.tag === HOST_COMPONENT && f.stateNode instanceof HTMLElement) {
|
|
143
|
+
nodes.push(f.stateNode);
|
|
144
|
+
return; // don't go deeper — this is the DOM root for this subtree
|
|
145
|
+
}
|
|
146
|
+
// Skip into child component fibers but only collect their DOM if
|
|
147
|
+
// they are NOT themselves user components (which get their own entry).
|
|
148
|
+
if (COMPONENT_TAGS.has(f.tag) && f !== fiber && isUserComponent(f)) {
|
|
149
|
+
return; // this child component will be reported separately
|
|
150
|
+
}
|
|
151
|
+
collect(f.child);
|
|
152
|
+
collect(f.sibling);
|
|
153
|
+
}
|
|
154
|
+
collect(fiber.child);
|
|
155
|
+
return nodes;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Find the fiber root by starting at a DOM element and walking up.
|
|
159
|
+
*/
|
|
160
|
+
function findFiberRoot() {
|
|
161
|
+
const candidates = [
|
|
162
|
+
document.getElementById("__next"),
|
|
163
|
+
document.body,
|
|
164
|
+
document.documentElement,
|
|
165
|
+
];
|
|
166
|
+
for (const el of candidates) {
|
|
167
|
+
if (!el)
|
|
168
|
+
continue;
|
|
169
|
+
const raw = getFiber(el);
|
|
170
|
+
if (!isFiber(raw))
|
|
171
|
+
continue;
|
|
172
|
+
let root = raw;
|
|
173
|
+
while (root.return)
|
|
174
|
+
root = root.return;
|
|
175
|
+
return root;
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Check whether a DOM node lives inside our own devtools overlay.
|
|
181
|
+
*/
|
|
182
|
+
function isInsideDevtools(el) {
|
|
183
|
+
let current = el;
|
|
184
|
+
while (current) {
|
|
185
|
+
if (current.dataset.rscDevtools != null)
|
|
186
|
+
return true;
|
|
187
|
+
current = current.parentElement;
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Scan the React fiber tree and return all user-defined client components
|
|
193
|
+
* with their root DOM nodes.
|
|
194
|
+
*
|
|
195
|
+
* Server regions combine explicit markers with heuristic DOM outside client
|
|
196
|
+
* subtrees (see getServerRegions).
|
|
197
|
+
*/
|
|
198
|
+
export function scanFiberTree() {
|
|
199
|
+
const root = findFiberRoot();
|
|
200
|
+
if (!root)
|
|
201
|
+
return [];
|
|
202
|
+
const components = [];
|
|
203
|
+
function walk(fiber) {
|
|
204
|
+
if (!fiber)
|
|
205
|
+
return;
|
|
206
|
+
if (isUserComponent(fiber)) {
|
|
207
|
+
const name = getComponentName(fiber) ?? "Anonymous";
|
|
208
|
+
const domNodes = getRootDomNodes(fiber).filter((node) => !isInsideDevtools(node));
|
|
209
|
+
if (domNodes.length > 0) {
|
|
210
|
+
components.push({ name, domNodes });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
walk(fiber.child);
|
|
214
|
+
walk(fiber.sibling);
|
|
215
|
+
}
|
|
216
|
+
walk(root.child);
|
|
217
|
+
return components;
|
|
218
|
+
}
|
|
219
|
+
function isInsideClientBoundary(el, clientRoots) {
|
|
220
|
+
let current = el;
|
|
221
|
+
while (current) {
|
|
222
|
+
if (clientRoots.has(current))
|
|
223
|
+
return true;
|
|
224
|
+
current = current.parentElement;
|
|
225
|
+
}
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* True if `el` is a strict DOM ancestor of any client component root.
|
|
230
|
+
* Those nodes are excluded from heuristic server highlights so we don't draw
|
|
231
|
+
* a box around a wrapper that contains a client boundary.
|
|
232
|
+
*/
|
|
233
|
+
function isStrictAncestorOfAnyClientRoot(el, clientRoots) {
|
|
234
|
+
for (const root of clientRoots) {
|
|
235
|
+
if (el !== root && el.contains(root))
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* True if `el` is the explicit marker element or a descendant of one.
|
|
242
|
+
*/
|
|
243
|
+
function isInsideExplicitMarkerSubtree(el, explicitRoots) {
|
|
244
|
+
let current = el;
|
|
245
|
+
while (current) {
|
|
246
|
+
if (explicitRoots.has(current))
|
|
247
|
+
return true;
|
|
248
|
+
current = current.parentElement;
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
function* elementDescendants(root) {
|
|
253
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
254
|
+
let n = walker.nextNode();
|
|
255
|
+
while (n) {
|
|
256
|
+
if (n instanceof HTMLElement && n !== root) {
|
|
257
|
+
yield n;
|
|
258
|
+
}
|
|
259
|
+
n = walker.nextNode();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function collectExplicitServerRegions() {
|
|
263
|
+
const selector = `[${SERVER_BOUNDARY_DATA_ATTR}]`;
|
|
264
|
+
const regions = [];
|
|
265
|
+
for (const el of document.querySelectorAll(selector)) {
|
|
266
|
+
if (!(el instanceof HTMLElement))
|
|
267
|
+
continue;
|
|
268
|
+
if (isInsideDevtools(el))
|
|
269
|
+
continue;
|
|
270
|
+
const raw = el.getAttribute(SERVER_BOUNDARY_DATA_ATTR) ?? "";
|
|
271
|
+
const name = raw.trim();
|
|
272
|
+
const displayLabel = name.length > 0 ? name : hostFallbackLabel(el);
|
|
273
|
+
regions.push({
|
|
274
|
+
element: el,
|
|
275
|
+
displayLabel,
|
|
276
|
+
source: "explicit",
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return regions;
|
|
280
|
+
}
|
|
281
|
+
function hostFallbackLabel(el) {
|
|
282
|
+
const tag = el.tagName.toLowerCase();
|
|
283
|
+
if (el.id)
|
|
284
|
+
return `<${tag}#${el.id}>`;
|
|
285
|
+
return `<${tag}>`;
|
|
286
|
+
}
|
|
287
|
+
function heuristicRegionPanelLabel(el, allRoots) {
|
|
288
|
+
const tag = el.tagName.toLowerCase();
|
|
289
|
+
if (el.id) {
|
|
290
|
+
return `<${tag}#${el.id}>`;
|
|
291
|
+
}
|
|
292
|
+
const sameTag = allRoots.filter((e) => e.tagName === el.tagName);
|
|
293
|
+
if (sameTag.length === 1) {
|
|
294
|
+
return `<${tag}>`;
|
|
295
|
+
}
|
|
296
|
+
const n = sameTag.indexOf(el) + 1;
|
|
297
|
+
return `<${tag}> (${n})`;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Heuristic server regions: DOM nodes outside every client component subtree,
|
|
301
|
+
* excluding wrappers that strictly contain a client root, and excluding
|
|
302
|
+
* subtrees already covered by explicit markers.
|
|
303
|
+
*/
|
|
304
|
+
function collectHeuristicServerRegions(clientComponents, explicitMarkerElements, container) {
|
|
305
|
+
const clientRoots = new Set();
|
|
306
|
+
for (const comp of clientComponents) {
|
|
307
|
+
for (const node of comp.domNodes) {
|
|
308
|
+
clientRoots.add(node);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const inHeuristicCandidate = (el) => {
|
|
312
|
+
if (el === container)
|
|
313
|
+
return false;
|
|
314
|
+
if (!container.contains(el))
|
|
315
|
+
return false;
|
|
316
|
+
if (isInsideDevtools(el))
|
|
317
|
+
return false;
|
|
318
|
+
if (isInsideClientBoundary(el, clientRoots))
|
|
319
|
+
return false;
|
|
320
|
+
if (isInsideExplicitMarkerSubtree(el, explicitMarkerElements))
|
|
321
|
+
return false;
|
|
322
|
+
return true;
|
|
323
|
+
};
|
|
324
|
+
const filtered = new Set();
|
|
325
|
+
for (const el of elementDescendants(container)) {
|
|
326
|
+
if (!inHeuristicCandidate(el))
|
|
327
|
+
continue;
|
|
328
|
+
if (isStrictAncestorOfAnyClientRoot(el, clientRoots))
|
|
329
|
+
continue;
|
|
330
|
+
filtered.add(el);
|
|
331
|
+
}
|
|
332
|
+
const roots = [];
|
|
333
|
+
for (const el of filtered) {
|
|
334
|
+
const parent = el.parentElement;
|
|
335
|
+
const parentIn = parent !== null &&
|
|
336
|
+
parent !== container &&
|
|
337
|
+
filtered.has(parent);
|
|
338
|
+
if (!parentIn) {
|
|
339
|
+
roots.push(el);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return roots.map((element) => ({
|
|
343
|
+
element,
|
|
344
|
+
displayLabel: heuristicRegionPanelLabel(element, roots),
|
|
345
|
+
source: "heuristic",
|
|
346
|
+
}));
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Collect server-rendered regions: optional explicit markers plus heuristic
|
|
350
|
+
* nested regions outside client component DOM subtrees.
|
|
351
|
+
*/
|
|
352
|
+
export function getServerRegions(clientComponents, container) {
|
|
353
|
+
const root = container ?? document.getElementById("__next") ?? document.body;
|
|
354
|
+
const explicit = collectExplicitServerRegions();
|
|
355
|
+
const explicitRoots = new Set(explicit.map((r) => r.element));
|
|
356
|
+
const heuristic = collectHeuristicServerRegions(clientComponents, explicitRoots, root);
|
|
357
|
+
return [...explicit, ...heuristic];
|
|
358
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM highlighting for RSC Boundary visualization.
|
|
3
|
+
*
|
|
4
|
+
* When active, this module:
|
|
5
|
+
* 1. Takes the list of client components (from fiber-utils) and applies
|
|
6
|
+
* orange dashed outlines to their root DOM elements.
|
|
7
|
+
* 2. Identifies server regions (explicit markers and/or heuristic DOM outside
|
|
8
|
+
* client subtrees) and applies blue dashed outlines.
|
|
9
|
+
* 3. Injects small floating labels (client name; server label + explicit vs ~).
|
|
10
|
+
* 4. Sets up a MutationObserver to re-scan when the DOM changes
|
|
11
|
+
* (e.g. route navigation, lazy-loaded content).
|
|
12
|
+
*
|
|
13
|
+
* All styling uses inline styles — no global CSS is injected.
|
|
14
|
+
*/
|
|
15
|
+
import type { ComponentInfo, ServerRegionInfo } from "./types";
|
|
16
|
+
/**
|
|
17
|
+
* Apply highlights to detected client components and server regions.
|
|
18
|
+
*/
|
|
19
|
+
export declare function applyHighlights(clientComponents: ComponentInfo[], serverRegions: ServerRegionInfo[]): void;
|
|
20
|
+
/**
|
|
21
|
+
* Remove all active highlights and restore original styles.
|
|
22
|
+
*/
|
|
23
|
+
export declare function removeHighlights(): void;
|
|
24
|
+
/**
|
|
25
|
+
* Start a MutationObserver that calls `onMutation` when the DOM subtree
|
|
26
|
+
* changes (route transitions, lazy loading, etc.).
|
|
27
|
+
* Returns a cleanup function to disconnect the observer.
|
|
28
|
+
*/
|
|
29
|
+
export declare function observeDomChanges(onMutation: () => void): () => void;
|
|
30
|
+
//# sourceMappingURL=highlight.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"highlight.d.ts","sourceRoot":"","sources":["../src/highlight.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAsB,MAAM,SAAS,CAAC;AAsFnF;;GAEG;AACH,wBAAgB,eAAe,CAC7B,gBAAgB,EAAE,aAAa,EAAE,EACjC,aAAa,EAAE,gBAAgB,EAAE,GAChC,IAAI,CAqBN;AAYD;;GAEG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CAmBpE"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM highlighting for RSC Boundary visualization.
|
|
3
|
+
*
|
|
4
|
+
* When active, this module:
|
|
5
|
+
* 1. Takes the list of client components (from fiber-utils) and applies
|
|
6
|
+
* orange dashed outlines to their root DOM elements.
|
|
7
|
+
* 2. Identifies server regions (explicit markers and/or heuristic DOM outside
|
|
8
|
+
* client subtrees) and applies blue dashed outlines.
|
|
9
|
+
* 3. Injects small floating labels (client name; server label + explicit vs ~).
|
|
10
|
+
* 4. Sets up a MutationObserver to re-scan when the DOM changes
|
|
11
|
+
* (e.g. route navigation, lazy-loaded content).
|
|
12
|
+
*
|
|
13
|
+
* All styling uses inline styles — no global CSS is injected.
|
|
14
|
+
*/
|
|
15
|
+
import { COLORS, LABEL_BASE_STYLES, applyStyles } from "./styles";
|
|
16
|
+
const HIGHLIGHT_ATTR = "data-rsc-highlight";
|
|
17
|
+
const LABEL_ATTR = "data-rsc-label";
|
|
18
|
+
let activeHighlights = [];
|
|
19
|
+
let observer = null;
|
|
20
|
+
const OBSERVER_OPTIONS = {
|
|
21
|
+
childList: true,
|
|
22
|
+
subtree: true,
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Our own highlight/label DOM updates trigger childList mutations. Pause the
|
|
26
|
+
* observer while mutating so we don't debounce-scan → setState in a loop.
|
|
27
|
+
*/
|
|
28
|
+
function withObserverPaused(callback) {
|
|
29
|
+
if (!observer) {
|
|
30
|
+
callback();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
observer.disconnect();
|
|
34
|
+
try {
|
|
35
|
+
callback();
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
observer.observe(document.body, OBSERVER_OPTIONS);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function createLabel(name, kind, serverSource) {
|
|
42
|
+
const label = document.createElement("div");
|
|
43
|
+
label.setAttribute(LABEL_ATTR, "");
|
|
44
|
+
label.setAttribute("data-rsc-devtools", "");
|
|
45
|
+
const prefix = kind === "client"
|
|
46
|
+
? "Client"
|
|
47
|
+
: serverSource === "explicit"
|
|
48
|
+
? "Server (explicit)"
|
|
49
|
+
: "Server (~)";
|
|
50
|
+
label.textContent = `${prefix}: ${name}`;
|
|
51
|
+
applyStyles(label, {
|
|
52
|
+
...LABEL_BASE_STYLES,
|
|
53
|
+
background: kind === "client" ? COLORS.client.label : COLORS.server.label,
|
|
54
|
+
});
|
|
55
|
+
return label;
|
|
56
|
+
}
|
|
57
|
+
function highlightElement(element, name, kind, serverSource) {
|
|
58
|
+
const colors = kind === "client" ? COLORS.client : COLORS.server;
|
|
59
|
+
const originalOutline = element.style.outline;
|
|
60
|
+
const originalPosition = element.style.position;
|
|
61
|
+
element.style.outline = `2px dashed ${colors.outline}`;
|
|
62
|
+
element.setAttribute(HIGHLIGHT_ATTR, kind);
|
|
63
|
+
// Labels need a positioned ancestor to sit correctly
|
|
64
|
+
const computed = globalThis.getComputedStyle(element);
|
|
65
|
+
if (computed.position === "static") {
|
|
66
|
+
element.style.position = "relative";
|
|
67
|
+
}
|
|
68
|
+
const label = createLabel(name, kind, serverSource);
|
|
69
|
+
element.appendChild(label);
|
|
70
|
+
return { element, originalOutline, originalPosition, label };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Apply highlights to detected client components and server regions.
|
|
74
|
+
*/
|
|
75
|
+
export function applyHighlights(clientComponents, serverRegions) {
|
|
76
|
+
withObserverPaused(() => {
|
|
77
|
+
removeHighlightsInternal();
|
|
78
|
+
for (const comp of clientComponents) {
|
|
79
|
+
for (const node of comp.domNodes) {
|
|
80
|
+
activeHighlights.push(highlightElement(node, comp.name, "client"));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const region of serverRegions) {
|
|
84
|
+
activeHighlights.push(highlightElement(region.element, region.displayLabel, "server", region.source));
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function removeHighlightsInternal() {
|
|
89
|
+
for (const entry of activeHighlights) {
|
|
90
|
+
entry.element.style.outline = entry.originalOutline;
|
|
91
|
+
entry.element.style.position = entry.originalPosition;
|
|
92
|
+
entry.element.removeAttribute(HIGHLIGHT_ATTR);
|
|
93
|
+
entry.label.remove();
|
|
94
|
+
}
|
|
95
|
+
activeHighlights = [];
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Remove all active highlights and restore original styles.
|
|
99
|
+
*/
|
|
100
|
+
export function removeHighlights() {
|
|
101
|
+
withObserverPaused(removeHighlightsInternal);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Start a MutationObserver that calls `onMutation` when the DOM subtree
|
|
105
|
+
* changes (route transitions, lazy loading, etc.).
|
|
106
|
+
* Returns a cleanup function to disconnect the observer.
|
|
107
|
+
*/
|
|
108
|
+
export function observeDomChanges(onMutation) {
|
|
109
|
+
if (observer) {
|
|
110
|
+
observer.disconnect();
|
|
111
|
+
}
|
|
112
|
+
let debounceTimer = null;
|
|
113
|
+
observer = new MutationObserver(() => {
|
|
114
|
+
if (debounceTimer)
|
|
115
|
+
clearTimeout(debounceTimer);
|
|
116
|
+
debounceTimer = setTimeout(onMutation, 300);
|
|
117
|
+
});
|
|
118
|
+
observer.observe(document.body, OBSERVER_OPTIONS);
|
|
119
|
+
return () => {
|
|
120
|
+
if (debounceTimer)
|
|
121
|
+
clearTimeout(debounceTimer);
|
|
122
|
+
observer?.disconnect();
|
|
123
|
+
observer = null;
|
|
124
|
+
};
|
|
125
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { SERVER_BOUNDARY_DATA_ATTR } from "./constants";
|
|
2
|
+
export { COLORS, LABEL_BASE_STYLES } from "./styles";
|
|
3
|
+
export type { ComponentInfo, HighlightState, ServerRegionInfo, ServerRegionSource, } from "./types";
|
|
4
|
+
export { RscBoundaryProvider } from "./provider";
|
|
5
|
+
export { RscDevtools } from "./devtools";
|
|
6
|
+
export { RscServerBoundaryMarker } from "./server-boundary-marker";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AACrD,YAAY,EACV,aAAa,EACb,cAAc,EACd,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { SERVER_BOUNDARY_DATA_ATTR } from "./constants";
|
|
2
|
+
export { COLORS, LABEL_BASE_STYLES } from "./styles";
|
|
3
|
+
export { RscBoundaryProvider } from "./provider";
|
|
4
|
+
export { RscDevtools } from "./devtools";
|
|
5
|
+
export { RscServerBoundaryMarker } from "./server-boundary-marker";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RscBoundaryProvider — the single integration point for RSC Boundary.
|
|
3
|
+
*
|
|
4
|
+
* This is a React Server Component (no "use client" directive). Add it once
|
|
5
|
+
* in your root layout:
|
|
6
|
+
*
|
|
7
|
+
* ```tsx
|
|
8
|
+
* import { RscBoundaryProvider } from "rsc-boundary";
|
|
9
|
+
*
|
|
10
|
+
* export default function RootLayout({ children }) {
|
|
11
|
+
* return (
|
|
12
|
+
* <html>
|
|
13
|
+
* <body>
|
|
14
|
+
* <RscBoundaryProvider>{children}</RscBoundaryProvider>
|
|
15
|
+
* </body>
|
|
16
|
+
* </html>
|
|
17
|
+
* );
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
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).
|
|
24
|
+
*/
|
|
25
|
+
import type { ReactNode } from "react";
|
|
26
|
+
interface RscBoundaryProviderProps {
|
|
27
|
+
children: ReactNode;
|
|
28
|
+
/** When `true`, always mount devtools (including production). When omitted, devtools run only in development. */
|
|
29
|
+
enabled?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export declare function RscBoundaryProvider({ children, enabled, }: RscBoundaryProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,UAAU,wBAAwB;IAChC,QAAQ,EAAE,SAAS,CAAC;IACpB,iHAAiH;IACjH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,mBAAmB,CAAC,EAClC,QAAQ,EACR,OAAO,GACR,EAAE,wBAAwB,2CAU1B"}
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { RscDevtools } from "./devtools";
|
|
3
|
+
export function RscBoundaryProvider({ children, enabled, }) {
|
|
4
|
+
const shouldEnable = enabled ?? process.env.NODE_ENV === "development";
|
|
5
|
+
return (_jsxs(_Fragment, { children: [children, shouldEnable ? _jsx(RscDevtools, {}) : null] }));
|
|
6
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
interface RscServerBoundaryMarkerProps {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
/** Shown in devtools when the region source is explicit. */
|
|
5
|
+
label?: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Server Component wrapper that opts a subtree into **explicit** server-region
|
|
10
|
+
* detection. Add `label` so the devtools panel shows a stable name.
|
|
11
|
+
*
|
|
12
|
+
* Heuristic detection still runs elsewhere; this marker is optional.
|
|
13
|
+
*/
|
|
14
|
+
export declare function RscServerBoundaryMarker({ children, label, className, }: RscServerBoundaryMarkerProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=server-boundary-marker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-boundary-marker.d.ts","sourceRoot":"","sources":["../src/server-boundary-marker.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,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,2CAM9B"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Server Component wrapper that opts a subtree into **explicit** server-region
|
|
4
|
+
* detection. Add `label` so the devtools panel shows a stable name.
|
|
5
|
+
*
|
|
6
|
+
* Heuristic detection still runs elsewhere; this marker is optional.
|
|
7
|
+
*/
|
|
8
|
+
export function RscServerBoundaryMarker({ children, label, className, }) {
|
|
9
|
+
return (_jsx("div", { className: className, "data-rsc-boundary-server": label ?? "", children: children }));
|
|
10
|
+
}
|
package/dist/styles.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS-in-JS constants for the devtools overlay.
|
|
3
|
+
* All styles are applied via inline styles or injected style elements
|
|
4
|
+
* to avoid requiring a CSS file or global stylesheet.
|
|
5
|
+
*/
|
|
6
|
+
export declare const COLORS: {
|
|
7
|
+
readonly client: {
|
|
8
|
+
readonly outline: "rgba(249, 115, 22, 0.8)";
|
|
9
|
+
readonly background: "rgba(249, 115, 22, 0.12)";
|
|
10
|
+
readonly label: "rgba(249, 115, 22, 0.95)";
|
|
11
|
+
};
|
|
12
|
+
readonly server: {
|
|
13
|
+
readonly outline: "rgba(59, 130, 246, 0.8)";
|
|
14
|
+
readonly background: "rgba(59, 130, 246, 0.12)";
|
|
15
|
+
readonly label: "rgba(59, 130, 246, 0.95)";
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export declare const PILL_STYLES: Record<string, string>;
|
|
19
|
+
export declare const PILL_ACTIVE_STYLES: Partial<typeof PILL_STYLES>;
|
|
20
|
+
export declare const PANEL_STYLES: Record<string, string>;
|
|
21
|
+
export declare const LABEL_BASE_STYLES: Record<string, string>;
|
|
22
|
+
export declare function applyStyles(el: HTMLElement, styles: Record<string, string>): void;
|
|
23
|
+
//# sourceMappingURL=styles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"styles.d.ts","sourceRoot":"","sources":["../src/styles.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,eAAO,MAAM,MAAM;;;;;;;;;;;CAWT,CAAC;AAEX,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAuB9C,CAAC;AAEF,eAAO,MAAM,kBAAkB,EAAE,OAAO,CAAC,OAAO,WAAW,CAG1D,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAkB/C,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAepD,CAAC;AAEF,wBAAgB,WAAW,CACzB,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC7B,IAAI,CAON"}
|
package/dist/styles.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSS-in-JS constants for the devtools overlay.
|
|
3
|
+
* All styles are applied via inline styles or injected style elements
|
|
4
|
+
* to avoid requiring a CSS file or global stylesheet.
|
|
5
|
+
*/
|
|
6
|
+
export const COLORS = {
|
|
7
|
+
client: {
|
|
8
|
+
outline: "rgba(249, 115, 22, 0.8)",
|
|
9
|
+
background: "rgba(249, 115, 22, 0.12)",
|
|
10
|
+
label: "rgba(249, 115, 22, 0.95)",
|
|
11
|
+
},
|
|
12
|
+
server: {
|
|
13
|
+
outline: "rgba(59, 130, 246, 0.8)",
|
|
14
|
+
background: "rgba(59, 130, 246, 0.12)",
|
|
15
|
+
label: "rgba(59, 130, 246, 0.95)",
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
export const PILL_STYLES = {
|
|
19
|
+
position: "fixed",
|
|
20
|
+
bottom: "64px",
|
|
21
|
+
left: "16px",
|
|
22
|
+
zIndex: "99999",
|
|
23
|
+
display: "flex",
|
|
24
|
+
alignItems: "center",
|
|
25
|
+
gap: "6px",
|
|
26
|
+
padding: "6px 12px",
|
|
27
|
+
borderRadius: "9999px",
|
|
28
|
+
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
29
|
+
background: "rgba(0, 0, 0, 0.8)",
|
|
30
|
+
backdropFilter: "blur(8px)",
|
|
31
|
+
color: "#fff",
|
|
32
|
+
fontSize: "13px",
|
|
33
|
+
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
|
|
34
|
+
fontWeight: "500",
|
|
35
|
+
cursor: "pointer",
|
|
36
|
+
userSelect: "none",
|
|
37
|
+
lineHeight: "1",
|
|
38
|
+
transition: "background 150ms ease, box-shadow 150ms ease",
|
|
39
|
+
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.3)",
|
|
40
|
+
};
|
|
41
|
+
export const PILL_ACTIVE_STYLES = {
|
|
42
|
+
background: "rgba(0, 0, 0, 0.9)",
|
|
43
|
+
boxShadow: `0 0 0 2px ${COLORS.client.outline}, 0 2px 8px rgba(0, 0, 0, 0.3)`,
|
|
44
|
+
};
|
|
45
|
+
export const PANEL_STYLES = {
|
|
46
|
+
position: "fixed",
|
|
47
|
+
bottom: "100px",
|
|
48
|
+
left: "16px",
|
|
49
|
+
zIndex: "99998",
|
|
50
|
+
width: "280px",
|
|
51
|
+
maxHeight: "400px",
|
|
52
|
+
overflowY: "auto",
|
|
53
|
+
borderRadius: "12px",
|
|
54
|
+
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
55
|
+
background: "rgba(0, 0, 0, 0.85)",
|
|
56
|
+
backdropFilter: "blur(12px)",
|
|
57
|
+
color: "#fff",
|
|
58
|
+
fontSize: "12px",
|
|
59
|
+
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
|
|
60
|
+
padding: "12px",
|
|
61
|
+
boxShadow: "0 4px 16px rgba(0, 0, 0, 0.4)",
|
|
62
|
+
};
|
|
63
|
+
export const LABEL_BASE_STYLES = {
|
|
64
|
+
position: "absolute",
|
|
65
|
+
top: "-1px",
|
|
66
|
+
left: "-1px",
|
|
67
|
+
padding: "1px 6px",
|
|
68
|
+
fontSize: "10px",
|
|
69
|
+
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
|
|
70
|
+
fontWeight: "600",
|
|
71
|
+
lineHeight: "16px",
|
|
72
|
+
color: "#fff",
|
|
73
|
+
borderRadius: "0 0 4px 0",
|
|
74
|
+
pointerEvents: "none",
|
|
75
|
+
zIndex: "99997",
|
|
76
|
+
whiteSpace: "nowrap",
|
|
77
|
+
};
|
|
78
|
+
export function applyStyles(el, styles) {
|
|
79
|
+
for (const [key, value] of Object.entries(styles)) {
|
|
80
|
+
el.style.setProperty(key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`), value);
|
|
81
|
+
}
|
|
82
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Represents a detected client component boundary in the fiber tree.
|
|
3
|
+
* Server components have no fibers on the client — they are identified
|
|
4
|
+
* by exclusion (DOM regions not owned by any client component) and/or
|
|
5
|
+
* optional explicit markers.
|
|
6
|
+
*/
|
|
7
|
+
export interface ComponentInfo {
|
|
8
|
+
name: string;
|
|
9
|
+
domNodes: HTMLElement[];
|
|
10
|
+
}
|
|
11
|
+
/** How a server region was detected. */
|
|
12
|
+
export type ServerRegionSource = "explicit" | "heuristic";
|
|
13
|
+
/**
|
|
14
|
+
* A highlighted server-rendered DOM region (explicit marker or heuristic).
|
|
15
|
+
*/
|
|
16
|
+
export interface ServerRegionInfo {
|
|
17
|
+
element: HTMLElement;
|
|
18
|
+
/** Label for panel and floating overlay */
|
|
19
|
+
displayLabel: string;
|
|
20
|
+
source: ServerRegionSource;
|
|
21
|
+
}
|
|
22
|
+
export interface HighlightState {
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
components: ComponentInfo[];
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,WAAW,EAAE,CAAC;CACzB;AAED,wCAAwC;AACxC,MAAM,MAAM,kBAAkB,GAAG,UAAU,GAAG,WAAW,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,WAAW,CAAC;IACrB,2CAA2C;IAC3C,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,kBAAkB,CAAC;CAC5B;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,aAAa,EAAE,CAAC;CAC7B"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rsc-boundary",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/foxted/rsc-boundary.git"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"README.md",
|
|
20
|
+
"LICENSE"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"react": "^19.0.0",
|
|
27
|
+
"react-dom": "^19.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^25.5.2",
|
|
31
|
+
"@types/react": "19.2.14",
|
|
32
|
+
"@types/react-dom": "19.2.3",
|
|
33
|
+
"eslint": "^10.2.0",
|
|
34
|
+
"typescript": "6.0.2",
|
|
35
|
+
"@repo/eslint-config": "0.0.0",
|
|
36
|
+
"@repo/typescript-config": "0.0.0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsc -p tsconfig.build.json",
|
|
40
|
+
"lint": "eslint --max-warnings 0",
|
|
41
|
+
"check-types": "tsc --noEmit"
|
|
42
|
+
}
|
|
43
|
+
}
|