@@ -14,9 +14,10 @@ import path from 'path'
1414import url from 'url'
1515import { launchEditor } from '../internal/helpers/launchEditor'
1616import type { StackFrame } from 'next/dist/compiled/stacktrace-parser'
17+ import { SourceMapConsumer } from 'next/dist/compiled/source-map08'
1718import type { Project , TurbopackStackFrame } from '../../../../build/swc/types'
1819import { getSourceMapFromFile } from '../internal/helpers/get-source-map-from-file'
19- import { findSourceMap } from 'node:module'
20+ import { findSourceMap , type SourceMapPayload } from 'node:module'
2021
2122function shouldIgnorePath ( modulePath : string ) : boolean {
2223 return (
@@ -33,7 +34,10 @@ export async function batchedTraceSource(
3334 project : Project ,
3435 frame : TurbopackStackFrame
3536) : Promise < { frame : IgnorableStackFrame ; source : string | null } | undefined > {
36- const file = frame . file ? decodeURIComponent ( frame . file ) : undefined
37+ const file = frame . file
38+ ? // TODO(veil): Why are the frames sent encoded?
39+ decodeURIComponent ( frame . file )
40+ : undefined
3741 if ( ! file ) return
3842
3943 const sourceFrame = await project . traceSource ( frame )
@@ -108,11 +112,155 @@ function createStackFrame(searchParams: URLSearchParams) {
108112 } satisfies TurbopackStackFrame
109113}
110114
115+ /**
116+ * https://tc39.es/source-map/#index-map
117+ */
118+ interface IndexSourceMapSection {
119+ offset : {
120+ line : number
121+ column : number
122+ }
123+ map : ModernRawSourceMap
124+ }
125+
126+ // TODO(veil): Upstream types
127+ interface IndexSourceMap {
128+ version : number
129+ file : string
130+ sections : IndexSourceMapSection [ ]
131+ }
132+
133+ interface ModernRawSourceMap extends SourceMapPayload {
134+ ignoreList ?: number [ ]
135+ }
136+
137+ type ModernSourceMapPayload = ModernRawSourceMap | IndexSourceMap
138+
139+ /**
140+ * Finds the sourcemap payload applicable to a given frame.
141+ * Equal to the input unless an Index Source Map is used.
142+ */
143+ function findApplicableSourceMapPayload (
144+ frame : TurbopackStackFrame ,
145+ payload : ModernSourceMapPayload
146+ ) : ModernRawSourceMap | undefined {
147+ if ( 'sections' in payload ) {
148+ const frameLine = frame . line ?? 0
149+ const frameColumn = frame . column ?? 0
150+ // Sections must not overlap and must be sorted: https://tc39.es/source-map/#section-object
151+ // Therefore the last section that has an offset less than or equal to the frame is the applicable one.
152+ // TODO(veil): Binary search
153+ let section : IndexSourceMapSection | undefined = payload . sections [ 0 ]
154+ for (
155+ let i = 0 ;
156+ i < payload . sections . length &&
157+ payload . sections [ i ] . offset . line <= frameLine &&
158+ payload . sections [ i ] . offset . column <= frameColumn ;
159+ i ++
160+ ) {
161+ section = payload . sections [ i ]
162+ }
163+
164+ return section === undefined ? undefined : section . map
165+ } else {
166+ return payload
167+ }
168+ }
169+
170+ async function nativeTraceSource (
171+ frame : TurbopackStackFrame
172+ ) : Promise < { frame : IgnorableStackFrame ; source : string | null } | undefined > {
173+ const sourceMap = findSourceMap (
174+ // TODO(veil): Why are the frames sent encoded?
175+ decodeURIComponent ( frame . file )
176+ )
177+ if ( sourceMap !== undefined ) {
178+ const traced = await SourceMapConsumer . with (
179+ sourceMap . payload ,
180+ null ,
181+ async ( consumer ) => {
182+ const originalPosition = consumer . originalPositionFor ( {
183+ line : frame . line ?? 1 ,
184+ column : frame . column ?? 1 ,
185+ } )
186+
187+ if ( originalPosition . source === null ) {
188+ return null
189+ }
190+
191+ const sourceContent : string | null =
192+ consumer . sourceContentFor (
193+ originalPosition . source ,
194+ /* returnNullOnMissing */ true
195+ ) ?? null
196+
197+ return { originalPosition, sourceContent }
198+ }
199+ )
200+
201+ if ( traced !== null ) {
202+ const { originalPosition, sourceContent } = traced
203+ const applicableSourceMap = findApplicableSourceMapPayload (
204+ frame ,
205+ sourceMap . payload
206+ )
207+
208+ // TODO(veil): Upstream a method to sourcemap consumer that immediately says if a frame is ignored or not.
209+ let ignored = false
210+ if ( applicableSourceMap === undefined ) {
211+ console . error (
212+ 'No applicable source map found in sections for frame' ,
213+ frame
214+ )
215+ } else {
216+ // TODO: O(n^2). Consider moving `ignoreList` into a Set
217+ const sourceIndex = applicableSourceMap . sources . indexOf (
218+ originalPosition . source !
219+ )
220+ ignored = applicableSourceMap . ignoreList ?. includes ( sourceIndex ) ?? false
221+ }
222+
223+ const originalStackFrame : IgnorableStackFrame = {
224+ methodName :
225+ originalPosition . name ||
226+ // default is not a valid identifier in JS so webpack uses a custom variable when it's an unnamed default export
227+ // Resolve it back to `default` for the method name if the source position didn't have the method.
228+ frame . methodName
229+ ?. replace ( '__WEBPACK_DEFAULT_EXPORT__' , 'default' )
230+ ?. replace ( '__webpack_exports__.' , '' ) ||
231+ '<unknown>' ,
232+ column : ( originalPosition . column ?? 0 ) + 1 ,
233+ file : originalPosition . source ?. startsWith ( 'file://' )
234+ ? path . relative (
235+ process . cwd ( ) ,
236+ url . fileURLToPath ( originalPosition . source )
237+ )
238+ : originalPosition . source ,
239+ lineNumber : originalPosition . line ?? 0 ,
240+ // TODO: c&p from async createOriginalStackFrame but why not frame.arguments?
241+ arguments : [ ] ,
242+ ignored,
243+ }
244+
245+ return {
246+ frame : originalStackFrame ,
247+ source : sourceContent ,
248+ }
249+ }
250+ }
251+
252+ return undefined
253+ }
254+
111255export async function createOriginalStackFrame (
112256 project : Project ,
113257 frame : TurbopackStackFrame
114258) : Promise < OriginalStackFrameResponse | null > {
115- const traced = await batchedTraceSource ( project , frame )
259+ const traced =
260+ ( await nativeTraceSource ( frame ) ) ??
261+ // TODO(veil): When would the bundler know more than native?
262+ // If it's faster, try the bundler first and fall back to native later.
263+ ( await batchedTraceSource ( project , frame ) )
116264 if ( ! traced ) {
117265 return null
118266 }
@@ -193,6 +341,8 @@ export function getSourceMapMiddleware(project: Project) {
193341 return badRequest ( res )
194342 }
195343
344+ // TODO(veil): Always try the native version first.
345+ // Externals could also be files that aren't bundled via Webpack.
196346 if (
197347 filename . startsWith ( 'webpack://' ) ||
198348 filename . startsWith ( 'webpack-internal:///' )
0 commit comments