Skip to content

Commit ae1f0be

Browse files
authored
[segment-explorer] Signal updates to React (#80316)
We never actually invoked the listeners attached by React. Updates only worked on soft-navigation because we also re-render the DevOverlayRoot and don't bail out anywhere at the moment. Just invoking the listeners is not sufficient after #79699 since userspace and dev-overlay would read different versions of `listeners`. I ported the segment explorer into the new folder structure. Updates now flow through the `dispatcher` and then invoking the listeners actually works. Though mid term we should move the segment tree into actual React tree to get rid of the sync updates. I also noticed that we're missing an implementation for a remove operation which is required to show the correct state during soft navigations.
1 parent f646f4d commit ae1f0be

File tree

11 files changed

+434
-190
lines changed

11 files changed

+434
-190
lines changed

packages/next/src/next-devtools/dev-overlay.browser.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ import type { DebugInfo } from './shared/types'
2727
import { DevOverlay } from './dev-overlay/dev-overlay'
2828
import type { DevIndicatorServerState } from '../server/dev/dev-indicator-server-state'
2929
import type { VersionInfo } from '../server/dev/parse-version-info'
30+
import {
31+
insertSegmentNode,
32+
removeSegmentNode,
33+
type SegmentNode,
34+
} from './dev-overlay/segment-explorer'
3035

3136
export interface Dispatcher {
3237
onBuildOk(): void
@@ -46,6 +51,14 @@ export interface Dispatcher {
4651
buildingIndicatorShow(): void
4752
renderingIndicatorHide(): void
4853
renderingIndicatorShow(): void
54+
segmentExplorerNodeAdd(
55+
nodeType: SegmentNode['type'],
56+
pagePath: SegmentNode['pagePath']
57+
): void
58+
segmentExplorerNodeRemove(
59+
nodeType: SegmentNode['type'],
60+
pagePath: SegmentNode['pagePath']
61+
): void
4962
}
5063

5164
type Dispatch = ReturnType<typeof useErrorOverlayReducer>[1]
@@ -131,6 +144,24 @@ export const dispatcher: Dispatcher = {
131144
renderingIndicatorShow: createQueuable((dispatch: Dispatch) => {
132145
dispatch({ type: ACTION_RENDERING_INDICATOR_SHOW })
133146
}),
147+
segmentExplorerNodeAdd: createQueuable(
148+
(
149+
_: Dispatch,
150+
nodeType: SegmentNode['type'],
151+
pagePath: SegmentNode['pagePath']
152+
) => {
153+
insertSegmentNode({ type: nodeType, pagePath })
154+
}
155+
),
156+
segmentExplorerNodeRemove: createQueuable(
157+
(
158+
_: Dispatch,
159+
nodeType: SegmentNode['type'],
160+
pagePath: SegmentNode['pagePath']
161+
) => {
162+
removeSegmentNode({ type: nodeType, pagePath })
163+
}
164+
),
134165
}
135166

136167
function replayQueuedEvents(dispatch: NonNullable<typeof maybeDispatch>) {

packages/next/src/next-devtools/dev-overlay/components/overview/segment-explorer.tsx

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,16 @@ import type { HTMLProps } from 'react'
22
import { css } from '../../utils/css'
33
import type { DevToolsInfoPropsCore } from '../errors/dev-tools-indicator/dev-tools-info/dev-tools-info'
44
import { DevToolsInfo } from '../errors/dev-tools-indicator/dev-tools-info/dev-tools-info'
5-
import {
6-
type SegmentNode,
7-
useSegmentTreeClientState,
8-
} from '../../../../shared/lib/devtool/app-segment-tree'
9-
import type { Trie, TrieNode } from '../../../../shared/lib/devtool/trie'
10-
11-
function PageSegmentTree({ tree }: { tree: Trie<SegmentNode> | undefined }) {
12-
if (!tree) {
13-
return null
14-
}
5+
import { useSegmentTree, type SegmentTrieNode } from '../../segment-explorer'
6+
7+
function PageSegmentTree({ tree }: { tree: SegmentTrieNode }) {
158
return (
169
<div
1710
className="segment-explorer-content"
1811
data-nextjs-devtool-segment-explorer
1912
>
2013
<PageSegmentTreeLayerPresentation
21-
tree={tree}
22-
node={tree.getRoot()}
14+
node={tree}
2315
level={0}
2416
segment=""
2517
parentSegment=""
@@ -29,16 +21,14 @@ function PageSegmentTree({ tree }: { tree: Trie<SegmentNode> | undefined }) {
2921
}
3022

3123
function PageSegmentTreeLayerPresentation({
32-
tree,
3324
segment,
3425
parentSegment,
3526
node,
3627
level,
3728
}: {
38-
tree: Trie<SegmentNode>
3929
segment: string
4030
parentSegment: string
41-
node: TrieNode<SegmentNode>
31+
node: SegmentTrieNode
4232
level: number
4333
}) {
4434
const pagePath = node.value?.pagePath || ''
@@ -111,7 +101,6 @@ function PageSegmentTreeLayerPresentation({
111101
key={childSegment}
112102
segment={childSegment}
113103
parentSegment={segment}
114-
tree={tree}
115104
node={child}
116105
level={hasFileChildren ? level + 1 : level}
117106
/>
@@ -124,14 +113,11 @@ function PageSegmentTreeLayerPresentation({
124113
export function SegmentsExplorer(
125114
props: DevToolsInfoPropsCore & HTMLProps<HTMLDivElement>
126115
) {
127-
const ctx = useSegmentTreeClientState()
128-
if (!ctx) {
129-
return null
130-
}
116+
const tree = useSegmentTree()
131117

132118
return (
133119
<DevToolsInfo title="Segment Explorer" {...props}>
134-
<PageSegmentTree tree={ctx.tree} />
120+
<PageSegmentTree tree={tree} />
135121
</DevToolsInfo>
136122
)
137123
}
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
/* eslint-disable @next/internal/typechecked-require -- Not a prod file */
5+
/* eslint-disable import/no-extraneous-dependencies -- Not a prod file */
6+
7+
import type * as SegmentExplorer from './segment-explorer'
8+
9+
describe('Segment Explorer', () => {
10+
let cleanup: typeof import('@testing-library/react').cleanup
11+
let renderHook: typeof import('@testing-library/react').renderHook
12+
let useSegmentTree: typeof SegmentExplorer.useSegmentTree
13+
let insertSegmentNode: typeof SegmentExplorer.insertSegmentNode
14+
let removeSegmentNode: typeof SegmentExplorer.removeSegmentNode
15+
16+
beforeEach(() => {
17+
jest.resetModules()
18+
jest.clearAllMocks()
19+
20+
const segmentExplorer = require('./segment-explorer')
21+
useSegmentTree = segmentExplorer.useSegmentTree
22+
insertSegmentNode = segmentExplorer.insertSegmentNode
23+
removeSegmentNode = segmentExplorer.removeSegmentNode
24+
const rtl = require('@testing-library/react/pure')
25+
renderHook = rtl.renderHook
26+
cleanup = rtl.cleanup
27+
})
28+
29+
afterEach(() => {
30+
cleanup()
31+
})
32+
33+
test('add complex structure', () => {
34+
insertSegmentNode({ pagePath: '/a/page.js', type: 'page' })
35+
insertSegmentNode({ pagePath: '/a/layout.js', type: 'layout' })
36+
insertSegmentNode({ pagePath: '/layout.js', type: 'layout' })
37+
38+
const { result } = renderHook(useSegmentTree)
39+
40+
expect(result.current).toEqual({
41+
children: {
42+
'': {
43+
children: {
44+
a: {
45+
children: {
46+
'layout.js': {
47+
children: {},
48+
value: {
49+
pagePath: '/a/layout.js',
50+
type: 'layout',
51+
},
52+
},
53+
'page.js': {
54+
children: {},
55+
value: {
56+
pagePath: '/a/page.js',
57+
type: 'page',
58+
},
59+
},
60+
},
61+
value: undefined,
62+
},
63+
'layout.js': {
64+
children: {},
65+
value: {
66+
pagePath: '/layout.js',
67+
type: 'layout',
68+
},
69+
},
70+
},
71+
value: undefined,
72+
},
73+
},
74+
value: undefined,
75+
})
76+
})
77+
78+
test.failing('remove node in the middle', () => {
79+
insertSegmentNode({ pagePath: '/a/b/@sidebar/page.js', type: 'page' })
80+
insertSegmentNode({ pagePath: '/a/b/page.js', type: 'page' })
81+
insertSegmentNode({ pagePath: '/a/b/layout.js', type: 'layout' })
82+
insertSegmentNode({ pagePath: '/a/layout.js', type: 'layout' })
83+
insertSegmentNode({ pagePath: '/layout.js', type: 'layout' })
84+
85+
const { result } = renderHook(useSegmentTree)
86+
87+
expect(result.current).toEqual({
88+
children: {
89+
'': {
90+
children: {
91+
a: {
92+
children: {
93+
b: {
94+
children: {
95+
'@sidebar': {
96+
children: {
97+
'page.js': {
98+
children: {},
99+
value: {
100+
pagePath: '/a/b/@sidebar/page.js',
101+
type: 'page',
102+
},
103+
},
104+
},
105+
value: undefined,
106+
},
107+
'layout.js': {
108+
children: {},
109+
value: {
110+
pagePath: '/a/b/layout.js',
111+
type: 'layout',
112+
},
113+
},
114+
'page.js': {
115+
children: {},
116+
value: {
117+
pagePath: '/a/b/page.js',
118+
type: 'page',
119+
},
120+
},
121+
},
122+
value: undefined,
123+
},
124+
'layout.js': {
125+
children: {},
126+
value: {
127+
pagePath: '/a/layout.js',
128+
type: 'layout',
129+
},
130+
},
131+
},
132+
value: undefined,
133+
},
134+
'layout.js': {
135+
children: {},
136+
value: {
137+
pagePath: '/layout.js',
138+
type: 'layout',
139+
},
140+
},
141+
},
142+
value: undefined,
143+
},
144+
},
145+
value: undefined,
146+
})
147+
148+
removeSegmentNode({ pagePath: '/a/b/layout.js', type: 'layout' })
149+
150+
expect(result.current).toEqual({
151+
children: {
152+
'': {
153+
children: {
154+
a: {
155+
children: {
156+
b: {
157+
children: {
158+
'@sidebar': {
159+
children: {
160+
'page.js': {
161+
children: {},
162+
value: {
163+
pagePath: '/a/b/@sidebar/page.js',
164+
type: 'page',
165+
},
166+
},
167+
},
168+
value: undefined,
169+
},
170+
'page.js': {
171+
children: {},
172+
value: {
173+
pagePath: '/a/b/page.js',
174+
type: 'page',
175+
},
176+
},
177+
},
178+
value: undefined,
179+
},
180+
'layout.js': {
181+
children: {},
182+
value: {
183+
pagePath: '/a/layout.js',
184+
type: 'layout',
185+
},
186+
},
187+
},
188+
value: undefined,
189+
},
190+
'layout.js': {
191+
children: {},
192+
value: {
193+
pagePath: '/layout.js',
194+
type: 'layout',
195+
},
196+
},
197+
},
198+
value: undefined,
199+
},
200+
},
201+
value: undefined,
202+
})
203+
})
204+
})

0 commit comments

Comments
 (0)