diff --git a/packages/next/src/build/webpack/config/blocks/base.ts b/packages/next/src/build/webpack/config/blocks/base.ts index d7b2ed0501668..e95ad5f1a3699 100644 --- a/packages/next/src/build/webpack/config/blocks/base.ts +++ b/packages/next/src/build/webpack/config/blocks/base.ts @@ -4,14 +4,7 @@ import { COMPILER_NAMES } from '../../../../shared/lib/constants' import type { ConfigurationContext } from '../utils' import DevToolsIgnorePlugin from '../../plugins/devtools-ignore-list-plugin' import EvalSourceMapDevToolPlugin from '../../plugins/eval-source-map-dev-tool-plugin' - -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') - ) -} +import { shouldIgnorePath } from '../ignore-list' export const base = curry(function base( ctx: ConfigurationContext, 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/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..8dc6e8349cadb 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,36 @@ 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 { shouldIgnorePath } from '../../../../build/webpack/config/ignore-list' +import type { MappedPosition } from 'source-map' + +interface ModernRawSourceMap extends RawSourceMap { + ignoreList?: number[] +} + +export interface IgnorableStackFrame extends StackFrame { + ignored: boolean +} + +type SourceAttributes = { + sourcePosition: NullableMappedPosition + sourceContent: string | null +} type Source = | { type: 'file' - sourceMap: RawSourceMap + sourceMap: ModernRawSourceMap modulePath: string } | { type: 'bundle' - sourceMap: RawSourceMap + sourceMap: ModernRawSourceMap compilation: webpack.Compilation moduleId: string modulePath: string @@ -56,9 +74,9 @@ function getSourcePath(source: string) { } async function findOriginalSourcePositionAndContent( - sourceMap: RawSourceMap, + sourceMap: ModernRawSourceMap, position: { line: number; column: number | null } -) { +): Promise { const consumer = await new SourceMapConsumer(sourceMap) try { const sourcePosition = consumer.originalPositionFor({ @@ -85,9 +103,23 @@ async function findOriginalSourcePositionAndContent( } } +function isIgnoredSource( + source: Source, + sourcePosition: MappedPosition | NullableMappedPosition +) { + if (sourcePosition.source == null) { + return true + } + const sourceIndex = source.sourceMap.sources.indexOf(sourcePosition.source) + const ignored = source.sourceMap.ignoreList?.includes(sourceIndex) ?? false + + return ignored +} + 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 +131,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,17 +168,22 @@ export async function createOriginalStackFrame({ }) })() - if (!result?.sourcePosition.source) { + if (!result) { return null } - const { sourcePosition, sourceContent } = result + if (!sourcePosition.source) { + return null + } + + const ignored = isIgnoredSource(source, sourcePosition) + const filePath = path.resolve( rootDirectory, getSourcePath( // When sourcePosition.source is the loader path the modulePath is generally better. - (sourcePosition.source.includes('|') + (sourcePosition.source!.includes('|') ? source.modulePath : sourcePosition.source) || source.modulePath ) @@ -156,7 +193,7 @@ export async function createOriginalStackFrame({ ? path.relative(rootDirectory, filePath) : sourcePosition.source - const traced = { + const traced: IgnorableStackFrame = { file: resolvedFilePath, lineNumber: sourcePosition.line, column: (sourcePosition.column ?? 0) + 1, @@ -168,7 +205,8 @@ export async function createOriginalStackFrame({ ?.replace('__WEBPACK_DEFAULT_EXPORT__', 'default') ?.replace('__webpack_exports__.', ''), arguments: [], - } satisfies StackFrame + ignored, + } return { originalStackFrame: traced, @@ -201,7 +239,7 @@ export async function getSourceMapFromCompilation( } } -export async function getSource( +async function getSource( filename: string, options: { getCompilations: () => webpack.Compilation[] @@ -241,9 +279,30 @@ export async function getSource( for (const compilation of getCompilations()) { // TODO: `ignoreList` const sourceMap = await getSourceMapFromCompilation(moduleId, compilation) + const ignoreList = [] + const moduleFilenames = sourceMap?.sources ?? [] + + // console.log('moduleFilenames', moduleFilenames) + 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 modernSourceMap = sourceMap as ModernRawSourceMap + modernSourceMap.ignoreList = ignoreList + return { + type: 'bundle', + sourceMap: modernSourceMap, + compilation, + moduleId, + modulePath, + } } } @@ -270,24 +329,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