Skip to content

Commit 0ca32b2

Browse files
authored
[devtool] initial support for segment explorer (#78858)
1 parent 3a7ea69 commit 0ca32b2

File tree

28 files changed

+587
-22
lines changed

28 files changed

+587
-22
lines changed

packages/next/src/build/define-env.ts

+2
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,8 @@ export function getDefineEnv({
279279
'process.env.__NEXT_RELATIVE_DIST_DIR': config.distDir,
280280
}
281281
: {}),
282+
'process.env.__NEXT_DEVTOOL_SEGMENT_EXPLORER':
283+
config.experimental.devtoolSegmentExplorer ?? false,
282284
}
283285

284286
const userDefines = config.compiler?.define ?? {}

packages/next/src/client/components/react-dev-overlay/ui/components/errors/dev-tools-indicator/dev-tools-indicator.tsx

+22
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { TurbopackInfo } from './dev-tools-info/turbopack-info'
1111
import { RouteInfo } from './dev-tools-info/route-info'
1212
import GearIcon from '../../../icons/gear-icon'
1313
import { UserPreferences } from './dev-tools-info/user-preferences'
14+
import { SegmentsExplorer } from '../../overview/segment-explorer'
1415
import {
1516
MENU_CURVE,
1617
MENU_DURATION_MS,
@@ -80,6 +81,7 @@ const OVERLAYS = {
8081
Turbo: 'turbo',
8182
Route: 'route',
8283
Preferences: 'preferences',
84+
SegmentExplorer: 'segment-explorer',
8385
} as const
8486

8587
export type Overlays = (typeof OVERLAYS)[keyof typeof OVERLAYS]
@@ -123,6 +125,7 @@ function DevToolsPopover({
123125
const isTurbopackInfoOpen = open === OVERLAYS.Turbo
124126
const isRouteInfoOpen = open === OVERLAYS.Route
125127
const isPreferencesOpen = open === OVERLAYS.Preferences
128+
const isSegmentExplorerOpen = open === OVERLAYS.SegmentExplorer
126129

127130
const { mounted: menuMounted, rendered: menuRendered } = useDelayedRender(
128131
isMenuOpen,
@@ -325,6 +328,16 @@ function DevToolsPopover({
325328
setScale={setScale}
326329
/>
327330

331+
{/* Page Segment Explorer */}
332+
{process.env.__NEXT_DEVTOOL_SEGMENT_EXPLORER ? (
333+
<SegmentsExplorer
334+
isOpen={isSegmentExplorerOpen}
335+
close={openRootMenu}
336+
triggerRef={triggerRef}
337+
style={popover}
338+
/>
339+
) : null}
340+
328341
{/* Dropdown Menu */}
329342
{menuMounted && (
330343
<div
@@ -390,6 +403,15 @@ function DevToolsPopover({
390403
onClick={() => setOpen(OVERLAYS.Preferences)}
391404
index={isTurbopack ? 2 : 3}
392405
/>
406+
{process.env.__NEXT_DEVTOOL_SEGMENT_EXPLORER ? (
407+
<MenuItem
408+
data-rendered-files
409+
label="Segment Explorer"
410+
value={<ChevronRight />}
411+
onClick={() => setOpen(OVERLAYS.SegmentExplorer)}
412+
index={isTurbopack ? 3 : 4}
413+
/>
414+
) : null}
393415
</div>
394416
</Context.Provider>
395417
</div>

packages/next/src/client/components/react-dev-overlay/ui/components/errors/dev-tools-indicator/dev-tools-info/dev-tools-info.tsx

+27-20
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@ export interface DevToolsInfoPropsCore {
99
}
1010

1111
export interface DevToolsInfoProps extends DevToolsInfoPropsCore {
12-
title: string
12+
title: React.ReactNode
1313
children: React.ReactNode
1414
learnMoreLink?: string
15+
closeButton?: boolean
1516
}
1617

1718
export function DevToolsInfo({
@@ -20,6 +21,7 @@ export function DevToolsInfo({
2021
learnMoreLink,
2122
isOpen,
2223
triggerRef,
24+
closeButton = true,
2325
close,
2426
...props
2527
}: DevToolsInfoProps) {
@@ -55,25 +57,29 @@ export function DevToolsInfo({
5557
<div className="dev-tools-info-container">
5658
<h1 className="dev-tools-info-title">{title}</h1>
5759
{children}
58-
<div className="dev-tools-info-button-container">
59-
<button
60-
ref={closeButtonRef}
61-
className="dev-tools-info-close-button"
62-
onClick={close}
63-
>
64-
Close
65-
</button>
66-
{learnMoreLink && (
67-
<a
68-
className="dev-tools-info-learn-more-button"
69-
href={learnMoreLink}
70-
target="_blank"
71-
rel="noreferrer noopener"
72-
>
73-
Learn More
74-
</a>
75-
)}
76-
</div>
60+
{(closeButton || learnMoreLink) && (
61+
<div className="dev-tools-info-button-container">
62+
{closeButton ? (
63+
<button
64+
ref={closeButtonRef}
65+
className="dev-tools-info-close-button"
66+
onClick={close}
67+
>
68+
Close
69+
</button>
70+
) : null}
71+
{learnMoreLink && (
72+
<a
73+
className="dev-tools-info-learn-more-button"
74+
href={learnMoreLink}
75+
target="_blank"
76+
rel="noreferrer noopener"
77+
>
78+
Learn More
79+
</a>
80+
)}
81+
</div>
82+
)}
7783
</div>
7884
</div>
7985
)
@@ -112,6 +118,7 @@ export const DEV_TOOLS_INFO_STYLES = `
112118
113119
.dev-tools-info-container {
114120
padding: 12px;
121+
width: 100%;
115122
}
116123
117124
.dev-tools-info-title {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import type { HTMLProps } from 'react'
2+
import { css } from '../../../utils/css'
3+
import type { DevToolsInfoPropsCore } from '../errors/dev-tools-indicator/dev-tools-info/dev-tools-info'
4+
import { DevToolsInfo } from '../errors/dev-tools-indicator/dev-tools-info/dev-tools-info'
5+
import { cx } from '../../utils/cx'
6+
import { LeftArrow } from '../../icons/left-arrow'
7+
import {
8+
useSegmentTreeClientState,
9+
type SegmentNode,
10+
} from '../../../../../../shared/lib/devtool/app-segment-tree'
11+
import type { Trie, TrieNode } from '../../../../../../shared/lib/devtool/trie'
12+
13+
const IconLayout = (props: React.SVGProps<SVGSVGElement>) => {
14+
return (
15+
<svg
16+
{...props}
17+
viewBox="0 0 16 16"
18+
fill="none"
19+
xmlns="http://www.w3.org/2000/svg"
20+
>
21+
<path
22+
d="M16 12.5L15.9873 12.7559C15.8677 13.9323 14.9323 14.8677 13.7559 14.9873L13.5 15H2.5L2.24414 14.9873C1.06772 14.8677 0.132274 13.9323 0.0126953 12.7559L0 12.5V1H16V12.5ZM1.5 6.25488V12.5C1.5 13.0523 1.94772 13.5 2.5 13.5H4.99512V6.25488H1.5ZM6.24512 6.25488V13.5H13.5C14.0523 13.5 14.5 13.0523 14.5 12.5V6.25488H6.24512ZM1.5 5.00488H14.5V2.5H1.5V5.00488Z"
23+
fill="currentColor"
24+
/>
25+
</svg>
26+
)
27+
}
28+
29+
const IconPage = (props: React.SVGProps<SVGSVGElement>) => {
30+
return (
31+
<svg
32+
{...props}
33+
viewBox="0 0 16 16"
34+
fill="none"
35+
strokeLinejoin="round"
36+
xmlns="http://www.w3.org/2000/svg"
37+
>
38+
<path
39+
fillRule="evenodd"
40+
clipRule="evenodd"
41+
d="M14.5 6.5V13.5C14.5 14.8807 13.3807 16 12 16H4C2.61929 16 1.5 14.8807 1.5 13.5V1.5V0H3H8H9.08579C9.351 0 9.60536 0.105357 9.79289 0.292893L14.2071 4.70711C14.3946 4.89464 14.5 5.149 14.5 5.41421V6.5ZM13 6.5V13.5C13 14.0523 12.5523 14.5 12 14.5H4C3.44772 14.5 3 14.0523 3 13.5V1.5H8V5V6.5H9.5H13ZM9.5 2.12132V5H12.3787L9.5 2.12132Z"
42+
fill="currentColor"
43+
/>
44+
</svg>
45+
)
46+
}
47+
48+
const ICONS = {
49+
layout: <IconLayout width={16} />,
50+
page: <IconPage width={16} />,
51+
}
52+
53+
function PageSegmentTree({ tree }: { tree: Trie<SegmentNode> | undefined }) {
54+
if (!tree) {
55+
return null
56+
}
57+
return (
58+
<div className="segment-explorer-content">
59+
<PageSegmentTreeLayerPresentation
60+
tree={tree}
61+
node={tree.getRoot()}
62+
level={0}
63+
/>
64+
</div>
65+
)
66+
}
67+
68+
function PageSegmentTreeLayerPresentation({
69+
tree,
70+
node,
71+
level,
72+
}: {
73+
tree: Trie<SegmentNode>
74+
node: TrieNode<SegmentNode>
75+
level: number
76+
}) {
77+
const segments = node.value?.pagePath?.split('/') || []
78+
const fileName = segments[segments.length - 1] || ''
79+
const nodeName = node.value?.type
80+
const pagePathPrefix = segments.slice(0, -1).join('/')
81+
82+
return (
83+
<div className="segment-explorer-item">
84+
{!fileName || level === 0 ? null : (
85+
<div className="segment-explorer-item-row">
86+
<div className="segment-explorer-line">
87+
<div className={`segment-explorer-line-text-${nodeName}`}>
88+
<span
89+
className={cx(
90+
'segment-explorer-line-icon',
91+
`segment-explorer-line-icon-${nodeName}`
92+
)}
93+
>
94+
{nodeName === 'layout' ? ICONS.layout : ICONS.page}
95+
</span>
96+
{pagePathPrefix === '' ? '' : `${pagePathPrefix}/`}
97+
<span className="segment-explorer-filename-path">{fileName}</span>
98+
</div>
99+
</div>
100+
</div>
101+
)}
102+
103+
<div className="tree-node-expanded-rendered-children">
104+
{Object.entries(node.children).map(
105+
([key, child]) =>
106+
child && (
107+
<PageSegmentTreeLayerPresentation
108+
key={key}
109+
tree={tree}
110+
node={child}
111+
level={level + 1}
112+
/>
113+
)
114+
)}
115+
</div>
116+
</div>
117+
)
118+
}
119+
120+
export function SegmentsExplorer(
121+
props: DevToolsInfoPropsCore & HTMLProps<HTMLDivElement>
122+
) {
123+
const ctx = useSegmentTreeClientState()
124+
if (!ctx) {
125+
return null
126+
}
127+
128+
return (
129+
<DevToolsInfo
130+
title={
131+
<>
132+
<button
133+
className="segment-explorer-back-button"
134+
onClick={props.close}
135+
>
136+
<LeftArrow />
137+
</button>
138+
{'Segment Explorer'}
139+
</>
140+
}
141+
closeButton={false}
142+
{...props}
143+
>
144+
<PageSegmentTree tree={ctx.tree} />
145+
</DevToolsInfo>
146+
)
147+
}
148+
149+
export const DEV_TOOLS_INFO_RENDER_FILES_STYLES = css`
150+
.segment-explorer-back-button {
151+
margin-right: 12px;
152+
color: var(--color-gray-1000);
153+
}
154+
.segment-explorer-back-button svg {
155+
width: 20px;
156+
height: 20px;
157+
}
158+
159+
.segment-explorer-content {
160+
overflow-y: auto;
161+
padding: 0 12px;
162+
font-size: var(--size-14);
163+
}
164+
165+
.segment-explorer-item-row {
166+
display: flex;
167+
align-items: center;
168+
gap: 8px;
169+
padding: 2px 0;
170+
}
171+
172+
.segment-explorer-filename-path {
173+
display: inline-block;
174+
175+
&:hover {
176+
color: var(--color-gray-1000);
177+
text-decoration: none;
178+
}
179+
}
180+
181+
.segment-explorer-filename-path a {
182+
color: inherit;
183+
text-decoration: inherit;
184+
}
185+
186+
.segment-explorer-line {
187+
white-space: pre;
188+
}
189+
190+
.segment-explorer-line-icon {
191+
margin-right: 4px;
192+
}
193+
.segment-explorer-line-icon-page {
194+
color: inherit;
195+
}
196+
.segment-explorer-line-icon-layout {
197+
color: var(--color-gray-1-00);
198+
}
199+
200+
.segment-explorer-line-text-page {
201+
color: var(--color-blue-900);
202+
font-weight: 500;
203+
}
204+
`

packages/next/src/client/components/react-dev-overlay/ui/styles/component-styles.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { DEV_TOOLS_INFO_STYLES } from '../components/errors/dev-tools-indicator/
2121
import { DEV_TOOLS_INFO_TURBOPACK_INFO_STYLES } from '../components/errors/dev-tools-indicator/dev-tools-info/turbopack-info'
2222
import { DEV_TOOLS_INFO_ROUTE_INFO_STYLES } from '../components/errors/dev-tools-indicator/dev-tools-info/route-info'
2323
import { DEV_TOOLS_INFO_USER_PREFERENCES_STYLES } from '../components/errors/dev-tools-indicator/dev-tools-info/user-preferences'
24+
import { DEV_TOOLS_INFO_RENDER_FILES_STYLES } from '../components/overview/segment-explorer'
2425
import { FADER_STYLES } from '../components/fader'
2526

2627
export function ComponentStyles() {
@@ -49,6 +50,7 @@ export function ComponentStyles() {
4950
${DEV_TOOLS_INFO_TURBOPACK_INFO_STYLES}
5051
${DEV_TOOLS_INFO_ROUTE_INFO_STYLES}
5152
${DEV_TOOLS_INFO_USER_PREFERENCES_STYLES}
53+
${DEV_TOOLS_INFO_RENDER_FILES_STYLES}
5254
${FADER_STYLES}
5355
`}
5456
</style>

0 commit comments

Comments
 (0)