Skip to content

Commit 536b6b7

Browse files
devjiwonchoihuozhi
andauthored
[devtools] save user config inside .next/cache (#81807)
### Why? It's common for developers to clear `localStorage` during development. Since the current DevTools config is saved there, this can hurt the developer experience when `localStorage` is cleared for any reason. To avoid this, we decided to store the DevTools config in the app’s cache directory, which is `{distDir}/cache/`. The content inside this directory is expected to remain during Next.js’ workflow (i.e., `next build`) unless it's manually removed. ### How? Added the `/__nextjs_devtools_config` endpoint to handle config writing. The client sends a POST request to this endpoint, passing the config data. After the config is successfully written, HMR is triggered to update the client state accordingly. --------- Co-authored-by: Jiachi Liu <[email protected]>
1 parent 907501e commit 536b6b7

File tree

24 files changed

+416
-266
lines changed

24 files changed

+416
-266
lines changed

packages/next/src/client/dev/hot-reloader/app/hot-reloader-app.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,8 @@ function processMessage(
293293
if ('versionInfo' in obj) dispatcher.onVersionInfo(obj.versionInfo)
294294
if ('debug' in obj && obj.debug) dispatcher.onDebugInfo(obj.debug)
295295
if ('devIndicator' in obj) dispatcher.onDevIndicator(obj.devIndicator)
296+
if ('devToolsConfig' in obj)
297+
dispatcher.onDevToolsConfig(obj.devToolsConfig)
296298

297299
const hasErrors = Boolean(errors && errors.length)
298300
// Compilation with errors (e.g. syntax error or missing modules).
@@ -440,6 +442,10 @@ function processMessage(
440442
case HMR_ACTIONS_SENT_TO_BROWSER.DEV_PAGES_MANIFEST_UPDATE: {
441443
return
442444
}
445+
case HMR_ACTIONS_SENT_TO_BROWSER.DEVTOOLS_CONFIG: {
446+
dispatcher.onDevToolsConfig(obj.data)
447+
return
448+
}
443449
default: {
444450
obj satisfies never
445451
}

packages/next/src/client/dev/hot-reloader/pages/hot-reloader-pages.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,8 @@ function processMessage(obj: HMR_ACTION_TYPES) {
285285
// Is undefined when it's a 'built' event
286286
if ('versionInfo' in obj) dispatcher.onVersionInfo(obj.versionInfo)
287287
if ('devIndicator' in obj) dispatcher.onDevIndicator(obj.devIndicator)
288+
if ('devToolsConfig' in obj)
289+
dispatcher.onDevToolsConfig(obj.devToolsConfig)
288290

289291
const hasErrors = Boolean(errors && errors.length)
290292
if (hasErrors) {
@@ -369,6 +371,9 @@ function processMessage(obj: HMR_ACTION_TYPES) {
369371
customHmrEventHandler(obj)
370372
}
371373
break
374+
case HMR_ACTIONS_SENT_TO_BROWSER.DEVTOOLS_CONFIG:
375+
dispatcher.onDevToolsConfig(obj.data)
376+
break
372377
default:
373378
obj satisfies never
374379
}

packages/next/src/client/page-bootstrap.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ export function pageBootstrap(assetPrefix: string) {
6161
case HMR_ACTIONS_SENT_TO_BROWSER.TURBOPACK_MESSAGE:
6262
case HMR_ACTIONS_SENT_TO_BROWSER.TURBOPACK_CONNECTED:
6363
case HMR_ACTIONS_SENT_TO_BROWSER.ISR_MANIFEST:
64+
case HMR_ACTIONS_SENT_TO_BROWSER.DEVTOOLS_CONFIG:
6465
// Most of these action types are handled in
6566
// src/client/dev/hot-reloader/pages/hot-reloader-pages.ts
6667
break

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
ACTION_RENDERING_INDICATOR_HIDE,
1919
ACTION_RENDERING_INDICATOR_SHOW,
2020
ACTION_DEVTOOL_UPDATE_ROUTE_STATE,
21+
ACTION_DEVTOOLS_CONFIG,
2122
type OverlayState,
2223
type DispatcherEvent,
2324
} from './dev-overlay/shared'
@@ -41,6 +42,7 @@ import {
4142
removeSegmentNode,
4243
} from './dev-overlay/segment-explorer-trie'
4344
import type { SegmentNodeState } from './userspace/app/segment-explorer-node'
45+
import type { DevToolsConfig } from './dev-overlay/shared'
4446

4547
export interface Dispatcher {
4648
onBuildOk(): void
@@ -51,6 +53,7 @@ export interface Dispatcher {
5153
onRefresh(): void
5254
onStaticIndicator(status: boolean): void
5355
onDevIndicator(devIndicator: DevIndicatorServerState): void
56+
onDevToolsConfig(config: DevToolsConfig): void
5457
onUnhandledError(reason: Error): void
5558
onUnhandledRejection(reason: Error): void
5659
openErrorOverlay(): void
@@ -115,6 +118,11 @@ export const dispatcher: Dispatcher = {
115118
dispatch({ type: ACTION_DEV_INDICATOR, devIndicator })
116119
}
117120
),
121+
onDevToolsConfig: createQueuable(
122+
(dispatch: Dispatch, devToolsConfig: DevToolsConfig) => {
123+
dispatch({ type: ACTION_DEVTOOLS_CONFIG, devToolsConfig })
124+
}
125+
),
118126
onUnhandledError: createQueuable((dispatch: Dispatch, error: Error) => {
119127
dispatch({
120128
type: ACTION_UNHANDLED_ERROR,

packages/next/src/next-devtools/dev-overlay/components/devtools-indicator/devtools-indicator.tsx

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import './devtools-indicator.css'
22
import type { CSSProperties } from 'react'
3+
import type { DevToolsIndicatorPosition } from '../../shared'
34
import { NextLogo } from './next-logo'
45
import { Toast } from '../toast'
56
import {
67
MENU_CURVE,
78
MENU_DURATION_MS,
89
} from '../errors/dev-tools-indicator/utils'
910
import {
10-
STORAGE_KEY_POSITION,
1111
ACTION_DEVTOOLS_POSITION,
1212
STORE_KEY_SHARED_PANEL_LOCATION,
1313
STORAGE_KEY_PANEL_POSITION_PREFIX,
@@ -16,7 +16,7 @@ import {
1616
import { Draggable } from '../errors/dev-tools-indicator/draggable'
1717
import { useDevOverlayContext } from '../../../dev-overlay.browser'
1818
import { usePanelRouterContext } from '../../menu/context'
19-
import type { DevToolsIndicatorPosition } from '../errors/dev-tools-indicator/dev-tools-info/preferences'
19+
import { saveDevToolsConfig } from '../../utils/save-devtools-config'
2020

2121
export const INDICATOR_PADDING = 20
2222

@@ -51,7 +51,7 @@ export function DevToolsIndicator() {
5151
type: ACTION_DEVTOOLS_POSITION,
5252
devToolsPosition: p,
5353
})
54-
localStorage.setItem(STORAGE_KEY_POSITION, p)
54+
saveDevToolsConfig({ devToolsPosition: p })
5555

5656
updateAllPanelPositions(p)
5757
}}
@@ -79,27 +79,34 @@ export function DevToolsIndicator() {
7979
* cannot be dragged when any panel is open
8080
*/
8181
export const useUpdateAllPanelPositions = () => {
82-
const { dispatch } = useDevOverlayContext()
82+
const { state, dispatch } = useDevOverlayContext()
8383
return (position: DevToolsIndicatorPosition) => {
8484
dispatch({
8585
type: ACTION_DEVTOOLS_PANEL_POSITION,
8686
devToolsPanelPosition: position,
8787
key: STORE_KEY_SHARED_PANEL_LOCATION,
8888
})
8989

90-
localStorage.setItem(STORE_KEY_SHARED_PANEL_LOCATION, position)
91-
92-
const panelPositionKeys = Object.keys(localStorage).filter((key) =>
93-
key.startsWith(STORAGE_KEY_PANEL_POSITION_PREFIX)
90+
const panelPositionKeys = Object.keys(state.devToolsPanelPosition).filter(
91+
(key) => key.startsWith(STORAGE_KEY_PANEL_POSITION_PREFIX)
9492
)
9593

94+
const panelPositionPatch: Record<string, DevToolsIndicatorPosition> = {
95+
[STORE_KEY_SHARED_PANEL_LOCATION]: position,
96+
}
97+
9698
panelPositionKeys.forEach((key) => {
9799
dispatch({
98100
type: ACTION_DEVTOOLS_PANEL_POSITION,
99101
devToolsPanelPosition: position,
100102
key,
101103
})
102-
localStorage.setItem(key, position)
104+
105+
panelPositionPatch[key] = position
106+
})
107+
108+
saveDevToolsConfig({
109+
devToolsPanelPosition: panelPositionPatch,
103110
})
104111
}
105112
}

packages/next/src/next-devtools/dev-overlay/components/devtools-panel/hooks/use-theme.ts

Lines changed: 0 additions & 14 deletions
This file was deleted.

packages/next/src/next-devtools/dev-overlay/components/devtools-panel/resize/resize-handle.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useState, useLayoutEffect } from 'react'
22
import type { Corners } from '../../../shared'
33
import { useResize, type ResizeDirection } from './resize-provider'
44
import './resize-handle.css'
5+
import { saveDevToolsConfig } from '../../../utils/save-devtools-config'
56

67
export const ResizeHandle = ({
78
direction,
@@ -127,8 +128,9 @@ export const ResizeHandle = ({
127128
}
128129

129130
const { width, height } = resizeRef.current.getBoundingClientRect()
130-
131-
localStorage.setItem(storageKey, JSON.stringify({ width, height }))
131+
saveDevToolsConfig({
132+
devToolsPanelSize: { [storageKey]: { width, height } },
133+
})
132134
}
133135
document.addEventListener('mousemove', handleMouseMove)
134136
document.addEventListener('mouseup', handleMouseUp)

packages/next/src/next-devtools/dev-overlay/components/devtools-panel/resize/resize-provider.tsx

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -46,26 +46,6 @@ const constrainDimensions = (params: {
4646
}
4747
}
4848

49-
const parseResizeLocalStorage = (key: string) => {
50-
const savedDimensions = localStorage.getItem(key)
51-
if (!savedDimensions) return null
52-
try {
53-
const parsed = JSON.parse(savedDimensions)
54-
if (
55-
typeof parsed === 'object' &&
56-
parsed !== null &&
57-
typeof parsed.width === 'number' &&
58-
typeof parsed.height === 'number'
59-
) {
60-
return { width: parsed.width, height: parsed.height }
61-
}
62-
return null
63-
} catch (e) {
64-
localStorage.removeItem(key)
65-
return null
66-
}
67-
}
68-
6949
interface ResizeProviderProps {
7050
value: {
7151
resizeRef: RefObject<HTMLElement | null>
@@ -74,6 +54,7 @@ interface ResizeProviderProps {
7454
maxWidth?: number
7555
maxHeight?: number
7656
devToolsPosition: Corners
57+
devToolsPanelSize: Record<string, { width: number; height: number }>
7758
storageKey?: string
7859
initialSize?: { height: number; width: number }
7960
}
@@ -105,7 +86,7 @@ export const ResizeProvider = ({ value, children }: ResizeProviderProps) => {
10586
return
10687
}
10788

108-
const dim = parseResizeLocalStorage(storageKey)
89+
const dim = value.devToolsPanelSize[storageKey]
10990
if (!dim) {
11091
return
11192
}
@@ -118,7 +99,14 @@ export const ResizeProvider = ({ value, children }: ResizeProviderProps) => {
11899
value.resizeRef.current.style.width = `${width}px`
119100
value.resizeRef.current.style.height = `${height}px`
120101
return true
121-
}, [value.resizeRef, draggingDirection, storageKey, minWidth, minHeight])
102+
}, [
103+
value.resizeRef,
104+
draggingDirection,
105+
storageKey,
106+
minWidth,
107+
minHeight,
108+
value.devToolsPanelSize,
109+
])
122110

123111
useLayoutEffect(() => {
124112
const applied = applyConstrainedDimensions()

packages/next/src/next-devtools/dev-overlay/components/errors/dev-tools-indicator/dev-tools-info/preferences.ts

Lines changed: 0 additions & 97 deletions
This file was deleted.

0 commit comments

Comments
 (0)