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 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,3 @@
1
+ /** Optional explicit server region marker (hybrid / higher-accuracy mode). */
2
+ export declare const SERVER_BOUNDARY_DATA_ATTR = "data-rsc-boundary-server";
3
+ //# sourceMappingURL=constants.d.ts.map
@@ -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,2 @@
1
+ /** Optional explicit server region marker (hybrid / higher-accuracy mode). */
2
+ export const SERVER_BOUNDARY_DATA_ATTR = "data-rsc-boundary-server";
@@ -0,0 +1,2 @@
1
+ export declare function RscDevtools(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=devtools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devtools.d.ts","sourceRoot":"","sources":["../src/devtools.tsx"],"names":[],"mappings":"AA0FA,wBAAgB,WAAW,4CAE1B"}
@@ -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
+ }
@@ -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"}
@@ -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
+ }
@@ -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
+ }
@@ -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
+ }