From 5bcf1762522f6f197f93473dd76961de2f13ce83 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Sun, 15 Jun 2025 18:08:06 +0200 Subject: [PATCH 1/4] [devtools] fork call stack --- .../call-stack/call-stack.stories.tsx | 89 ++++++++++++++ .../components/call-stack/call-stack.tsx | 113 ++++++++++++++++++ .../dev-overlay/icons/chevron-up-down.tsx | 18 +++ .../dev-overlay/styles/component-styles.tsx | 2 + 4 files changed, 222 insertions(+) create mode 100644 packages/next/src/next-devtools/dev-overlay/components/call-stack/call-stack.stories.tsx create mode 100644 packages/next/src/next-devtools/dev-overlay/components/call-stack/call-stack.tsx create mode 100644 packages/next/src/next-devtools/dev-overlay/icons/chevron-up-down.tsx diff --git a/packages/next/src/next-devtools/dev-overlay/components/call-stack/call-stack.stories.tsx b/packages/next/src/next-devtools/dev-overlay/components/call-stack/call-stack.stories.tsx new file mode 100644 index 0000000000000..3538a671d67e4 --- /dev/null +++ b/packages/next/src/next-devtools/dev-overlay/components/call-stack/call-stack.stories.tsx @@ -0,0 +1,89 @@ +import type { Meta, StoryObj } from '@storybook/react' +import { CallStack } from './call-stack' +import { withShadowPortal } from '../../storybook/with-shadow-portal' + +const meta: Meta = { + component: CallStack, + parameters: { + layout: 'fullscreen', + backgrounds: { + default: 'background-100-dark', + }, + a11y: { + config: { + rules: [ + { + id: 'color-contrast', + // Manual testing shows no violation. + // TODO: We might have setup more explicit backgrounds depending on theme. + enabled: false, + }, + ], + }, + }, + }, + decorators: [withShadowPortal], +} + +export default meta +type Story = StoryObj + +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 as const, + reason: null, + external: false, + ignored: false, +} + +const ignoredFrame = { + ...frame, + ignored: true, +} + +export const SingleFrame: Story = { + args: { + frames: [frame], + }, +} + +export const MultipleFrames: Story = { + args: { + frames: [ + frame, + { + ...frame, + originalStackFrame: { + ...frame.originalStackFrame, + methodName: 'ParentComponent', + lineNumber: 5, + }, + }, + ...Array(5).fill(ignoredFrame), + { + ...frame, + originalStackFrame: { + ...frame.originalStackFrame, + methodName: 'GrandparentComponent', + lineNumber: 1, + }, + }, + ...Array(5).fill(ignoredFrame), + ], + }, +} diff --git a/packages/next/src/next-devtools/dev-overlay/components/call-stack/call-stack.tsx b/packages/next/src/next-devtools/dev-overlay/components/call-stack/call-stack.tsx new file mode 100644 index 0000000000000..c1af62f66d523 --- /dev/null +++ b/packages/next/src/next-devtools/dev-overlay/components/call-stack/call-stack.tsx @@ -0,0 +1,113 @@ +import type { OriginalStackFrame } from '../../../shared/stack-frame' + +import { useMemo, useState } from 'react' +import { CallStackFrame } from '../call-stack-frame/call-stack-frame' +import { ChevronUpDownIcon } from '../../icons/chevron-up-down' +import { css } from '../../utils/css' + +export function CallStack({ frames }: { frames: OriginalStackFrame[] }) { + const [isIgnoreListOpen, setIsIgnoreListOpen] = useState(false) + + const ignoredFramesTally = useMemo(() => { + return frames.reduce((tally, frame) => tally + (frame.ignored ? 1 : 0), 0) + }, [frames]) + + return ( +
+
+

+ Call Stack {frames.length} +

+ {ignoredFramesTally > 0 && ( + + )} +
+ {frames.map((frame, frameIndex) => { + return !frame.ignored || isIgnoreListOpen ? ( + + ) : null + })} +
+ ) +} + +export const CALL_STACK_STYLES = css` + [data-nextjs-call-stack-container] { + position: relative; + margin-top: 8px; + } + + [data-nextjs-call-stack-header] { + display: flex; + justify-content: space-between; + align-items: center; + min-height: var(--size-28); + padding: 8px 8px 12px 4px; + width: 100%; + } + + [data-nextjs-call-stack-title] { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + + margin: 0; + + color: var(--color-gray-1000); + font-size: var(--size-16); + font-weight: 500; + } + + [data-nextjs-call-stack-count] { + display: flex; + justify-content: center; + align-items: center; + + width: var(--size-20); + height: var(--size-20); + gap: 4px; + + color: var(--color-gray-1000); + text-align: center; + font-size: var(--size-11); + font-weight: 500; + line-height: var(--size-16); + + border-radius: var(--rounded-full); + background: var(--color-gray-300); + } + + [data-nextjs-call-stack-ignored-list-toggle-button] { + all: unset; + display: flex; + align-items: center; + gap: 6px; + color: var(--color-gray-900); + font-size: var(--size-14); + line-height: var(--size-20); + border-radius: 6px; + padding: 4px 6px; + margin-right: -6px; + transition: background 150ms ease; + + &:hover { + background: var(--color-gray-100); + } + + &:focus { + outline: var(--focus-ring); + } + + svg { + width: var(--size-16); + height: var(--size-16); + } + } +` diff --git a/packages/next/src/next-devtools/dev-overlay/icons/chevron-up-down.tsx b/packages/next/src/next-devtools/dev-overlay/icons/chevron-up-down.tsx new file mode 100644 index 0000000000000..9c9f5f31a3d8f --- /dev/null +++ b/packages/next/src/next-devtools/dev-overlay/icons/chevron-up-down.tsx @@ -0,0 +1,18 @@ +export function ChevronUpDownIcon() { + return ( + + + + ) +} diff --git a/packages/next/src/next-devtools/dev-overlay/styles/component-styles.tsx b/packages/next/src/next-devtools/dev-overlay/styles/component-styles.tsx index c5cc5652f3d9b..1c262fe33e4f3 100644 --- a/packages/next/src/next-devtools/dev-overlay/styles/component-styles.tsx +++ b/packages/next/src/next-devtools/dev-overlay/styles/component-styles.tsx @@ -28,6 +28,7 @@ import { DEVTOOLS_PANEL_STYLES } from '../components/devtools-panel/devtools-pan import { DEVTOOLS_PANEL_FOOTER_STYLES } from '../components/devtools-panel/devtools-panel-footer' import { DEVTOOLS_PANEL_VERSION_INFO_STYLES } from '../components/devtools-panel/devtools-panel-version-info' import { DEVTOOLS_PANEL_TAB_SETTINGS_STYLES } from '../components/devtools-panel/devtools-panel-tab/settings-tab' +import { CALL_STACK_STYLES } from '../components/call-stack/call-stack' export function ComponentStyles() { return ( @@ -35,6 +36,7 @@ export function ComponentStyles() { {css` ${COPY_BUTTON_STYLES} ${CALL_STACK_FRAME_STYLES} + ${CALL_STACK_STYLES} ${ENVIRONMENT_NAME_LABEL_STYLES} ${overlay} ${toast} From 837f41d2096b267c58e782be084114a8a19dc423 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Thu, 19 Jun 2025 18:47:04 +0200 Subject: [PATCH 2/4] share callstack component with legacy Co-authored-by: Jiachi Liu --- .../components/call-stack/call-stack.tsx | 21 +-- .../errors/call-stack/call-stack.stories.tsx | 8 +- .../errors/call-stack/call-stack.tsx | 132 ++---------------- .../error-overlay-layout.tsx | 2 - .../container/runtime-error/index.tsx | 7 +- 5 files changed, 32 insertions(+), 138 deletions(-) diff --git a/packages/next/src/next-devtools/dev-overlay/components/call-stack/call-stack.tsx b/packages/next/src/next-devtools/dev-overlay/components/call-stack/call-stack.tsx index c1af62f66d523..b5ea7b9f9b0a3 100644 --- a/packages/next/src/next-devtools/dev-overlay/components/call-stack/call-stack.tsx +++ b/packages/next/src/next-devtools/dev-overlay/components/call-stack/call-stack.tsx @@ -1,17 +1,20 @@ import type { OriginalStackFrame } from '../../../shared/stack-frame' -import { useMemo, useState } from 'react' import { CallStackFrame } from '../call-stack-frame/call-stack-frame' import { ChevronUpDownIcon } from '../../icons/chevron-up-down' import { css } from '../../utils/css' -export function CallStack({ frames }: { frames: OriginalStackFrame[] }) { - const [isIgnoreListOpen, setIsIgnoreListOpen] = useState(false) - - const ignoredFramesTally = useMemo(() => { - return frames.reduce((tally, frame) => tally + (frame.ignored ? 1 : 0), 0) - }, [frames]) - +export function CallStack({ + frames, + isIgnoreListOpen, + ignoredFramesTally, + onToggleIgnoreList, +}: { + frames: OriginalStackFrame[] + isIgnoreListOpen: boolean + ignoredFramesTally: number + onToggleIgnoreList: () => void +}) { return (
@@ -21,7 +24,7 @@ export function CallStack({ frames }: { frames: OriginalStackFrame[] }) { {ignoredFramesTally > 0 && ( - )} -
- {frames.map((frame, frameIndex) => { - return !frame.ignored || isIgnoreListOpen ? ( - - ) : null - })} -
+ ) } - -function ChevronUpDown() { - return ( - - - - ) -} - -export const CALL_STACK_STYLES = ` - .error-overlay-call-stack-container { - position: relative; - margin-top: 8px; - } - - .error-overlay-call-stack-header { - display: flex; - justify-content: space-between; - align-items: center; - min-height: var(--size-28); - padding: 8px 8px 12px 4px; - width: 100%; - } - - .error-overlay-call-stack-title { - display: flex; - justify-content: space-between; - align-items: center; - gap: 8px; - - margin: 0; - - color: var(--color-gray-1000); - font-size: var(--size-16); - font-weight: 500; - } - - .error-overlay-call-stack-count { - display: flex; - justify-content: center; - align-items: center; - - width: var(--size-20); - height: var(--size-20); - gap: 4px; - - color: var(--color-gray-1000); - text-align: center; - font-size: var(--size-11); - font-weight: 500; - line-height: var(--size-16); - - border-radius: var(--rounded-full); - background: var(--color-gray-300); - } - - .error-overlay-call-stack-ignored-list-toggle-button { - all: unset; - display: flex; - align-items: center; - gap: 6px; - color: var(--color-gray-900); - font-size: var(--size-14); - line-height: var(--size-20); - border-radius: 6px; - padding: 4px 6px; - margin-right: -6px; - transition: background 150ms ease; - - &:hover { - background: var(--color-gray-100); - } - - &:focus { - outline: var(--focus-ring); - } - - svg { - width: var(--size-16); - height: var(--size-16); - } - } -` diff --git a/packages/next/src/next-devtools/dev-overlay/components/errors/error-overlay-layout/error-overlay-layout.tsx b/packages/next/src/next-devtools/dev-overlay/components/errors/error-overlay-layout/error-overlay-layout.tsx index 350cdc18c0b9d..7c943634c8a89 100644 --- a/packages/next/src/next-devtools/dev-overlay/components/errors/error-overlay-layout/error-overlay-layout.tsx +++ b/packages/next/src/next-devtools/dev-overlay/components/errors/error-overlay-layout/error-overlay-layout.tsx @@ -28,7 +28,6 @@ import { DIALOG_HEADER_STYLES, } from '../dialog/header' import { ErrorOverlayDialogBody, DIALOG_BODY_STYLES } from '../dialog/body' -import { CALL_STACK_STYLES } from '../call-stack/call-stack' import { OVERLAY_STYLES, ErrorOverlayOverlay } from '../overlay/overlay' import { ErrorOverlayBottomStack } from '../error-overlay-bottom-stack' import type { ErrorBaseProps } from '../error-overlay/error-overlay' @@ -186,7 +185,6 @@ export const styles = ` ${errorTypeLabelStyles} ${errorMessageStyles} ${toolbarStyles} - ${CALL_STACK_STYLES} [data-nextjs-error-label-group] { display: flex; diff --git a/packages/next/src/next-devtools/dev-overlay/container/runtime-error/index.tsx b/packages/next/src/next-devtools/dev-overlay/container/runtime-error/index.tsx index bfea61f549c71..973c96f81dc4a 100644 --- a/packages/next/src/next-devtools/dev-overlay/container/runtime-error/index.tsx +++ b/packages/next/src/next-devtools/dev-overlay/container/runtime-error/index.tsx @@ -1,6 +1,6 @@ import { useMemo } from 'react' import { CodeFrame } from '../../components/code-frame/code-frame' -import { CallStack } from '../../components/errors/call-stack/call-stack' +import { ErrorOverlayCallStack } from '../../components/errors/call-stack/call-stack' import { PSEUDO_HTML_DIFF_STYLES } from './component-stack-pseudo-html' import { useFrames, @@ -36,7 +36,10 @@ export function RuntimeError({ error, dialogResizerRef }: RuntimeErrorProps) { )} {frames.length > 0 && ( - + )} ) From 92af623d26e8789fc912eb6c2038c0e9f82227b7 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Thu, 19 Jun 2025 19:01:40 +0200 Subject: [PATCH 3/4] rename to error-overlay-call-stack Co-authored-by: Jiachi Liu --- .../error-overlay-call-stack.stories.tsx} | 2 +- .../error-overlay-call-stack.tsx} | 0 .../next-devtools/dev-overlay/container/runtime-error/index.tsx | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/next/src/next-devtools/dev-overlay/components/errors/{call-stack/call-stack.stories.tsx => error-overlay-call-stack/error-overlay-call-stack.stories.tsx} (96%) rename packages/next/src/next-devtools/dev-overlay/components/errors/{call-stack/call-stack.tsx => error-overlay-call-stack/error-overlay-call-stack.tsx} (100%) diff --git a/packages/next/src/next-devtools/dev-overlay/components/errors/call-stack/call-stack.stories.tsx b/packages/next/src/next-devtools/dev-overlay/components/errors/error-overlay-call-stack/error-overlay-call-stack.stories.tsx similarity index 96% rename from packages/next/src/next-devtools/dev-overlay/components/errors/call-stack/call-stack.stories.tsx rename to packages/next/src/next-devtools/dev-overlay/components/errors/error-overlay-call-stack/error-overlay-call-stack.stories.tsx index d14a1986f18c3..eefc507caf531 100644 --- a/packages/next/src/next-devtools/dev-overlay/components/errors/call-stack/call-stack.stories.tsx +++ b/packages/next/src/next-devtools/dev-overlay/components/errors/error-overlay-call-stack/error-overlay-call-stack.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from '@storybook/react' -import { ErrorOverlayCallStack } from './call-stack' +import { ErrorOverlayCallStack } from './error-overlay-call-stack' import { withShadowPortal } from '../../../storybook/with-shadow-portal' const meta: Meta = { diff --git a/packages/next/src/next-devtools/dev-overlay/components/errors/call-stack/call-stack.tsx b/packages/next/src/next-devtools/dev-overlay/components/errors/error-overlay-call-stack/error-overlay-call-stack.tsx similarity index 100% rename from packages/next/src/next-devtools/dev-overlay/components/errors/call-stack/call-stack.tsx rename to packages/next/src/next-devtools/dev-overlay/components/errors/error-overlay-call-stack/error-overlay-call-stack.tsx diff --git a/packages/next/src/next-devtools/dev-overlay/container/runtime-error/index.tsx b/packages/next/src/next-devtools/dev-overlay/container/runtime-error/index.tsx index 973c96f81dc4a..c2e14da49237f 100644 --- a/packages/next/src/next-devtools/dev-overlay/container/runtime-error/index.tsx +++ b/packages/next/src/next-devtools/dev-overlay/container/runtime-error/index.tsx @@ -1,6 +1,6 @@ import { useMemo } from 'react' import { CodeFrame } from '../../components/code-frame/code-frame' -import { ErrorOverlayCallStack } from '../../components/errors/call-stack/call-stack' +import { ErrorOverlayCallStack } from '../../components/errors/error-overlay-call-stack/error-overlay-call-stack' import { PSEUDO_HTML_DIFF_STYLES } from './component-stack-pseudo-html' import { useFrames, From 2624d41b8c07f5e53a27cb8fe7de66dc946d4138 Mon Sep 17 00:00:00 2001 From: devjiwonchoi Date: Fri, 20 Jun 2025 16:29:36 +0200 Subject: [PATCH 4/4] fix: match attr for test --- .../dev-overlay/components/call-stack/call-stack.tsx | 3 ++- test/lib/next-test-utils.ts | 12 +++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/next/src/next-devtools/dev-overlay/components/call-stack/call-stack.tsx b/packages/next/src/next-devtools/dev-overlay/components/call-stack/call-stack.tsx index b5ea7b9f9b0a3..16fcbb6585e39 100644 --- a/packages/next/src/next-devtools/dev-overlay/components/call-stack/call-stack.tsx +++ b/packages/next/src/next-devtools/dev-overlay/components/call-stack/call-stack.tsx @@ -23,7 +23,8 @@ export function CallStack({

{ignoredFramesTally > 0 && (