From 926792d925fe42c1731301a43db0d404ec0a25d0 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Fri, 20 Jun 2025 18:32:22 +0200 Subject: [PATCH 1/2] [devtools] storybook: port utils to be reusable --- .../dev-overlay/container/errors.stories.tsx | 140 +------------ .../dev-overlay/dev-overlay.stories.tsx | 117 ++--------- .../dev-overlay/storybook/errors.ts | 184 ++++++++++++++++++ .../storybook/use-overlay-reducer.ts | 58 ++++++ packages/next/tsconfig.build.json | 3 +- 5 files changed, 257 insertions(+), 245 deletions(-) create mode 100644 packages/next/src/next-devtools/dev-overlay/storybook/errors.ts create mode 100644 packages/next/src/next-devtools/dev-overlay/storybook/use-overlay-reducer.ts diff --git a/packages/next/src/next-devtools/dev-overlay/container/errors.stories.tsx b/packages/next/src/next-devtools/dev-overlay/container/errors.stories.tsx index 2ab9c068cf71..5c0dc1976b2c 100644 --- a/packages/next/src/next-devtools/dev-overlay/container/errors.stories.tsx +++ b/packages/next/src/next-devtools/dev-overlay/container/errors.stories.tsx @@ -2,8 +2,8 @@ import type { Meta, StoryObj } from '@storybook/react' import { Errors } from './errors' import { withShadowPortal } from '../storybook/with-shadow-portal' -import type { ReadyRuntimeError } from '../utils/get-error-by-type' import { lorem } from '../utils/lorem' +import { runtimeErrors } from '../storybook/errors' const meta: Meta = { component: Errors, @@ -16,144 +16,6 @@ const meta: Meta = { export default meta type Story = StoryObj -const originalCodeFrame = (message: string) => { - return `\u001b[0m \u001b[90m 1 \u001b[39m \u001b[36mexport\u001b[39m \u001b[36mdefault\u001b[39m \u001b[36mfunction\u001b[39m \u001b[33mHome\u001b[39m() {\u001b[0m -\u001b[0m\u001b[31m\u001b[1m>\u001b[22m\u001b[39m\u001b[90m 2 \u001b[39m \u001b[36mthrow\u001b[39m \u001b[36mnew\u001b[39m \u001b[33mError\u001b[39m(\u001b[32m'${message}'\u001b[39m)\u001b[0m -\u001b[0m \u001b[90m \u001b[39m \u001b[31m\u001b[1m^\u001b[22m\u001b[39m\u001b[0m -\u001b[0m \u001b[90m 3 \u001b[39m \u001b[36mreturn\u001b[39m \u001b[33m<\u001b[39m\u001b[33mdiv\u001b[39m\u001b[33m>\u001b[39m\u001b[33mWelcome to my Next.js application! This is a longer piece of text that will demonstrate text wrapping behavior in the code frame.\u001b[39m\u001b[33m<\u001b[39m\u001b[33m/\u001b[39m\u001b[33mdiv\u001b[39m\u001b[33m>\u001b[39m\u001b[0m -\u001b[0m \u001b[90m 4 \u001b[39m }\u001b[0m -\u001b[0m \u001b[90m 5 \u001b[39m\u001b[0m` -} - -const sourceStackFrame = { - file: 'app/page.tsx', - methodName: 'Home', - arguments: [], - lineNumber: 2, - column: 9, -} - -const originalStackFrame = { - file: 'app/page.tsx', - methodName: 'Home', - arguments: [], - lineNumber: 2, - column: 9, - ignored: false, -} - -const frame = { - originalStackFrame: { - file: './app/page.tsx', - methodName: 'MyComponent', - arguments: [], - lineNumber: 10, - column: 5, - ignored: false, - }, - sourceStackFrame: { - file: './app/page.tsx', - methodName: 'MyComponent', - arguments: [], - lineNumber: 10, - column: 5, - }, - originalCodeFrame: 'export default function MyComponent() {', - error: false, - reason: null, - external: false, - ignored: false, -} - -const ignoredFrame = { - ...frame, - ignored: true, -} - -const runtimeErrors: ReadyRuntimeError[] = [ - { - id: 1, - runtime: true, - error: new Error('First error message'), - frames: () => - Promise.resolve([ - frame, - { - ...frame, - originalStackFrame: { - ...frame.originalStackFrame, - methodName: 'ParentComponent', - lineNumber: 5, - }, - }, - { - ...frame, - originalStackFrame: { - ...frame.originalStackFrame, - methodName: 'GrandparentComponent', - lineNumber: 1, - }, - }, - ...Array(20).fill(ignoredFrame), - ]), - type: 'runtime', - }, - { - id: 2, - runtime: true, - error: new Error('Second error message'), - frames: () => - Promise.resolve([ - { - error: true, - reason: 'Second error message', - external: false, - ignored: false, - sourceStackFrame, - originalStackFrame, - originalCodeFrame: originalCodeFrame('Second error message'), - }, - ]), - type: 'runtime', - }, - { - id: 3, - runtime: true, - error: new Error('Third error message'), - frames: () => - Promise.resolve([ - { - error: true, - reason: 'Third error message', - external: false, - ignored: false, - sourceStackFrame, - originalStackFrame, - originalCodeFrame: originalCodeFrame('Third error message'), - }, - ]), - type: 'runtime', - }, - { - id: 4, - runtime: true, - error: new Error('typeof window !== undefined'), - frames: () => - Promise.resolve([ - { - error: true, - reason: 'typeof window !== undefined', - external: false, - ignored: false, - sourceStackFrame, - originalStackFrame, - originalCodeFrame: originalCodeFrame('typeof window !== undefined'), - }, - ]), - type: 'runtime', - }, -] - export const Default: Story = { args: { getSquashedHydrationErrorDetails: () => null, diff --git a/packages/next/src/next-devtools/dev-overlay/dev-overlay.stories.tsx b/packages/next/src/next-devtools/dev-overlay/dev-overlay.stories.tsx index 1ff10db59049..34cd7054ba52 100644 --- a/packages/next/src/next-devtools/dev-overlay/dev-overlay.stories.tsx +++ b/packages/next/src/next-devtools/dev-overlay/dev-overlay.stories.tsx @@ -1,22 +1,15 @@ import type { Meta, StoryObj } from '@storybook/react' -import type { DispatcherEvent, OverlayState } from './shared' +import type { OverlayState } from './shared' // @ts-expect-error import imgApp from './app.png' -import { useReducer } from 'react' import { DevOverlay } from './dev-overlay' +import { errors } from './storybook/errors' import { - ACTION_DEVTOOLS_POSITION, - ACTION_DEVTOOLS_PANEL_CLOSE, - ACTION_DEVTOOLS_PANEL_TOGGLE, - ACTION_ERROR_OVERLAY_CLOSE, - ACTION_ERROR_OVERLAY_OPEN, - ACTION_ERROR_OVERLAY_TOGGLE, - ACTION_DEVTOOLS_SCALE, - NEXT_DEV_TOOLS_SCALE, - INITIAL_OVERLAY_STATE, -} from './shared' + storybookDefaultOverlayState, + useStorybookOverlayReducer, +} from './storybook/use-overlay-reducer' const meta: Meta = { component: DevOverlay, @@ -40,104 +33,18 @@ const meta: Meta = { export default meta type Story = StoryObj -const initialState: OverlayState = { - ...INITIAL_OVERLAY_STATE, - routerType: 'app', - showIndicator: true, - errors: [ - { - id: 1, - error: Object.assign(new Error('First error message'), { - __NEXT_ERROR_CODE: 'E001', - }), - componentStackFrames: [ - { - file: 'app/page.tsx', - component: 'Home', - lineNumber: 10, - column: 5, - canOpenInEditor: true, - }, - ], - frames: [ - { - file: 'app/page.tsx', - methodName: 'Home', - arguments: [], - lineNumber: 10, - column: 5, - }, - ], - type: 'runtime', - }, - { - id: 2, - error: Object.assign(new Error('Second error message'), { - __NEXT_ERROR_CODE: 'E002', - }), - frames: [], - type: 'runtime', - }, - { - id: 3, - error: Object.assign(new Error('Third error message'), { - __NEXT_ERROR_CODE: 'E003', - }), - frames: [], - type: 'runtime', - }, - ], - versionInfo: { - installed: '15.2.0', - staleness: 'fresh', - }, - isErrorOverlayOpen: false, - isDevToolsPanelOpen: false, - devToolsPosition: 'bottom-left', - scale: NEXT_DEV_TOOLS_SCALE.Medium, -} - -function useOverlayReducer() { - return useReducer( - (state, action): OverlayState => { - switch (action.type) { - case ACTION_ERROR_OVERLAY_CLOSE: { - return { ...state, isErrorOverlayOpen: false } - } - case ACTION_ERROR_OVERLAY_OPEN: { - return { ...state, isErrorOverlayOpen: true } - } - case ACTION_ERROR_OVERLAY_TOGGLE: { - return { ...state, isErrorOverlayOpen: !state.isErrorOverlayOpen } - } - case ACTION_DEVTOOLS_PANEL_TOGGLE: { - return { ...state, isDevToolsPanelOpen: !state.isDevToolsPanelOpen } - } - case ACTION_DEVTOOLS_PANEL_CLOSE: { - return { ...state, isDevToolsPanelOpen: false } - } - case ACTION_DEVTOOLS_POSITION: { - return { ...state, devToolsPosition: action.devToolsPosition } - } - case ACTION_DEVTOOLS_SCALE: { - return { ...state, scale: action.scale } - } - default: { - return state - } - } - }, - initialState - ) -} - function getNoSquashedHydrationErrorDetails() { return null } +const initialState: OverlayState = { + ...storybookDefaultOverlayState, + errors, +} + export const Default: Story = { render: function DevOverlayStory() { - const [state, dispatch] = useOverlayReducer() + const [state, dispatch] = useStorybookOverlayReducer(initialState) return ( <> { + return `\u001b[0m \u001b[90m 1 \u001b[39m \u001b[36mexport\u001b[39m \u001b[36mdefault\u001b[39m \u001b[36mfunction\u001b[39m \u001b[33mHome\u001b[39m() {\u001b[0m +\u001b[0m\u001b[31m\u001b[1m>\u001b[22m\u001b[39m\u001b[90m 2 \u001b[39m \u001b[36mthrow\u001b[39m \u001b[36mnew\u001b[39m \u001b[33mError\u001b[39m(\u001b[32m'${message}'\u001b[39m)\u001b[0m +\u001b[0m \u001b[90m \u001b[39m \u001b[31m\u001b[1m^\u001b[22m\u001b[39m\u001b[0m +\u001b[0m \u001b[90m 3 \u001b[39m \u001b[36mreturn\u001b[39m \u001b[33m<\u001b[39m\u001b[33mdiv\u001b[39m\u001b[33m>\u001b[39m\u001b[33mWelcome to my Next.js application! This is a longer piece of text that will demonstrate text wrapping behavior in the code frame.\u001b[39m\u001b[33m<\u001b[39m\u001b[33m/\u001b[39m\u001b[33mdiv\u001b[39m\u001b[33m>\u001b[39m\u001b[0m +\u001b[0m \u001b[90m 4 \u001b[39m }\u001b[0m +\u001b[0m \u001b[90m 5 \u001b[39m\u001b[0m` +} + +const sourceStackFrame = { + file: 'app/page.tsx', + methodName: 'Home', + arguments: [], + lineNumber: 2, + column: 9, +} + +const originalStackFrame = { + file: 'app/page.tsx', + methodName: 'Home', + arguments: [], + lineNumber: 2, + column: 9, + ignored: false, +} + +const frame = { + originalStackFrame: { + file: './app/page.tsx', + methodName: 'MyComponent', + arguments: [], + lineNumber: 10, + column: 5, + ignored: false, + }, + sourceStackFrame: { + file: './app/page.tsx', + methodName: 'MyComponent', + arguments: [], + lineNumber: 10, + column: 5, + }, + originalCodeFrame: 'export default function MyComponent() {', + error: false, + reason: null, + external: false, + ignored: false, +} + +const ignoredFrame = { + ...frame, + ignored: true, +} + +export const errors: SupportedErrorEvent[] = [ + { + id: 1, + error: Object.assign(new Error('First error message'), { + __NEXT_ERROR_CODE: 'E001', + }), + componentStackFrames: [ + { + file: 'app/page.tsx', + component: 'Home', + lineNumber: 10, + column: 5, + canOpenInEditor: true, + }, + ], + frames: [ + { + file: 'app/page.tsx', + methodName: 'Home', + arguments: [], + lineNumber: 10, + column: 5, + }, + ], + type: 'runtime', + }, + { + id: 2, + error: Object.assign(new Error('Second error message'), { + __NEXT_ERROR_CODE: 'E002', + }), + frames: [], + type: 'runtime', + }, + { + id: 3, + error: Object.assign(new Error('Third error message'), { + __NEXT_ERROR_CODE: 'E003', + }), + frames: [], + type: 'runtime', + }, +] + +export const runtimeErrors: ReadyRuntimeError[] = [ + { + id: 1, + runtime: true, + error: new Error('First error message'), + frames: () => + Promise.resolve([ + frame, + { + ...frame, + originalStackFrame: { + ...frame.originalStackFrame, + methodName: 'ParentComponent', + lineNumber: 5, + }, + }, + { + ...frame, + originalStackFrame: { + ...frame.originalStackFrame, + methodName: 'GrandparentComponent', + lineNumber: 1, + }, + }, + ...Array(20).fill(ignoredFrame), + ]), + type: 'runtime', + }, + { + id: 2, + runtime: true, + error: new Error('Second error message'), + frames: () => + Promise.resolve([ + { + error: true, + reason: 'Second error message', + external: false, + ignored: false, + sourceStackFrame, + originalStackFrame, + originalCodeFrame: originalCodeFrame('Second error message'), + }, + ]), + type: 'console', + }, + { + id: 3, + runtime: true, + error: new Error('Third error message'), + frames: () => + Promise.resolve([ + { + error: true, + reason: 'Third error message', + external: false, + ignored: false, + sourceStackFrame, + originalStackFrame, + originalCodeFrame: originalCodeFrame('Third error message'), + }, + ]), + type: 'recoverable', + }, + { + id: 4, + runtime: true, + error: new Error('typeof window !== undefined'), + frames: () => + Promise.resolve([ + { + error: true, + reason: 'typeof window !== undefined', + external: false, + ignored: false, + sourceStackFrame, + originalStackFrame, + originalCodeFrame: originalCodeFrame('typeof window !== undefined'), + }, + ]), + type: 'runtime', + }, +] diff --git a/packages/next/src/next-devtools/dev-overlay/storybook/use-overlay-reducer.ts b/packages/next/src/next-devtools/dev-overlay/storybook/use-overlay-reducer.ts new file mode 100644 index 000000000000..5f859623895e --- /dev/null +++ b/packages/next/src/next-devtools/dev-overlay/storybook/use-overlay-reducer.ts @@ -0,0 +1,58 @@ +import type { DispatcherEvent, OverlayState } from '../shared' + +import { useReducer } from 'react' +import { + ACTION_DEVTOOLS_POSITION, + ACTION_DEVTOOLS_PANEL_CLOSE, + ACTION_DEVTOOLS_PANEL_TOGGLE, + ACTION_ERROR_OVERLAY_CLOSE, + ACTION_ERROR_OVERLAY_OPEN, + ACTION_ERROR_OVERLAY_TOGGLE, + ACTION_DEVTOOLS_SCALE, + INITIAL_OVERLAY_STATE, +} from '../shared' + +export const storybookDefaultOverlayState: OverlayState = { + ...INITIAL_OVERLAY_STATE, + routerType: 'app', + isErrorOverlayOpen: true, + showIndicator: true, + versionInfo: { + installed: '15.4.0', + staleness: 'fresh', + }, +} + +export function useStorybookOverlayReducer(initialState?: OverlayState) { + return useReducer( + (state, action): OverlayState => { + switch (action.type) { + case ACTION_ERROR_OVERLAY_CLOSE: { + return { ...state, isErrorOverlayOpen: false } + } + case ACTION_ERROR_OVERLAY_OPEN: { + return { ...state, isErrorOverlayOpen: true } + } + case ACTION_ERROR_OVERLAY_TOGGLE: { + return { ...state, isErrorOverlayOpen: !state.isErrorOverlayOpen } + } + case ACTION_DEVTOOLS_PANEL_TOGGLE: { + return { ...state, isDevToolsPanelOpen: !state.isDevToolsPanelOpen } + } + case ACTION_DEVTOOLS_PANEL_CLOSE: { + return { ...state, isDevToolsPanelOpen: false } + } + case ACTION_DEVTOOLS_POSITION: { + return { ...state, devToolsPosition: action.devToolsPosition } + } + case ACTION_DEVTOOLS_SCALE: { + return { ...state, scale: action.scale } + } + default: { + return state + } + } + }, + initialState || storybookDefaultOverlayState + ) +} diff --git a/packages/next/tsconfig.build.json b/packages/next/tsconfig.build.json index 0190e0c0147a..7463f0bc09d8 100644 --- a/packages/next/tsconfig.build.json +++ b/packages/next/tsconfig.build.json @@ -15,6 +15,7 @@ // Don't generate types for internal files. "./**/*.test.ts", "./**/*.test.tsx", - "./**/*.stories.tsx" + "./**/*.stories.tsx", + "./**/storybook/**/*" ] } From 767f50e671c4912293848f733bd64968a1175489 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Fri, 20 Jun 2025 18:51:29 +0200 Subject: [PATCH 2/2] needs initialState too --- .../next/src/next-devtools/dev-overlay/dev-overlay.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/src/next-devtools/dev-overlay/dev-overlay.stories.tsx b/packages/next/src/next-devtools/dev-overlay/dev-overlay.stories.tsx index 34cd7054ba52..68ab6c82e0a9 100644 --- a/packages/next/src/next-devtools/dev-overlay/dev-overlay.stories.tsx +++ b/packages/next/src/next-devtools/dev-overlay/dev-overlay.stories.tsx @@ -78,7 +78,7 @@ export const WithPanel: Story = { } }, render: function DevOverlayStory() { - const [state, dispatch] = useStorybookOverlayReducer() + const [state, dispatch] = useStorybookOverlayReducer(initialState) return ( <>