diff --git a/packages/next/src/server/patch-error-inspect.ts b/packages/next/src/server/patch-error-inspect.ts index cc7e1a9907f38..cb00b47c5b45f 100644 --- a/packages/next/src/server/patch-error-inspect.ts +++ b/packages/next/src/server/patch-error-inspect.ts @@ -9,17 +9,37 @@ import { getOriginalCodeFrame } from '../client/components/react-dev-overlay/ser import { workUnitAsyncStorage } from './app-render/work-unit-async-storage.external' import { dim } from '../lib/picocolors' +/** + * https://tc39.es/source-map/#index-map + */ +interface IndexSourceMapSection { + offset: { + line: number + column: number + } + map: ModernRawSourceMap +} + +// TODO(veil): Upstream types +interface IndexSourceMap { + version: number + file: string + sections: IndexSourceMapSection[] +} + interface ModernRawSourceMap extends SourceMapPayload { ignoreList?: number[] } +type ModernSourceMapPayload = ModernRawSourceMap | IndexSourceMap + interface IgnoreableStackFrame extends StackFrame { ignored: boolean } type SourceMapCache = Map< string, - { map: SyncSourceMapConsumer; raw: ModernRawSourceMap } + { map: SyncSourceMapConsumer; payload: ModernSourceMapPayload } > // TODO: Implement for Edge runtime @@ -77,6 +97,37 @@ function shouldIgnoreListByDefault(file: string): boolean { return file.startsWith('node:') } +/** + * Finds the sourcemap payload applicable to a given frame. + * Equal to the input unless an Index Source Map is used. + */ +function findApplicableSourceMapPayload( + frame: StackFrame, + payload: ModernSourceMapPayload +): ModernRawSourceMap | undefined { + if ('sections' in payload) { + const frameLine = frame.lineNumber ?? 0 + const frameColumn = frame.column ?? 0 + // Sections must not overlap and must be sorted: https://tc39.es/source-map/#section-object + // Therefore the last section that has an offset less than or equal to the frame is the applicable one. + // TODO(veil): Binary search + let section: IndexSourceMapSection | undefined = payload.sections[0] + for ( + let i = 0; + i < payload.sections.length && + payload.sections[i].offset.line <= frameLine && + payload.sections[i].offset.column <= frameColumn; + i++ + ) { + section = payload.sections[i] + } + + return section === undefined ? undefined : section.map + } else { + return payload + } +} + function getSourcemappedFrameIfPossible( frame: StackFrame, sourceMapCache: SourceMapCache @@ -91,24 +142,24 @@ function getSourcemappedFrameIfPossible( const sourceMapCacheEntry = sourceMapCache.get(frame.file) let sourceMap: SyncSourceMapConsumer - let rawSourceMap: ModernRawSourceMap + let sourceMapPayload: ModernSourceMapPayload if (sourceMapCacheEntry === undefined) { const moduleSourceMap = findSourceMap(frame.file) if (moduleSourceMap === undefined) { return null } - rawSourceMap = moduleSourceMap.payload + sourceMapPayload = moduleSourceMap.payload sourceMap = new SyncSourceMapConsumer( // @ts-expect-error -- Module.SourceMap['version'] is number but SyncSourceMapConsumer wants a string - rawSourceMap + sourceMapPayload ) sourceMapCache.set(frame.file, { map: sourceMap, - raw: rawSourceMap, + payload: sourceMapPayload, }) } else { sourceMap = sourceMapCacheEntry.map - rawSourceMap = sourceMapCacheEntry.raw + sourceMapPayload = sourceMapCacheEntry.payload } const sourcePosition = sourceMap.originalPositionFor({ @@ -126,9 +177,21 @@ function getSourcemappedFrameIfPossible( /* returnNullOnMissing */ true ) ?? null - // TODO: O(n^2). Consider moving `ignoreList` into a Set - const sourceIndex = rawSourceMap.sources.indexOf(sourcePosition.source) - const ignored = rawSourceMap.ignoreList?.includes(sourceIndex) ?? false + const applicableSourcMap = findApplicableSourceMapPayload( + frame, + sourceMapPayload + ) + // TODO(veil): Upstream a method to sourcemap consumer that immediately says if a frame is ignored or not. + let ignored = false + if (applicableSourcMap === undefined) { + console.error('No applicable source map found in sections for frame', frame) + } else { + // TODO: O(n^2). Consider moving `ignoreList` into a Set + const sourceIndex = applicableSourcMap.sources.indexOf( + sourcePosition.source + ) + ignored = applicableSourcMap.ignoreList?.includes(sourceIndex) ?? false + } const originalFrame: IgnoreableStackFrame = { methodName: diff --git a/test/development/app-dir/dynamic-io-dev-errors/dynamic-io-dev-errors.test.ts b/test/development/app-dir/dynamic-io-dev-errors/dynamic-io-dev-errors.test.ts index c14dd4e04ed37..33bcd0ab7964b 100644 --- a/test/development/app-dir/dynamic-io-dev-errors/dynamic-io-dev-errors.test.ts +++ b/test/development/app-dir/dynamic-io-dev-errors/dynamic-io-dev-errors.test.ts @@ -67,12 +67,12 @@ describe('Dynamic IO Dev Errors', () => { `We don't have the exact line number added to error messages yet but you can see which component in the stack below. ` + `See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense` + '\n at Page [Server] ()' + - // TODO(veil): Should be ignore-listed. Feel free to adjust the component name since it's Next.js internals. - '\n at InnerLayoutRouter (' + (isTurbopack - ? 'node_modules' - : // TODO(veil): Why is this not pointing to n_m in Webpack? - '../') + ? // TODO(Veil): Should be sourcemapped + '\n at InnerScrollAndFocusHandler (.next/' + : // TODO(veil): Should be ignore-listed + // TODO(veil): Why is this not pointing to n_m in Webpack? + '\n at InnerLayoutRouter (..') ) const description = await getRedboxDescription(browser)