From 3f4899f7230d1fb9e126e816e87d0a6e7b46ea2e Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Thu, 21 Nov 2024 16:34:59 +0100 Subject: [PATCH] Pass down ignored field to error overlay (#72925) --- .../src/build/webpack/config/ignore-list.ts | 7 + .../parseNotFoundError.ts | 6 +- .../container/RuntimeError/CallStackFrame.tsx | 2 +- .../internal/container/RuntimeError/index.tsx | 21 +-- .../internal/helpers/stack-frame.ts | 15 +- .../server/middleware-turbopack.ts | 31 ++-- .../react-dev-overlay/server/middleware.ts | 147 ++++++++++++++---- .../react-dev-overlay/server/shared.ts | 2 +- .../lib/router-utils/setup-dev-bundler.ts | 2 + 9 files changed, 164 insertions(+), 69 deletions(-) create mode 100644 packages/next/src/build/webpack/config/ignore-list.ts diff --git a/packages/next/src/build/webpack/config/ignore-list.ts b/packages/next/src/build/webpack/config/ignore-list.ts new file mode 100644 index 0000000000000..266eccf10c2c6 --- /dev/null +++ b/packages/next/src/build/webpack/config/ignore-list.ts @@ -0,0 +1,7 @@ +export function shouldIgnorePath(modulePath: string): boolean { + return ( + modulePath.includes('node_modules') || + // Only relevant for when Next.js is symlinked e.g. in the Next.js monorepo + modulePath.includes('next/dist') + ) +} diff --git a/packages/next/src/build/webpack/plugins/wellknown-errors-plugin/parseNotFoundError.ts b/packages/next/src/build/webpack/plugins/wellknown-errors-plugin/parseNotFoundError.ts index a8c991fd09465..76a65f621aa7a 100644 --- a/packages/next/src/build/webpack/plugins/wellknown-errors-plugin/parseNotFoundError.ts +++ b/packages/next/src/build/webpack/plugins/wellknown-errors-plugin/parseNotFoundError.ts @@ -1,6 +1,9 @@ import { bold, cyan, green, red, yellow } from '../../../../lib/picocolors' import { SimpleWebpackError } from './simpleWebpackError' -import { createOriginalStackFrame } from '../../../../client/components/react-dev-overlay/server/middleware' +import { + createOriginalStackFrame, + getIgnoredSources, +} from '../../../../client/components/react-dev-overlay/server/middleware' import type { webpack } from 'next/dist/compiled/webpack/webpack' // Based on https://github.com/webpack/webpack/blob/fcdd04a833943394bbb0a9eeb54a962a24cc7e41/lib/stats/DefaultStatsFactoryPlugin.js#L422-L431 @@ -62,6 +65,7 @@ async function getSourceFrame( source: { type: 'bundle', sourceMap, + ignoredSources: getIgnoredSources(sourceMap), compilation, moduleId, modulePath: fileName, diff --git a/packages/next/src/client/components/react-dev-overlay/internal/container/RuntimeError/CallStackFrame.tsx b/packages/next/src/client/components/react-dev-overlay/internal/container/RuntimeError/CallStackFrame.tsx index 7a3f4e5c09075..d6afb06e61784 100644 --- a/packages/next/src/client/components/react-dev-overlay/internal/container/RuntimeError/CallStackFrame.tsx +++ b/packages/next/src/client/components/react-dev-overlay/internal/container/RuntimeError/CallStackFrame.tsx @@ -37,7 +37,7 @@ export const CallStackFrame: React.FC<{ return (
-

+

{ - const filteredFrames = error.frames - // Filter out nodejs internal frames since you can't do anything about them. - // e.g. node:internal/timers shows up pretty often due to timers, but not helpful to users. - // Only present the last line before nodejs internal trace. - .filter((f) => !f.sourceStackFrame.file?.startsWith('node:')) - - const firstFirstPartyFrameIndex = filteredFrames.findIndex( + const firstFirstPartyFrameIndex = frames.findIndex( (entry) => - entry.expanded && + !entry.ignored && Boolean(entry.originalCodeFrame) && Boolean(entry.originalStackFrame) ) return { - firstFrame: filteredFrames[firstFirstPartyFrameIndex] ?? null, + firstFrame: frames[firstFirstPartyFrameIndex] ?? null, allLeadingFrames: firstFirstPartyFrameIndex < 0 ? [] - : filteredFrames.slice(0, firstFirstPartyFrameIndex), - allCallStackFrames: filteredFrames.slice(firstFirstPartyFrameIndex + 1), + : frames.slice(0, firstFirstPartyFrameIndex), + allCallStackFrames: frames.slice(firstFirstPartyFrameIndex + 1), } - }, [error.frames]) + }, [frames]) const { leadingFramesGroupedByFramework, stackFramesGroupedByFramework } = React.useMemo(() => { - const leadingFrames = allLeadingFrames.filter((f) => f.expanded) + const leadingFrames = allLeadingFrames.filter((f) => !f.ignored) return { stackFramesGroupedByFramework: diff --git a/packages/next/src/client/components/react-dev-overlay/internal/helpers/stack-frame.ts b/packages/next/src/client/components/react-dev-overlay/internal/helpers/stack-frame.ts index 43254ee296012..3c45d792c16a9 100644 --- a/packages/next/src/client/components/react-dev-overlay/internal/helpers/stack-frame.ts +++ b/packages/next/src/client/components/react-dev-overlay/internal/helpers/stack-frame.ts @@ -8,7 +8,7 @@ export interface OriginalStackFrame extends OriginalStackFrameResponse { error: boolean reason: string | null external: boolean - expanded: boolean + ignored: boolean sourceStackFrame: StackFrame } @@ -49,20 +49,15 @@ function getOriginalStackFrame( error: false, reason: null, external: false, - expanded: !Boolean( - /* collapsed */ - (source.file?.includes('node_modules') || - body.originalStackFrame?.file?.includes('node_modules') || - body.originalStackFrame?.file?.startsWith('[turbopack]/')) ?? - true - ), sourceStackFrame: source, originalStackFrame: body.originalStackFrame, originalCodeFrame: body.originalCodeFrame || null, sourcePackage: body.sourcePackage, + ignored: body.originalStackFrame?.ignored || false, } } + // TODO: merge this section into ignoredList handling if ( source.file === '' || source.file === 'file://' || @@ -73,11 +68,11 @@ function getOriginalStackFrame( error: false, reason: null, external: true, - expanded: false, sourceStackFrame: source, originalStackFrame: null, originalCodeFrame: null, sourcePackage: null, + ignored: true, }) } @@ -85,11 +80,11 @@ function getOriginalStackFrame( error: true, reason: err?.message ?? err?.toString() ?? 'Unknown Error', external: false, - expanded: false, sourceStackFrame: source, originalStackFrame: null, originalCodeFrame: null, sourcePackage: null, + ignored: false, })) } diff --git a/packages/next/src/client/components/react-dev-overlay/server/middleware-turbopack.ts b/packages/next/src/client/components/react-dev-overlay/server/middleware-turbopack.ts index b9dc0b0410efd..a416c230e4582 100644 --- a/packages/next/src/client/components/react-dev-overlay/server/middleware-turbopack.ts +++ b/packages/next/src/client/components/react-dev-overlay/server/middleware-turbopack.ts @@ -19,11 +19,13 @@ import type { Project, TurbopackStackFrame } from '../../../../build/swc/types' import { getSourceMapFromFile } from '../internal/helpers/get-source-map-from-file' import { findSourceMap } from 'node:module' +type IgnorableStackFrame = StackFrame & { ignored: boolean } + const currentSourcesByFile: Map> = new Map() export async function batchedTraceSource( project: Project, frame: TurbopackStackFrame -): Promise<{ frame: StackFrame; source: string | null } | undefined> { +): Promise<{ frame: IgnorableStackFrame; source: string | null } | undefined> { const file = frame.file ? decodeURIComponent(frame.file) : undefined if (!file) return @@ -31,10 +33,15 @@ export async function batchedTraceSource( if (!sourceFrame) return let source = null + let ignored = true // Don't look up source for node_modules or internals. These can often be large bundled files. if ( sourceFrame.file && - !(sourceFrame.file.includes('node_modules') || sourceFrame.isInternal) + !( + sourceFrame.file.includes('node_modules') || + // isInternal means resource starts with turbopack://[turbopack] + sourceFrame.isInternal + ) ) { let sourcePromise = currentSourcesByFile.get(sourceFrame.file) if (!sourcePromise) { @@ -46,18 +53,22 @@ export async function batchedTraceSource( currentSourcesByFile.delete(sourceFrame.file!) }, 100) } - + ignored = false source = await sourcePromise } + // TODO: get ignoredList from turbopack source map + const ignorableFrame = { + file: sourceFrame.file, + lineNumber: sourceFrame.line ?? 0, + column: sourceFrame.column ?? 0, + methodName: sourceFrame.methodName ?? frame.methodName ?? '', + ignored, + arguments: [], + } + return { - frame: { - file: sourceFrame.file, - lineNumber: sourceFrame.line ?? 0, - column: sourceFrame.column ?? 0, - methodName: sourceFrame.methodName ?? frame.methodName ?? '', - arguments: [], - }, + frame: ignorableFrame, source, } } diff --git a/packages/next/src/client/components/react-dev-overlay/server/middleware.ts b/packages/next/src/client/components/react-dev-overlay/server/middleware.ts index b1c6e42d98a47..ac8baf005f7ed 100644 --- a/packages/next/src/client/components/react-dev-overlay/server/middleware.ts +++ b/packages/next/src/client/components/react-dev-overlay/server/middleware.ts @@ -21,18 +21,43 @@ export { getSourceMapFromFile } import type { IncomingMessage, ServerResponse } from 'http' import type webpack from 'webpack' -import type { RawSourceMap } from 'next/dist/compiled/source-map08' +import type { + NullableMappedPosition, + RawSourceMap, +} from 'next/dist/compiled/source-map08' import { formatFrameSourceFile } from '../internal/helpers/webpack-module-path' +import type { MappedPosition } from 'source-map' + +function shouldIgnorePath(modulePath: string): boolean { + return ( + modulePath.includes('node_modules') || + // Only relevant for when Next.js is symlinked e.g. in the Next.js monorepo + modulePath.includes('next/dist') + ) +} + +type IgnoredSources = Array<{ url: string; ignored: boolean }> + +export interface IgnorableStackFrame extends StackFrame { + ignored: boolean +} + +type SourceAttributes = { + sourcePosition: NullableMappedPosition + sourceContent: string | null +} type Source = | { type: 'file' sourceMap: RawSourceMap + ignoredSources: IgnoredSources modulePath: string } | { type: 'bundle' sourceMap: RawSourceMap + ignoredSources: IgnoredSources compilation: webpack.Compilation moduleId: string modulePath: string @@ -58,7 +83,7 @@ function getSourcePath(source: string) { async function findOriginalSourcePositionAndContent( sourceMap: RawSourceMap, position: { line: number; column: number | null } -) { +): Promise { const consumer = await new SourceMapConsumer(sourceMap) try { const sourcePosition = consumer.originalPositionFor({ @@ -85,9 +110,50 @@ async function findOriginalSourcePositionAndContent( } } +export function getIgnoredSources(sourceMap: RawSourceMap): IgnoredSources { + const ignoreList = new Set() + const moduleFilenames = sourceMap?.sources ?? [] + + for (let index = 0; index < moduleFilenames.length; index++) { + // bundlerFilePath case: webpack://./app/page.tsx + const bundlerFilePath = moduleFilenames[index] + // Format the path to the normal file path + const formattedFilePath = formatFrameSourceFile(bundlerFilePath) + if (shouldIgnorePath(formattedFilePath)) { + ignoreList.add(index) + } + } + + const ignoredSources = sourceMap.sources.map((source, index) => { + return { + url: source, + ignored: ignoreList.has(sourceMap.sources.indexOf(source)), + content: sourceMap.sourcesContent?.[index] ?? null, + } + }) + return ignoredSources +} + +function isIgnoredSource( + source: Source, + sourcePosition: MappedPosition | NullableMappedPosition +) { + if (sourcePosition.source == null) { + return true + } + for (const ignoredSource of source.ignoredSources) { + if (ignoredSource.ignored && ignoredSource.url === sourcePosition.source) { + return true + } + } + + return false +} + function createStackFrame(searchParams: URLSearchParams) { + const file = searchParams.get('file') as string return { - file: searchParams.get('file') as string, + file, methodName: searchParams.get('methodName') as string, lineNumber: parseInt(searchParams.get('lineNumber') ?? '0', 10) || 0, column: parseInt(searchParams.get('column') ?? '0', 10) || 0, @@ -99,7 +165,7 @@ function findOriginalSourcePositionAndContentFromCompilation( moduleId: string | undefined, importedModule: string, compilation: webpack.Compilation -) { +): SourceAttributes | null { const module = getModuleById(moduleId, compilation) return module?.buildInfo?.importLocByPath?.get(importedModule) ?? null } @@ -136,27 +202,34 @@ export async function createOriginalStackFrame({ }) })() - if (!result?.sourcePosition.source) { + if (!result) { return null } - const { sourcePosition, sourceContent } = result - const filePath = path.resolve( - rootDirectory, - getSourcePath( - // When sourcePosition.source is the loader path the modulePath is generally better. - (sourcePosition.source.includes('|') - ? source.modulePath - : sourcePosition.source) || source.modulePath - ) + if (!sourcePosition.source) { + return null + } + + const ignored = + isIgnoredSource(source, sourcePosition) || + // If the source file is externals, should be excluded even it's not ignored source. + // e.g. webpack://next/dist/.. needs to be ignored + shouldIgnorePath(source.modulePath) + + const sourcePath = getSourcePath( + // When sourcePosition.source is the loader path the modulePath is generally better. + (sourcePosition.source!.includes('|') + ? source.modulePath + : sourcePosition.source) || source.modulePath ) + const filePath = path.resolve(rootDirectory, sourcePath) const resolvedFilePath = sourceContent ? path.relative(rootDirectory, filePath) : sourcePosition.source - const traced = { + const traced: IgnorableStackFrame = { file: resolvedFilePath, lineNumber: sourcePosition.line, column: (sourcePosition.column ?? 0) + 1, @@ -168,7 +241,8 @@ export async function createOriginalStackFrame({ ?.replace('__WEBPACK_DEFAULT_EXPORT__', 'default') ?.replace('__webpack_exports__.', ''), arguments: [], - } satisfies StackFrame + ignored, + } return { originalStackFrame: traced, @@ -201,7 +275,7 @@ export async function getSourceMapFromCompilation( } } -export async function getSource( +async function getSource( filename: string, options: { getCompilations: () => webpack.Compilation[] @@ -215,11 +289,11 @@ export async function getSource( if (filename.startsWith('file:')) { const sourceMap = await getSourceMapFromFile(filename) - return sourceMap ? { type: 'file', sourceMap, + ignoredSources: getIgnoredSources(sourceMap), modulePath: filename.replace(/^file:\/\//, ''), } : undefined @@ -241,9 +315,29 @@ export async function getSource( for (const compilation of getCompilations()) { // TODO: `ignoreList` const sourceMap = await getSourceMapFromCompilation(moduleId, compilation) + const ignoreList = [] + const moduleFilenames = sourceMap?.sources ?? [] + + for (let index = 0; index < moduleFilenames.length; index++) { + // bundlerFilePath case: webpack://./app/page.tsx + const bundlerFilePath = moduleFilenames[index] + // Format the path to the normal file path + const formattedFilePath = formatFrameSourceFile(bundlerFilePath) + if (shouldIgnorePath(formattedFilePath)) { + ignoreList.push(index) + } + } if (sourceMap) { - return { type: 'bundle', sourceMap, compilation, moduleId, modulePath } + const ignoredSources = getIgnoredSources(sourceMap) + return { + type: 'bundle', + sourceMap, + compilation, + moduleId, + modulePath, + ignoredSources, + } } } @@ -270,24 +364,11 @@ export function getOverlayMiddleware(options: { const isEdgeServer = searchParams.get('isEdgeServer') === 'true' const isAppDirectory = searchParams.get('isAppDirectory') === 'true' const frame = createStackFrame(searchParams) - - let sourcePackage = findSourcePackage(frame) - - if ( - !( - /^(rsc:\/\/React\/[^/]+\/)?(webpack-internal:\/\/\/|(file|webpack):\/\/)/.test( - frame.file - ) && frame.lineNumber - ) - ) { - if (sourcePackage) return json(res, { sourcePackage }) - return badRequest(res) - } - const formattedFilePath = formatFrameSourceFile(frame.file) const filePath = path.join(rootDirectory, formattedFilePath) const isNextjsSource = filePath.startsWith(NEXT_PROJECT_ROOT) + let sourcePackage = findSourcePackage(frame) let source: Source | undefined if (isNextjsSource) { diff --git a/packages/next/src/client/components/react-dev-overlay/server/shared.ts b/packages/next/src/client/components/react-dev-overlay/server/shared.ts index 6ebfd3d43518f..efa87a0317eca 100644 --- a/packages/next/src/client/components/react-dev-overlay/server/shared.ts +++ b/packages/next/src/client/components/react-dev-overlay/server/shared.ts @@ -10,7 +10,7 @@ import isInternal, { export type SourcePackage = 'react' | 'next' export interface OriginalStackFrameResponse { - originalStackFrame?: StackFrame | null + originalStackFrame?: (StackFrame & { ignored: boolean }) | null originalCodeFrame?: string | null /** We use this to group frames in the error overlay */ sourcePackage?: SourcePackage | null diff --git a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts index 953efd257c26a..2106524da5204 100644 --- a/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts +++ b/packages/next/src/server/lib/router-utils/setup-dev-bundler.ts @@ -69,6 +69,7 @@ import { getSourceMapFromCompilation, getSourceMapFromFile, parseStack, + getIgnoredSources, } from '../../../client/components/react-dev-overlay/server/middleware' import { batchedTraceSource, @@ -1012,6 +1013,7 @@ async function startWatcher(opts: SetupOpts) { type: 'bundle', sourceMap, compilation, + ignoredSources: getIgnoredSources(sourceMap), moduleId, modulePath, },