rsc-boundary 0.1.0 → 0.2.0

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