diff --git a/packages/next/client/components/react-dev-overlay/internal/helpers/stack-frame.ts b/packages/next/client/components/react-dev-overlay/internal/helpers/stack-frame.ts index a8349ba16b49..b26cc1642c1c 100644 --- a/packages/next/client/components/react-dev-overlay/internal/helpers/stack-frame.ts +++ b/packages/next/client/components/react-dev-overlay/internal/helpers/stack-frame.ts @@ -39,6 +39,7 @@ export function getOriginalStackFrame( const params = new URLSearchParams() params.append('isServer', String(type === 'server')) params.append('isEdgeServer', String(type === 'edge-server')) + params.append('isAppDirectory', 'true') params.append('errorMessage', errorMessage) for (const key in source) { params.append(key, ((source as any)[key] ?? '').toString()) diff --git a/packages/next/server/dev/next-dev-server.ts b/packages/next/server/dev/next-dev-server.ts index 9279bdf6bc4e..c4dae0ce6361 100644 --- a/packages/next/server/dev/next-dev-server.ts +++ b/packages/next/server/dev/next-dev-server.ts @@ -1053,7 +1053,8 @@ export default class DevServer extends Server { ({ file }) => !file?.startsWith('eval') && !file?.includes('web/adapter') && - !file?.includes('sandbox/context') + !file?.includes('sandbox/context') && + !file?.includes('') )! if (frame.lineNumber && frame?.file) { @@ -1061,10 +1062,15 @@ export default class DevServer extends Server { /^(webpack-internal:\/\/\/|file:\/\/)/, '' ) + const modulePath = frame.file.replace( + /^(webpack-internal:\/\/\/|file:\/\/)(\(.*\)\/)?/, + '' + ) const src = getErrorSource(err as Error) + const isEdgeCompiler = src === COMPILER_NAMES.edgeServer const compilation = ( - src === COMPILER_NAMES.edgeServer + isEdgeCompiler ? this.hotReloader?.edgeServerStats?.compilation : this.hotReloader?.serverStats?.compilation )! @@ -1080,10 +1086,16 @@ export default class DevServer extends Server { column: frame.column, source, frame, - modulePath: moduleId, + moduleId, + modulePath, rootDirectory: this.dir, errorMessage: err.message, - compilation, + serverCompilation: isEdgeCompiler + ? undefined + : this.hotReloader?.serverStats?.compilation, + edgeCompilation: isEdgeCompiler + ? this.hotReloader?.edgeServerStats?.compilation + : undefined, }) if (originalFrame) { @@ -1093,7 +1105,7 @@ export default class DevServer extends Server { Log[type === 'warning' ? 'warn' : 'error']( `${file} (${lineNumber}:${column}) @ ${methodName}` ) - if (src === COMPILER_NAMES.edgeServer) { + if (isEdgeCompiler) { err = err.message } if (type === 'warning') { diff --git a/packages/next/taskfile.js b/packages/next/taskfile.js index aa662f9432a9..7f9e92ae0473 100644 --- a/packages/next/taskfile.js +++ b/packages/next/taskfile.js @@ -389,7 +389,7 @@ export async function ncc_next__react_dev_overlay(task, opts) { precompiled: false, packageName: '@next/react-dev-overlay', externals: overlayExternals, - target: 'es5', + target: 'es2018', }) .target('dist/compiled/@next/react-dev-overlay/dist') diff --git a/packages/react-dev-overlay/src/middleware.ts b/packages/react-dev-overlay/src/middleware.ts index 9320a2fa8480..1fbd3508a0a5 100644 --- a/packages/react-dev-overlay/src/middleware.ts +++ b/packages/react-dev-overlay/src/middleware.ts @@ -41,9 +41,10 @@ function getModuleById( id: string | undefined, compilation: webpack.Compilation ) { - return [...compilation.modules].find( - (searchModule) => getModuleId(compilation, searchModule) === id - ) + return [...compilation.modules].find((searchModule) => { + const moduleId = getModuleId(compilation, searchModule) + return moduleId === id + }) } function findModuleNotFoundFromError(errorMessage: string | undefined) { @@ -112,11 +113,11 @@ async function findOriginalSourcePositionAndContent( } function findOriginalSourcePositionAndContentFromCompilation( - modulePath: string | undefined, + moduleId: string | undefined, importedModule: string, compilation: webpack.Compilation ) { - const module = getModuleById(modulePath, compilation) + const module = getModuleById(moduleId, compilation) return module?.buildInfo?.importLocByPath?.get(importedModule) ?? null } @@ -124,33 +125,66 @@ export async function createOriginalStackFrame({ line, column, source, + moduleId, modulePath, rootDirectory, frame, errorMessage, - compilation, + clientCompilation, + serverCompilation, + edgeCompilation, }: { line: number column: number | null source: any + moduleId?: string modulePath?: string rootDirectory: string frame: any errorMessage?: string - compilation?: webpack.Compilation + clientCompilation?: webpack.Compilation + serverCompilation?: webpack.Compilation + edgeCompilation?: webpack.Compilation }): Promise { const moduleNotFound = findModuleNotFoundFromError(errorMessage) - const result = - moduleNotFound && compilation - ? findOriginalSourcePositionAndContentFromCompilation( - modulePath, - moduleNotFound, - compilation - ) - : await findOriginalSourcePositionAndContent(source, { - line, - column, - }) + const result = await (async () => { + if (moduleNotFound) { + let moduleNotFoundResult = null + + if (clientCompilation) { + moduleNotFoundResult = + findOriginalSourcePositionAndContentFromCompilation( + moduleId, + moduleNotFound, + clientCompilation + ) + } + + if (moduleNotFoundResult === null && serverCompilation) { + moduleNotFoundResult = + findOriginalSourcePositionAndContentFromCompilation( + moduleId, + moduleNotFound, + serverCompilation + ) + } + + if (moduleNotFoundResult === null && edgeCompilation) { + moduleNotFoundResult = + findOriginalSourcePositionAndContentFromCompilation( + moduleId, + moduleNotFound, + edgeCompilation + ) + } + + return moduleNotFoundResult + } + return await findOriginalSourcePositionAndContent(source, { + line, + column, + }) + })() if (result === null) { return null @@ -164,7 +198,12 @@ export async function createOriginalStackFrame({ const filePath = path.resolve( rootDirectory, - modulePath || getSourcePath(sourcePosition.source) + getSourcePath( + // When sourcePosition.source is the loader path the modulePath is generally better. + (sourcePosition.source.includes('|') + ? modulePath + : sourcePosition.source) || modulePath + ) ) const originalFrame: StackFrame = { @@ -173,7 +212,11 @@ export async function createOriginalStackFrame({ : sourcePosition.source, lineNumber: sourcePosition.line, column: sourcePosition.column, - methodName: frame.methodName, // TODO: resolve original method name (?) + methodName: + sourcePosition.name || + // default is not a valid identifier in JS so webpack uses a custom variable when it's an unnamed default export + // Resolve it back to `default` for the method name if the source position didn't have the method. + frame.methodName?.replace('__WEBPACK_DEFAULT_EXPORT__', 'default'), arguments: [], } @@ -250,8 +293,14 @@ function getOverlayMiddleware(options: OverlayMiddlewareOptions) { const frame = query as unknown as StackFrame & { isEdgeServer: 'true' | 'false' isServer: 'true' | 'false' + isAppDirectory: 'true' | 'false' errorMessage: string | undefined } + const isAppDirectory = frame.isAppDirectory === 'true' + const isServerError = frame.isServer === 'true' + const isEdgeServerError = frame.isEdgeServer === 'true' + const isClientError = !isServerError && !isEdgeServerError + if ( !( (frame.file?.startsWith('webpack-internal:///') || @@ -268,20 +317,45 @@ function getOverlayMiddleware(options: OverlayMiddlewareOptions) { /^(webpack-internal:\/\/\/|file:\/\/)/, '' ) + const modulePath = frame.file.replace( + /^(webpack-internal:\/\/\/|file:\/\/)(\(.*\)\/)?/, + '' + ) - let source: Source - const compilation = - frame.isEdgeServer === 'true' - ? options.edgeServerStats()?.compilation - : frame.isServer === 'true' - ? options.serverStats()?.compilation - : options.stats()?.compilation + let source: Source = null + const clientCompilation = options.stats()?.compilation + const serverCompilation = options.serverStats()?.compilation + const edgeCompilation = options.edgeServerStats()?.compilation try { - source = await getSourceById( - frame.file.startsWith('file:'), - moduleId, - compilation - ) + if (isClientError || isAppDirectory) { + // Try Client Compilation first + // In `pages` we leverage `isClientError` to check + // In `app` it depends on if it's a server / client component and when the code throws. E.g. during HTML rendering it's the server/edge compilation. + source = await getSourceById( + frame.file.startsWith('file:'), + moduleId, + clientCompilation + ) + } + // Try Server Compilation + // In `pages` this could be something imported in getServerSideProps/getStaticProps as the code for those is tree-shaken. + // In `app` this finds server components and code that was imported from a server component. It also covers when client component code throws during HTML rendering. + if ((isServerError || isAppDirectory) && source === null) { + source = await getSourceById( + frame.file.startsWith('file:'), + moduleId, + serverCompilation + ) + } + // Try Edge Server Compilation + // Both cases are the same as Server Compilation, main difference is that it covers `runtime: 'edge'` pages/app routes. + if ((isEdgeServerError || isAppDirectory) && source === null) { + source = await getSourceById( + frame.file.startsWith('file:'), + moduleId, + edgeCompilation + ) + } } catch (err) { console.log('Failed to get source map:', err) res.statusCode = 500 @@ -310,10 +384,13 @@ function getOverlayMiddleware(options: OverlayMiddlewareOptions) { column: frameColumn, source, frame, - modulePath: moduleId, + moduleId, + modulePath, rootDirectory: options.rootDirectory, errorMessage: frame.errorMessage, - compilation, + clientCompilation: isClientError ? clientCompilation : undefined, + serverCompilation: isServerError ? serverCompilation : undefined, + edgeCompilation: isEdgeServerError ? edgeCompilation : undefined, }) if (originalStackFrameResponse === null) { diff --git a/packages/react-dev-overlay/tsconfig.json b/packages/react-dev-overlay/tsconfig.json index 7bb540dee0f1..551d3124364c 100644 --- a/packages/react-dev-overlay/tsconfig.json +++ b/packages/react-dev-overlay/tsconfig.json @@ -4,14 +4,15 @@ "sourceMap": true, "strict": true, "esModuleInterop": true, - "target": "es3", + "target": "es2020", "lib": ["dom"], "downlevelIteration": true, "preserveWatchOutput": true, "outDir": "dist", "jsx": "react", "noFallthroughCasesInSwitch": true, - "skipLibCheck": true + "skipLibCheck": true, + "moduleResolution": "Node16" }, "include": ["src/**/*.ts", "src/**/*.tsx"], "exclude": ["node_modules"] diff --git a/test/development/acceptance-app/ReactRefreshRegression.test.ts b/test/development/acceptance-app/ReactRefreshRegression.test.ts index c296c66b24cf..88c19bf0cd53 100644 --- a/test/development/acceptance-app/ReactRefreshRegression.test.ts +++ b/test/development/acceptance-app/ReactRefreshRegression.test.ts @@ -270,19 +270,15 @@ describe('ReactRefreshRegression app', () => { // https://github.com/vercel/next.js/issues/11504 // TODO-APP: fix case where error is not resolved to source correctly. - test.skip('shows an overlay for a server-side error', async () => { - const { session, cleanup } = await sandbox(next) + test('shows an overlay for anonymous function server-side error', async () => { + const { session, browser, cleanup } = await sandbox(next) await session.patch( - 'app/page.js', - `export default function () { throw new Error('pre boom'); }` - ) - - const didNotReload = await session.patch( 'app/page.js', `export default function () { throw new Error('boom'); }` ) - expect(didNotReload).toBe(false) + + await browser.refresh() expect(await session.hasRedbox(true)).toBe(true) @@ -295,6 +291,50 @@ describe('ReactRefreshRegression app', () => { await cleanup() }) + test('shows an overlay for server-side error in server component', async () => { + const { session, browser, cleanup } = await sandbox(next) + + await session.patch( + 'app/page.js', + `export default function Page() { throw new Error('boom'); }` + ) + + await browser.refresh() + + expect(await session.hasRedbox(true)).toBe(true) + + const source = await session.getRedboxSource() + expect(source.split(/\r?\n/g).slice(2).join('\n')).toMatchInlineSnapshot(` + "> 1 | export default function Page() { throw new Error('boom'); } + | ^" + `) + + await cleanup() + }) + + test('shows an overlay for server-side error in client component', async () => { + const { session, browser, cleanup } = await sandbox(next) + + await session.patch( + 'app/page.js', + `'use client' + export default function Page() { throw new Error('boom'); }` + ) + + await browser.refresh() + + expect(await session.hasRedbox(true)).toBe(true) + + const source = await session.getRedboxSource() + expect(source.split(/\r?\n/g).slice(2).join('\n')).toMatchInlineSnapshot(` + " 1 | 'use client' + > 2 | export default function Page() { throw new Error('boom'); } + | ^" + `) + + await cleanup() + }) + // https://github.com/vercel/next.js/issues/13574 test('custom loader mdx should have Fast Refresh enabled', async () => { const files = new Map() diff --git a/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap b/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap index 488a0c937654..031f532d4b6d 100644 --- a/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap +++ b/test/development/acceptance-app/__snapshots__/ReactRefreshLogBox.test.ts.snap @@ -115,7 +115,7 @@ exports[`ReactRefreshLogBox app stuck error 1`] = ` `; exports[`ReactRefreshLogBox app syntax > runtime error 1`] = ` -"index.js (6:16) @ eval +"index.js (6:16) @ Error 4 | setInterval(() => { 5 | i++ diff --git a/test/development/acceptance-app/helpers.ts b/test/development/acceptance-app/helpers.ts index 0802671de7ea..f0787fb84b76 100644 --- a/test/development/acceptance-app/helpers.ts +++ b/test/development/acceptance-app/helpers.ts @@ -22,6 +22,7 @@ export async function sandbox( await next.start() const browser = await webdriver(next.appPort, '/') return { + browser, session: { async write(filename, content) { // Update the file on filesystem diff --git a/test/development/acceptance/ReactRefreshLogBox.test.ts b/test/development/acceptance/ReactRefreshLogBox.test.ts index 45ed9991c360..d9aced58dba6 100644 --- a/test/development/acceptance/ReactRefreshLogBox.test.ts +++ b/test/development/acceptance/ReactRefreshLogBox.test.ts @@ -358,11 +358,7 @@ describe('ReactRefreshLogBox', () => { ) expect(await session.hasRedbox(true)).toBe(true) - if (process.platform === 'win32') { - expect(await session.getRedboxSource()).toMatchSnapshot() - } else { - expect(await session.getRedboxSource()).toMatchSnapshot() - } + expect(await session.getRedboxSource()).toMatchSnapshot() await cleanup() }) diff --git a/test/development/acceptance/__snapshots__/ReactRefreshLogBox.test.ts.snap b/test/development/acceptance/__snapshots__/ReactRefreshLogBox.test.ts.snap index cbcc7f18ef1d..6f59ee031abe 100644 --- a/test/development/acceptance/__snapshots__/ReactRefreshLogBox.test.ts.snap +++ b/test/development/acceptance/__snapshots__/ReactRefreshLogBox.test.ts.snap @@ -127,7 +127,7 @@ exports[`ReactRefreshLogBox stuck error 1`] = ` `; exports[`ReactRefreshLogBox syntax > runtime error 1`] = ` -"index.js (6:16) @ eval +"index.js (6:16) @ Error 4 | setInterval(() => { 5 | i++ diff --git a/test/development/api-route-errors/index.test.ts b/test/development/api-route-errors/index.test.ts index 09950e59d885..b086f8b10dca 100644 --- a/test/development/api-route-errors/index.test.ts +++ b/test/development/api-route-errors/index.test.ts @@ -24,7 +24,7 @@ describe('api-route-errors cli output', () => { const output = stripAnsi(next.cliOutput.slice(outputIndex)) // Location - expect(output).toContain('error - (api)/pages/api/error.js (2:8) @ error') + expect(output).toContain('error - pages/api/error.js (2:8) @ error') // Stack expect(output).toContain('pages/api/error.js:6:11') // Source code @@ -39,7 +39,7 @@ describe('api-route-errors cli output', () => { const output = stripAnsi(next.cliOutput.slice(outputIndex)) // Location expect(output).toContain( - 'error - (api)/pages/api/uncaught-exception.js (3:10) @ Timeout' + 'error - pages/api/uncaught-exception.js (3:10) @ Timeout' ) // Stack expect(output).toContain('pages/api/uncaught-exception.js:7:15') @@ -57,7 +57,7 @@ describe('api-route-errors cli output', () => { const output = stripAnsi(next.cliOutput.slice(outputIndex)) // Location expect(output).toContain( - 'error - (api)/pages/api/unhandled-rejection.js (2:17) @ unhandledRejection' + 'error - pages/api/unhandled-rejection.js (2:17) @ unhandledRejection' ) // Stack expect(output).toContain('pages/api/unhandled-rejection.js:6:20') diff --git a/test/development/basic-basepath/hmr.test.ts b/test/development/basic-basepath/hmr.test.ts index f4cba53e3e09..8f80697a64d6 100644 --- a/test/development/basic-basepath/hmr.test.ts +++ b/test/development/basic-basepath/hmr.test.ts @@ -536,12 +536,7 @@ describe('basic HMR', () => { expect(await hasRedbox(browser)).toBe(true) // TODO: Replace this when webpack 5 is the default - expect( - (await getRedboxHeader(browser)).replace( - '__WEBPACK_DEFAULT_EXPORT__', - 'Unknown' - ) - ).toMatch( + expect(await getRedboxHeader(browser)).toMatch( `Objects are not valid as a React child (found: ${ isReact17 ? '/search/' : '[object RegExp]' }). If you meant to render a collection of children, use an array instead.` diff --git a/test/development/basic/hmr.test.ts b/test/development/basic/hmr.test.ts index 75fea4552e34..4bfd3ab66c1c 100644 --- a/test/development/basic/hmr.test.ts +++ b/test/development/basic/hmr.test.ts @@ -601,12 +601,7 @@ describe('basic HMR', () => { expect(await hasRedbox(browser)).toBe(true) // TODO: Replace this when webpack 5 is the default - expect( - (await getRedboxHeader(browser)).replace( - '__WEBPACK_DEFAULT_EXPORT__', - 'Unknown' - ) - ).toMatch( + expect(await getRedboxHeader(browser)).toMatch( `Objects are not valid as a React child (found: ${ isReact17 ? '/search/' : '[object RegExp]' }). If you meant to render a collection of children, use an array instead.` diff --git a/test/integration/middleware-dev-errors/test/index.test.js b/test/integration/middleware-dev-errors/test/index.test.js index 1d8f32ee27b0..ff02a97e37f1 100644 --- a/test/integration/middleware-dev-errors/test/index.test.js +++ b/test/integration/middleware-dev-errors/test/index.test.js @@ -58,7 +58,7 @@ describe('Middleware development errors', () => { const output = stripAnsi(context.logs.output) expect(output).toMatch( new RegExp( - `error - \\(middleware\\)/middleware.js \\(\\d+:\\d+\\) @ Object.__WEBPACK_DEFAULT_EXPORT__ \\[as handler\\]\nerror - boom`, + `error - middleware.js \\(\\d+:\\d+\\) @ Object.default \\[as handler\\]\nerror - boom`, 'm' ) ) @@ -93,7 +93,7 @@ describe('Middleware development errors', () => { const output = stripAnsi(context.logs.output) expect(output).toMatch( new RegExp( - `error - \\(middleware\\)/middleware.js \\(\\d+:\\d+\\) @ throwError\nerror - unhandledRejection: async boom!`, + `error - middleware.js \\(\\d+:\\d+\\) @ throwError\nerror - unhandledRejection: async boom!`, 'm' ) ) @@ -124,7 +124,7 @@ describe('Middleware development errors', () => { const output = stripAnsi(context.logs.output) expect(output).toMatch( new RegExp( - `error - \\(middleware\\)/middleware.js \\(\\d+:\\d+\\) @ eval\nerror - test is not defined`, + `error - middleware.js \\(\\d+:\\d+\\) @ eval\nerror - test is not defined`, 'm' ) ) @@ -157,7 +157,7 @@ describe('Middleware development errors', () => { const output = stripAnsi(context.logs.output) expect(output).toMatch( new RegExp( - `error - \\(middleware\\)/middleware.js \\(\\d+:\\d+\\) @ \nerror - booooom!`, + `error - middleware.js \\(\\d+:\\d+\\) @ \nerror - booooom!`, 'm' ) ) @@ -195,7 +195,7 @@ describe('Middleware development errors', () => { const output = stripAnsi(context.logs.output) expect(output).toMatch( new RegExp( - `error - \\(middleware\\)/middleware.js \\(\\d+:\\d+\\) @ eval\nerror - unhandledRejection: you shall see me`, + `error - middleware.js \\(\\d+:\\d+\\) @ eval\nerror - unhandledRejection: you shall see me`, 'm' ) ) @@ -227,7 +227,7 @@ describe('Middleware development errors', () => { const output = stripAnsi(context.logs.output) expect(output).toMatch( new RegExp( - `error - \\(middleware\\)/lib/unhandled.js \\(\\d+:\\d+\\) @ Timeout.eval \\[as _onTimeout\\]\nerror - uncaughtException: This file asynchronously fails while loading`, + `error - lib/unhandled.js \\(\\d+:\\d+\\) @ Timeout.eval \\[as _onTimeout\\]\nerror - uncaughtException: This file asynchronously fails while loading`, 'm' ) ) diff --git a/test/lib/next-test-utils.js b/test/lib/next-test-utils.js index 79ac065b5b9e..ec4592e1287c 100644 --- a/test/lib/next-test-utils.js +++ b/test/lib/next-test-utils.js @@ -649,13 +649,13 @@ export async function getRedboxHeader(browser) { evaluate(browser, () => { const portal = [].slice .call(document.querySelectorAll('nextjs-portal')) - .find((p) => p.shadowRoot.querySelector('[data-nextjs-dialog-header')) + .find((p) => + p.shadowRoot.querySelector('[data-nextjs-dialog-header]') + ) const root = portal.shadowRoot - return root - .querySelector('[data-nextjs-dialog-header]') - .innerText.replace(/__WEBPACK_DEFAULT_EXPORT__/, 'Unknown') + return root.querySelector('[data-nextjs-dialog-header]').innerText }), - 3000, + 10000, 500, 'getRedboxHeader' ) @@ -673,11 +673,11 @@ export async function getRedboxSource(browser) { ) ) const root = portal.shadowRoot - return root - .querySelector('[data-nextjs-codeframe], [data-nextjs-terminal]') - .innerText.replace(/__WEBPACK_DEFAULT_EXPORT__/, 'Unknown') + return root.querySelector( + '[data-nextjs-codeframe], [data-nextjs-terminal]' + ).innerText }), - 3000, + 10000, 500, 'getRedboxSource' ) @@ -693,9 +693,7 @@ export async function getRedboxDescription(browser) { p.shadowRoot.querySelector('[data-nextjs-dialog-header]') ) const root = portal.shadowRoot - return root - .querySelector('#nextjs__container_errors_desc') - .innerText.replace(/__WEBPACK_DEFAULT_EXPORT__/, 'Unknown') + return root.querySelector('#nextjs__container_errors_desc').innerText }), 3000, 500,