From 4bad5d1bbbebe013ca98502908a743f867e4d998 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 30 Oct 2024 21:52:43 +0100 Subject: [PATCH 1/8] display stitched error --- .../client/components/react-dev-overlay/app/ReactDevOverlay.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/src/client/components/react-dev-overlay/app/ReactDevOverlay.tsx b/packages/next/src/client/components/react-dev-overlay/app/ReactDevOverlay.tsx index 75cfeb06eb94b..b0333e8e69023 100644 --- a/packages/next/src/client/components/react-dev-overlay/app/ReactDevOverlay.tsx +++ b/packages/next/src/client/components/react-dev-overlay/app/ReactDevOverlay.tsx @@ -79,7 +79,7 @@ export default class ReactDevOverlay extends React.PureComponent< Date: Wed, 30 Oct 2024 22:45:03 +0100 Subject: [PATCH 2/8] handle cause --- .../client/react-client-callbacks/shared.ts | 9 +++-- .../acceptance-app/dynamic-error.test.ts | 37 +++++++++---------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/packages/next/src/client/react-client-callbacks/shared.ts b/packages/next/src/client/react-client-callbacks/shared.ts index 645e4b473332e..9347e75a89fd2 100644 --- a/packages/next/src/client/react-client-callbacks/shared.ts +++ b/packages/next/src/client/react-client-callbacks/shared.ts @@ -4,18 +4,21 @@ import type { HydrationOptions } from 'react-dom/client' import { isBailoutToCSRError } from '../../shared/lib/lazy-dynamic/bailout-to-csr' import { reportGlobalError } from './report-global-error' import { getReactStitchedError } from '../components/react-dev-overlay/internal/helpers/stitched-error' +import isError from '../../lib/is-error' export const onRecoverableError: HydrationOptions['onRecoverableError'] = ( - err, + error, errorInfo ) => { - const stitchedError = getReactStitchedError(err) + // x-ref: https://github.com/facebook/react/pull/28736 + const cause = isError(error) && 'cause' in error ? error.cause : error + const stitchedError = getReactStitchedError(cause) // In development mode, pass along the component stack to the error if (process.env.NODE_ENV === 'development' && errorInfo.componentStack) { ;(stitchedError as any)._componentStack = errorInfo.componentStack } // Skip certain custom errors which are not expected to be reported on client - if (isBailoutToCSRError(err)) return + if (isBailoutToCSRError(cause)) return reportGlobalError(stitchedError) } diff --git a/test/development/acceptance-app/dynamic-error.test.ts b/test/development/acceptance-app/dynamic-error.test.ts index 1e7e53583598f..258945892e6b4 100644 --- a/test/development/acceptance-app/dynamic-error.test.ts +++ b/test/development/acceptance-app/dynamic-error.test.ts @@ -7,31 +7,30 @@ import { outdent } from 'outdent' describe('dynamic = "error" in devmode', () => { const { next } = nextTestSetup({ files: new FileRef(path.join(__dirname, 'fixtures', 'default-template')), - skipStart: true, }) it('should show error overlay when dynamic is forced', async () => { - const { session, cleanup } = await sandbox(next, undefined, '/server') - - // dynamic = "error" and force dynamic - await session.patch( - 'app/server/page.js', - outdent` - import { cookies } from 'next/headers'; - - import Component from '../../index' - - export default async function Page() { - await cookies() - return - } - - export const dynamic = "error" - ` + const { session, cleanup } = await sandbox( + next, + new Map([ + [ + 'app/server/page.js', + outdent` + import { cookies } from 'next/headers'; + + export default async function Page() { + await cookies() + return null + } + + export const dynamic = "error" + `, + ], + ]), + '/server' ) await session.assertHasRedbox() - console.log(await session.getRedboxDescription()) expect(await session.getRedboxDescription()).toMatchInlineSnapshot( `"[ Server ] Error: Route /server with \`dynamic = "error"\` couldn't be rendered statically because it used \`cookies\`. See more info here: https://nextjs.org/docs/app/building-your-application/rendering/static-and-dynamic#dynamic-rendering"` ) From faefe3e32428eb4b6933826d0500d1b75b5a9616 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 30 Oct 2024 23:13:15 +0100 Subject: [PATCH 3/8] test: add case for handling invalid element --- .../app/browser/browser-only.js | 11 ++ .../invalid-element-type/app/browser/page.js | 11 ++ .../app-dir/invalid-element-type/app/foo.js | 0 .../invalid-element-type/app/layout.tsx | 8 + .../invalid-element-type/app/rsc/page.js | 9 ++ .../invalid-element-type/app/ssr/page.js | 11 ++ .../invalid-element-type.test.ts | 147 ++++++++++++++++++ .../invalid-element-type/next.config.js | 10 ++ 8 files changed, 207 insertions(+) create mode 100644 test/development/app-dir/invalid-element-type/app/browser/browser-only.js create mode 100644 test/development/app-dir/invalid-element-type/app/browser/page.js create mode 100644 test/development/app-dir/invalid-element-type/app/foo.js create mode 100644 test/development/app-dir/invalid-element-type/app/layout.tsx create mode 100644 test/development/app-dir/invalid-element-type/app/rsc/page.js create mode 100644 test/development/app-dir/invalid-element-type/app/ssr/page.js create mode 100644 test/development/app-dir/invalid-element-type/invalid-element-type.test.ts create mode 100644 test/development/app-dir/invalid-element-type/next.config.js diff --git a/test/development/app-dir/invalid-element-type/app/browser/browser-only.js b/test/development/app-dir/invalid-element-type/app/browser/browser-only.js new file mode 100644 index 0000000000000..dc00bde91399b --- /dev/null +++ b/test/development/app-dir/invalid-element-type/app/browser/browser-only.js @@ -0,0 +1,11 @@ +'use client' + +import Foo from '../foo' + +export default function BrowserOnly() { + return ( +
+ +
+ ) +} diff --git a/test/development/app-dir/invalid-element-type/app/browser/page.js b/test/development/app-dir/invalid-element-type/app/browser/page.js new file mode 100644 index 0000000000000..77875a1db1096 --- /dev/null +++ b/test/development/app-dir/invalid-element-type/app/browser/page.js @@ -0,0 +1,11 @@ +'use client' + +import dynamic from 'next/dynamic' + +const BrowserOnly = dynamic(() => import('./browser-only'), { + ssr: false, +}) + +export default function Page() { + return +} diff --git a/test/development/app-dir/invalid-element-type/app/foo.js b/test/development/app-dir/invalid-element-type/app/foo.js new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/test/development/app-dir/invalid-element-type/app/layout.tsx b/test/development/app-dir/invalid-element-type/app/layout.tsx new file mode 100644 index 0000000000000..888614deda3ba --- /dev/null +++ b/test/development/app-dir/invalid-element-type/app/layout.tsx @@ -0,0 +1,8 @@ +import { ReactNode } from 'react' +export default function Root({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/development/app-dir/invalid-element-type/app/rsc/page.js b/test/development/app-dir/invalid-element-type/app/rsc/page.js new file mode 100644 index 0000000000000..3b835737b3983 --- /dev/null +++ b/test/development/app-dir/invalid-element-type/app/rsc/page.js @@ -0,0 +1,9 @@ +import Foo from '../foo' + +export default function Page() { + return ( +
+ +
+ ) +} diff --git a/test/development/app-dir/invalid-element-type/app/ssr/page.js b/test/development/app-dir/invalid-element-type/app/ssr/page.js new file mode 100644 index 0000000000000..297ed0e3ef260 --- /dev/null +++ b/test/development/app-dir/invalid-element-type/app/ssr/page.js @@ -0,0 +1,11 @@ +'use client' + +import Foo from '../foo' + +export default function Page() { + return ( +
+ +
+ ) +} diff --git a/test/development/app-dir/invalid-element-type/invalid-element-type.test.ts b/test/development/app-dir/invalid-element-type/invalid-element-type.test.ts new file mode 100644 index 0000000000000..dfdc08aab00b1 --- /dev/null +++ b/test/development/app-dir/invalid-element-type/invalid-element-type.test.ts @@ -0,0 +1,147 @@ +import { nextTestSetup } from 'e2e-utils' +import { assertHasRedbox, getRedboxSource } from 'next-test-utils' + +async function getStackFramesContent(browser) { + const stackFrameElements = await browser.elementsByCss( + '[data-nextjs-call-stack-frame]' + ) + const stackFramesContent = ( + await Promise.all( + stackFrameElements.map(async (frame) => { + const functionNameEl = await frame.$('[data-nextjs-frame-expanded]') + const sourceEl = await frame.$('[data-has-source]') + const functionName = functionNameEl + ? await functionNameEl.innerText() + : '' + const source = sourceEl ? await sourceEl.innerText() : '' + + if (!functionName) { + return '' + } + return `at ${functionName} (${source})` + }) + ) + ) + .filter(Boolean) + .join('\n') + + return stackFramesContent +} + +describe('app-dir - invalid-element-type', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should catch invalid element from on client-only component', async () => { + const browser = await next.browser('/browser') + + await assertHasRedbox(browser) + const source = await getRedboxSource(browser) + + const stackFramesContent = await getStackFramesContent(browser) + if (process.env.TURBOPACK) { + expect(stackFramesContent).toMatchInlineSnapshot( + `"at Page (app/browser/page.js (10:10))"` + ) + expect(source).toMatchInlineSnapshot(` + "app/browser/browser-only.js (8:7) @ BrowserOnly + + 6 | return ( + 7 |
+ > 8 | + | ^ + 9 |
+ 10 | ) + 11 | }" + `) + } else { + expect(stackFramesContent).toMatchInlineSnapshot( + `"at BrowserOnly (app/browser/page.js (10:11))"` + ) + expect(source).toMatchInlineSnapshot(` + "app/browser/browser-only.js (8:8) @ Foo + + 6 | return ( + 7 |
+ > 8 | + | ^ + 9 |
+ 10 | ) + 11 | }" + `) + } + }) + + it('should catch invalid element from on rsc component', async () => { + const browser = await next.browser('/rsc') + + await assertHasRedbox(browser) + const stackFramesContent = await getStackFramesContent(browser) + const source = await getRedboxSource(browser) + + if (process.env.TURBOPACK) { + expect(stackFramesContent).toMatchInlineSnapshot(`""`) + expect(source).toMatchInlineSnapshot(` + "app/rsc/page.js (6:7) @ Page + + 4 | return ( + 5 |
+ > 6 | + | ^ + 7 |
+ 8 | ) + 9 | }" + `) + } else { + expect(stackFramesContent).toMatchInlineSnapshot(`""`) + expect(source).toMatchInlineSnapshot(` + "app/rsc/page.js (6:8) @ Foo + + 4 | return ( + 5 |
+ > 6 | + | ^ + 7 |
+ 8 | ) + 9 | }" + `) + } + }) + + it('should catch invalid element from on ssr client component', async () => { + const browser = await next.browser('/ssr') + + await assertHasRedbox(browser) + + const stackFramesContent = await getStackFramesContent(browser) + const source = await getRedboxSource(browser) + if (process.env.TURBOPACK) { + expect(stackFramesContent).toMatchInlineSnapshot(`""`) + expect(source).toMatchInlineSnapshot(` + "app/ssr/page.js (8:7) @ Page + + 6 | return ( + 7 |
+ > 8 | + | ^ + 9 |
+ 10 | ) + 11 | }" + `) + } else { + expect(stackFramesContent).toMatchInlineSnapshot(`""`) + expect(source).toMatchInlineSnapshot(` + "app/ssr/page.js (8:8) @ Foo + + 6 | return ( + 7 |
+ > 8 | + | ^ + 9 |
+ 10 | ) + 11 | }" + `) + } + }) +}) diff --git a/test/development/app-dir/invalid-element-type/next.config.js b/test/development/app-dir/invalid-element-type/next.config.js new file mode 100644 index 0000000000000..d14b1bf8fdc37 --- /dev/null +++ b/test/development/app-dir/invalid-element-type/next.config.js @@ -0,0 +1,10 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = { + experimental: { + reactOwnerStack: true, + }, +} + +module.exports = nextConfig From 38e28676834d5b03d875194d4eb9c1639ce6648e Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Wed, 30 Oct 2024 23:28:20 +0100 Subject: [PATCH 4/8] do no map react error state --- .../react-dev-overlay/app/ReactDevOverlay.tsx | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/packages/next/src/client/components/react-dev-overlay/app/ReactDevOverlay.tsx b/packages/next/src/client/components/react-dev-overlay/app/ReactDevOverlay.tsx index b0333e8e69023..2b46347d4fa2d 100644 --- a/packages/next/src/client/components/react-dev-overlay/app/ReactDevOverlay.tsx +++ b/packages/next/src/client/components/react-dev-overlay/app/ReactDevOverlay.tsx @@ -1,9 +1,8 @@ import React from 'react' -import { ACTION_UNHANDLED_ERROR, type OverlayState } from '../shared' +import type { OverlayState } from '../shared' import { ShadowPortal } from '../internal/components/ShadowPortal' import { BuildError } from '../internal/container/BuildError' -import { Errors, type SupportedErrorEvent } from '../internal/container/Errors' -import { parseStack } from '../internal/helpers/parse-stack' +import { Errors } from '../internal/container/Errors' import { StaticIndicator } from '../internal/container/StaticIndicator' import { Base } from '../internal/styles/Base' import { ComponentStyles } from '../internal/styles/ComponentStyles' @@ -13,7 +12,7 @@ import type { Dispatcher } from './hot-reloader-client' import { RuntimeErrorHandler } from '../internal/helpers/runtime-error-handler' interface ReactDevOverlayState { - reactError: SupportedErrorEvent | null + isReactError: boolean } export default class ReactDevOverlay extends React.PureComponent< { @@ -23,27 +22,20 @@ export default class ReactDevOverlay extends React.PureComponent< }, ReactDevOverlayState > { - state = { reactError: null } + state = { isReactError: false } static getDerivedStateFromError(error: Error): ReactDevOverlayState { - if (!error.stack) return { reactError: null } + if (!error.stack) return { isReactError: false } RuntimeErrorHandler.hadRuntimeError = true return { - reactError: { - id: 0, - event: { - type: ACTION_UNHANDLED_ERROR, - reason: error, - frames: parseStack(error.stack), - }, - }, + isReactError: true, } } render() { const { state, children, dispatcher } = this.props - const { reactError } = this.state + const { isReactError } = this.state const hasBuildError = state.buildError != null const hasRuntimeErrors = Boolean(state.errors.length) @@ -52,7 +44,7 @@ export default class ReactDevOverlay extends React.PureComponent< return ( <> - {reactError ? ( + {isReactError ? ( @@ -78,7 +70,9 @@ export default class ReactDevOverlay extends React.PureComponent< {hasRuntimeErrors ? ( Date: Thu, 31 Oct 2024 01:11:26 +0100 Subject: [PATCH 5/8] rename test --- .../app/browser/browser-only.js | 0 .../app/browser/page.js | 0 .../app/foo.js | 0 .../app/layout.tsx | 0 .../app/rsc/page.js | 0 .../app/ssr/page.js | 0 .../next.config.js | 0 .../owner-stack-invalid-element-type.test.ts} | 2 +- 8 files changed, 1 insertion(+), 1 deletion(-) rename test/development/app-dir/{invalid-element-type => owner-stack-invalid-element-type}/app/browser/browser-only.js (100%) rename test/development/app-dir/{invalid-element-type => owner-stack-invalid-element-type}/app/browser/page.js (100%) rename test/development/app-dir/{invalid-element-type => owner-stack-invalid-element-type}/app/foo.js (100%) rename test/development/app-dir/{invalid-element-type => owner-stack-invalid-element-type}/app/layout.tsx (100%) rename test/development/app-dir/{invalid-element-type => owner-stack-invalid-element-type}/app/rsc/page.js (100%) rename test/development/app-dir/{invalid-element-type => owner-stack-invalid-element-type}/app/ssr/page.js (100%) rename test/development/app-dir/{invalid-element-type => owner-stack-invalid-element-type}/next.config.js (100%) rename test/development/app-dir/{invalid-element-type/invalid-element-type.test.ts => owner-stack-invalid-element-type/owner-stack-invalid-element-type.test.ts} (98%) diff --git a/test/development/app-dir/invalid-element-type/app/browser/browser-only.js b/test/development/app-dir/owner-stack-invalid-element-type/app/browser/browser-only.js similarity index 100% rename from test/development/app-dir/invalid-element-type/app/browser/browser-only.js rename to test/development/app-dir/owner-stack-invalid-element-type/app/browser/browser-only.js diff --git a/test/development/app-dir/invalid-element-type/app/browser/page.js b/test/development/app-dir/owner-stack-invalid-element-type/app/browser/page.js similarity index 100% rename from test/development/app-dir/invalid-element-type/app/browser/page.js rename to test/development/app-dir/owner-stack-invalid-element-type/app/browser/page.js diff --git a/test/development/app-dir/invalid-element-type/app/foo.js b/test/development/app-dir/owner-stack-invalid-element-type/app/foo.js similarity index 100% rename from test/development/app-dir/invalid-element-type/app/foo.js rename to test/development/app-dir/owner-stack-invalid-element-type/app/foo.js diff --git a/test/development/app-dir/invalid-element-type/app/layout.tsx b/test/development/app-dir/owner-stack-invalid-element-type/app/layout.tsx similarity index 100% rename from test/development/app-dir/invalid-element-type/app/layout.tsx rename to test/development/app-dir/owner-stack-invalid-element-type/app/layout.tsx diff --git a/test/development/app-dir/invalid-element-type/app/rsc/page.js b/test/development/app-dir/owner-stack-invalid-element-type/app/rsc/page.js similarity index 100% rename from test/development/app-dir/invalid-element-type/app/rsc/page.js rename to test/development/app-dir/owner-stack-invalid-element-type/app/rsc/page.js diff --git a/test/development/app-dir/invalid-element-type/app/ssr/page.js b/test/development/app-dir/owner-stack-invalid-element-type/app/ssr/page.js similarity index 100% rename from test/development/app-dir/invalid-element-type/app/ssr/page.js rename to test/development/app-dir/owner-stack-invalid-element-type/app/ssr/page.js diff --git a/test/development/app-dir/invalid-element-type/next.config.js b/test/development/app-dir/owner-stack-invalid-element-type/next.config.js similarity index 100% rename from test/development/app-dir/invalid-element-type/next.config.js rename to test/development/app-dir/owner-stack-invalid-element-type/next.config.js diff --git a/test/development/app-dir/invalid-element-type/invalid-element-type.test.ts b/test/development/app-dir/owner-stack-invalid-element-type/owner-stack-invalid-element-type.test.ts similarity index 98% rename from test/development/app-dir/invalid-element-type/invalid-element-type.test.ts rename to test/development/app-dir/owner-stack-invalid-element-type/owner-stack-invalid-element-type.test.ts index dfdc08aab00b1..07d5553b547bc 100644 --- a/test/development/app-dir/invalid-element-type/invalid-element-type.test.ts +++ b/test/development/app-dir/owner-stack-invalid-element-type/owner-stack-invalid-element-type.test.ts @@ -28,7 +28,7 @@ async function getStackFramesContent(browser) { return stackFramesContent } -describe('app-dir - invalid-element-type', () => { +describe('app-dir - owner-stack-invalid-element-type', () => { const { next } = nextTestSetup({ files: __dirname, }) From d2e4a80071a3601e952c282b9d51a793e2f15493 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 31 Oct 2024 01:07:42 +0100 Subject: [PATCH 6/8] Append owner stack for captured string console error --- .../globals/intercept-console-error.ts | 2 + .../internal/helpers/console-error.ts | 4 + .../internal/helpers/stitched-error.ts | 16 ++- .../app/layout.tsx | 8 ++ .../app/rsc/page.tsx | 11 ++ .../app/ssr/page.tsx | 13 ++ .../next.config.js | 10 ++ ...owner-stack-react-missing-key-prop.test.ts | 114 ++++++++++++++++++ 8 files changed, 173 insertions(+), 5 deletions(-) create mode 100644 test/development/app-dir/owner-stack-react-missing-key-prop/app/layout.tsx create mode 100644 test/development/app-dir/owner-stack-react-missing-key-prop/app/rsc/page.tsx create mode 100644 test/development/app-dir/owner-stack-react-missing-key-prop/app/ssr/page.tsx create mode 100644 test/development/app-dir/owner-stack-react-missing-key-prop/next.config.js create mode 100644 test/development/app-dir/owner-stack-react-missing-key-prop/owner-stack-react-missing-key-prop.test.ts diff --git a/packages/next/src/client/components/globals/intercept-console-error.ts b/packages/next/src/client/components/globals/intercept-console-error.ts index 5eb5f8c814151..9e4018533e2cc 100644 --- a/packages/next/src/client/components/globals/intercept-console-error.ts +++ b/packages/next/src/client/components/globals/intercept-console-error.ts @@ -1,3 +1,4 @@ +import React from 'react' import isError from '../../../lib/is-error' import { isNextRouterError } from '../is-next-router-error' import { captureStackTrace } from '../react-dev-overlay/internal/helpers/capture-stack-trace' @@ -43,6 +44,7 @@ export function patchConsoleError() { ) } + console.log('args', args, (React as any).captureOwnerStack()) originConsoleError.apply(window.console, args) } } diff --git a/packages/next/src/client/components/react-dev-overlay/internal/helpers/console-error.ts b/packages/next/src/client/components/react-dev-overlay/internal/helpers/console-error.ts index c3f35a8fcecc7..a15a70791f552 100644 --- a/packages/next/src/client/components/react-dev-overlay/internal/helpers/console-error.ts +++ b/packages/next/src/client/components/react-dev-overlay/internal/helpers/console-error.ts @@ -1,9 +1,13 @@ // Represent non Error shape unhandled promise rejections or console.error errors. + +import { appendOwnerStack } from './stitched-error' + // Those errors will be captured and displayed in Error Overlay. type UnhandledError = Error & { digest: 'NEXT_UNHANDLED_ERROR' } export function createUnhandledError(message: string): UnhandledError { const error = new Error(message) as UnhandledError + appendOwnerStack(error) error.digest = 'NEXT_UNHANDLED_ERROR' return error } diff --git a/packages/next/src/client/components/react-dev-overlay/internal/helpers/stitched-error.ts b/packages/next/src/client/components/react-dev-overlay/internal/helpers/stitched-error.ts index 3e40528344e8c..d6c84e083d652 100644 --- a/packages/next/src/client/components/react-dev-overlay/internal/helpers/stitched-error.ts +++ b/packages/next/src/client/components/react-dev-overlay/internal/helpers/stitched-error.ts @@ -27,13 +27,19 @@ export function getReactStitchedError(err: T): Error | T { Object.assign(newError, err) newError.stack = newStack + // Avoid duplicate overriding stack frames + appendOwnerStack(newError) + + return newError +} + +export function appendOwnerStack(error: Error) { + let stack = error.stack || '' // Avoid duplicate overriding stack frames const ownerStack = (React as any).captureOwnerStack() - if (ownerStack && newStack.endsWith(ownerStack) === false) { - newStack += ownerStack + if (ownerStack && stack.endsWith(ownerStack) === false) { + stack += ownerStack // Override stack - newError.stack = newStack + error.stack = stack } - - return newError } diff --git a/test/development/app-dir/owner-stack-react-missing-key-prop/app/layout.tsx b/test/development/app-dir/owner-stack-react-missing-key-prop/app/layout.tsx new file mode 100644 index 0000000000000..888614deda3ba --- /dev/null +++ b/test/development/app-dir/owner-stack-react-missing-key-prop/app/layout.tsx @@ -0,0 +1,8 @@ +import { ReactNode } from 'react' +export default function Root({ children }: { children: ReactNode }) { + return ( + + {children} + + ) +} diff --git a/test/development/app-dir/owner-stack-react-missing-key-prop/app/rsc/page.tsx b/test/development/app-dir/owner-stack-react-missing-key-prop/app/rsc/page.tsx new file mode 100644 index 0000000000000..8b07887166394 --- /dev/null +++ b/test/development/app-dir/owner-stack-react-missing-key-prop/app/rsc/page.tsx @@ -0,0 +1,11 @@ +const list = [1, 2, 3] + +export default function Page() { + return ( +
+ {list.map((item, index) => ( + {item} + ))} +
+ ) +} diff --git a/test/development/app-dir/owner-stack-react-missing-key-prop/app/ssr/page.tsx b/test/development/app-dir/owner-stack-react-missing-key-prop/app/ssr/page.tsx new file mode 100644 index 0000000000000..eea0bc9a463c9 --- /dev/null +++ b/test/development/app-dir/owner-stack-react-missing-key-prop/app/ssr/page.tsx @@ -0,0 +1,13 @@ +'use client' + +const list = [1, 2, 3] + +export default function Page() { + return ( +
+ {list.map((item, index) => ( +

{item}

+ ))} +
+ ) +} diff --git a/test/development/app-dir/owner-stack-react-missing-key-prop/next.config.js b/test/development/app-dir/owner-stack-react-missing-key-prop/next.config.js new file mode 100644 index 0000000000000..d14b1bf8fdc37 --- /dev/null +++ b/test/development/app-dir/owner-stack-react-missing-key-prop/next.config.js @@ -0,0 +1,10 @@ +/** + * @type {import('next').NextConfig} + */ +const nextConfig = { + experimental: { + reactOwnerStack: true, + }, +} + +module.exports = nextConfig diff --git a/test/development/app-dir/owner-stack-react-missing-key-prop/owner-stack-react-missing-key-prop.test.ts b/test/development/app-dir/owner-stack-react-missing-key-prop/owner-stack-react-missing-key-prop.test.ts new file mode 100644 index 0000000000000..423f561b4b65e --- /dev/null +++ b/test/development/app-dir/owner-stack-react-missing-key-prop/owner-stack-react-missing-key-prop.test.ts @@ -0,0 +1,114 @@ +import { nextTestSetup } from 'e2e-utils' +import { getRedboxSource, waitForAndOpenRuntimeError } from 'next-test-utils' + +async function getStackFramesContent(browser) { + const stackFrameElements = await browser.elementsByCss( + '[data-nextjs-call-stack-frame]' + ) + const stackFramesContent = ( + await Promise.all( + stackFrameElements.map(async (frame) => { + const functionNameEl = await frame.$('[data-nextjs-frame-expanded]') + const sourceEl = await frame.$('[data-has-source]') + const functionName = functionNameEl + ? await functionNameEl.innerText() + : '' + const source = sourceEl ? await sourceEl.innerText() : '' + + if (!functionName) { + return '' + } + return `at ${functionName} (${source})` + }) + ) + ) + .filter(Boolean) + .join('\n') + + return stackFramesContent +} + +describe('owner-stack-react-missing-key-prop', () => { + const { next } = nextTestSetup({ + files: __dirname, + }) + + it('should catch invalid element from on rsc component', async () => { + const browser = await next.browser('/rsc') + await waitForAndOpenRuntimeError(browser) + + const stackFramesContent = await getStackFramesContent(browser) + const source = await getRedboxSource(browser) + + if (process.env.TURBOPACK) { + expect(stackFramesContent).toMatchInlineSnapshot( + `"at Page (app/rsc/page.tsx (6:13))"` + ) + expect(source).toMatchInlineSnapshot(` + "app/rsc/page.tsx (7:9) @ + + 5 |
+ 6 | {list.map((item, index) => ( + > 7 | {item} + | ^ + 8 | ))} + 9 |
+ 10 | )" + `) + } else { + expect(stackFramesContent).toMatchInlineSnapshot( + `"at map (app/rsc/page.tsx (6:13))"` + ) + expect(source).toMatchInlineSnapshot(` + "app/rsc/page.tsx (7:10) @ span + + 5 |
+ 6 | {list.map((item, index) => ( + > 7 | {item} + | ^ + 8 | ))} + 9 |
+ 10 | )" + `) + } + }) + + it('should catch invalid element from on ssr client component', async () => { + const browser = await next.browser('/ssr') + await waitForAndOpenRuntimeError(browser) + + const stackFramesContent = await getStackFramesContent(browser) + const source = await getRedboxSource(browser) + if (process.env.TURBOPACK) { + expect(stackFramesContent).toMatchInlineSnapshot( + `"at Page (app/ssr/page.tsx (8:13))"` + ) + expect(source).toMatchInlineSnapshot(` + "app/ssr/page.tsx (9:9) @ + + 7 |
+ 8 | {list.map((item, index) => ( + > 9 |

{item}

+ | ^ + 10 | ))} + 11 |
+ 12 | )" + `) + } else { + expect(stackFramesContent).toMatchInlineSnapshot( + `"at map (app/ssr/page.tsx (8:13))"` + ) + expect(source).toMatchInlineSnapshot(` + "app/ssr/page.tsx (9:10) @ p + + 7 |
+ 8 | {list.map((item, index) => ( + > 9 |

{item}

+ | ^ + 10 | ))} + 11 |
+ 12 | )" + `) + } + }) +}) From 0321a80f81cd0f991c3c1fe7d899fefff0227c0d Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 31 Oct 2024 16:51:25 +0100 Subject: [PATCH 7/8] rm console.log --- .../src/client/components/globals/intercept-console-error.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/next/src/client/components/globals/intercept-console-error.ts b/packages/next/src/client/components/globals/intercept-console-error.ts index 9e4018533e2cc..5eb5f8c814151 100644 --- a/packages/next/src/client/components/globals/intercept-console-error.ts +++ b/packages/next/src/client/components/globals/intercept-console-error.ts @@ -1,4 +1,3 @@ -import React from 'react' import isError from '../../../lib/is-error' import { isNextRouterError } from '../is-next-router-error' import { captureStackTrace } from '../react-dev-overlay/internal/helpers/capture-stack-trace' @@ -44,7 +43,6 @@ export function patchConsoleError() { ) } - console.log('args', args, (React as any).captureOwnerStack()) originConsoleError.apply(window.console, args) } } From 6510d983ca4c445a7fc4341128367ac4adff2fd3 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 31 Oct 2024 01:14:11 +0100 Subject: [PATCH 8/8] test: share stack frame util --- .../owner-stack-invalid-element-type.test.ts | 33 +++---------------- ...owner-stack-react-missing-key-prop.test.ts | 33 +++---------------- .../app-dir/owner-stack/owner-stack.test.ts | 28 +--------------- test/lib/next-test-utils.ts | 29 ++++++++++++++++ 4 files changed, 40 insertions(+), 83 deletions(-) diff --git a/test/development/app-dir/owner-stack-invalid-element-type/owner-stack-invalid-element-type.test.ts b/test/development/app-dir/owner-stack-invalid-element-type/owner-stack-invalid-element-type.test.ts index 07d5553b547bc..6a97c1b257f0d 100644 --- a/test/development/app-dir/owner-stack-invalid-element-type/owner-stack-invalid-element-type.test.ts +++ b/test/development/app-dir/owner-stack-invalid-element-type/owner-stack-invalid-element-type.test.ts @@ -1,32 +1,9 @@ import { nextTestSetup } from 'e2e-utils' -import { assertHasRedbox, getRedboxSource } from 'next-test-utils' - -async function getStackFramesContent(browser) { - const stackFrameElements = await browser.elementsByCss( - '[data-nextjs-call-stack-frame]' - ) - const stackFramesContent = ( - await Promise.all( - stackFrameElements.map(async (frame) => { - const functionNameEl = await frame.$('[data-nextjs-frame-expanded]') - const sourceEl = await frame.$('[data-has-source]') - const functionName = functionNameEl - ? await functionNameEl.innerText() - : '' - const source = sourceEl ? await sourceEl.innerText() : '' - - if (!functionName) { - return '' - } - return `at ${functionName} (${source})` - }) - ) - ) - .filter(Boolean) - .join('\n') - - return stackFramesContent -} +import { + assertHasRedbox, + getRedboxSource, + getStackFramesContent, +} from 'next-test-utils' describe('app-dir - owner-stack-invalid-element-type', () => { const { next } = nextTestSetup({ diff --git a/test/development/app-dir/owner-stack-react-missing-key-prop/owner-stack-react-missing-key-prop.test.ts b/test/development/app-dir/owner-stack-react-missing-key-prop/owner-stack-react-missing-key-prop.test.ts index 423f561b4b65e..2ba38962cbf38 100644 --- a/test/development/app-dir/owner-stack-react-missing-key-prop/owner-stack-react-missing-key-prop.test.ts +++ b/test/development/app-dir/owner-stack-react-missing-key-prop/owner-stack-react-missing-key-prop.test.ts @@ -1,32 +1,9 @@ import { nextTestSetup } from 'e2e-utils' -import { getRedboxSource, waitForAndOpenRuntimeError } from 'next-test-utils' - -async function getStackFramesContent(browser) { - const stackFrameElements = await browser.elementsByCss( - '[data-nextjs-call-stack-frame]' - ) - const stackFramesContent = ( - await Promise.all( - stackFrameElements.map(async (frame) => { - const functionNameEl = await frame.$('[data-nextjs-frame-expanded]') - const sourceEl = await frame.$('[data-has-source]') - const functionName = functionNameEl - ? await functionNameEl.innerText() - : '' - const source = sourceEl ? await sourceEl.innerText() : '' - - if (!functionName) { - return '' - } - return `at ${functionName} (${source})` - }) - ) - ) - .filter(Boolean) - .join('\n') - - return stackFramesContent -} +import { + getRedboxSource, + waitForAndOpenRuntimeError, + getStackFramesContent, +} from 'next-test-utils' describe('owner-stack-react-missing-key-prop', () => { const { next } = nextTestSetup({ diff --git a/test/development/app-dir/owner-stack/owner-stack.test.ts b/test/development/app-dir/owner-stack/owner-stack.test.ts index 1257740989d9d..db1b383339ff6 100644 --- a/test/development/app-dir/owner-stack/owner-stack.test.ts +++ b/test/development/app-dir/owner-stack/owner-stack.test.ts @@ -4,6 +4,7 @@ import { assertNoRedbox, waitForAndOpenRuntimeError, getRedboxDescription, + getStackFramesContent, } from 'next-test-utils' // TODO: parse the location and assert them in the future @@ -17,33 +18,6 @@ function normalizeStackTrace(trace: string) { .trim() } -async function getStackFramesContent(browser) { - const stackFrameElements = await browser.elementsByCss( - '[data-nextjs-call-stack-frame]' - ) - const stackFramesContent = ( - await Promise.all( - stackFrameElements.map(async (frame) => { - const functionNameEl = await frame.$('[data-nextjs-frame-expanded]') - const sourceEl = await frame.$('[data-has-source]') - const functionName = functionNameEl - ? await functionNameEl.innerText() - : '' - const source = sourceEl ? await sourceEl.innerText() : '' - - if (!functionName) { - return '' - } - return `at ${functionName} (${source})` - }) - ) - ) - .filter(Boolean) - .join('\n') - - return stackFramesContent -} - describe('app-dir - owner-stack', () => { const { next } = nextTestSetup({ files: __dirname, diff --git a/test/lib/next-test-utils.ts b/test/lib/next-test-utils.ts index 0304d2464cbea..81977645a77c8 100644 --- a/test/lib/next-test-utils.ts +++ b/test/lib/next-test-utils.ts @@ -1492,3 +1492,32 @@ export const checkLink = ( rel: string, content: string | string[] ) => checkMeta(browser, rel, content, 'rel', 'link', 'href') + +export async function getStackFramesContent( + browser: BrowserInterface +): Promise { + const stackFrameElements = await browser.elementsByCss( + '[data-nextjs-call-stack-frame]' + ) + const stackFramesContent = ( + await Promise.all( + stackFrameElements.map(async (frame) => { + const functionNameEl = await frame.$('[data-nextjs-frame-expanded]') + const sourceEl = await frame.$('[data-has-source]') + const functionName = functionNameEl + ? await functionNameEl.innerText() + : '' + const source = sourceEl ? await sourceEl.innerText() : '' + + if (!functionName) { + return '' + } + return `at ${functionName} (${source})` + }) + ) + ) + .filter(Boolean) + .join('\n') + + return stackFramesContent +}