@@ -2,6 +2,11 @@ import { URL } from 'node:url';
22import { codeFrameColumns } from '@babel/code-frame' ;
33import type { FastifyBaseLogger } from 'fastify' ;
44import { SourceMapConsumer } from 'source-map' ;
5+ import {
6+ isGeneratedBundleFrame ,
7+ isSymbolicatableFrame ,
8+ normalizeInvalidWebpackSourceUrls ,
9+ } from '../../utils/symbolication.js' ;
510import type {
611 CodeFrame ,
712 InputStackFrame ,
@@ -45,11 +50,6 @@ export class Symbolicator {
4550 }
4651 }
4752
48- /**
49- * Cache with initialized `SourceMapConsumer` to improve symbolication performance.
50- */
51- sourceMapConsumerCache : Record < string , SourceMapConsumer > = { } ;
52-
5353 /**
5454 * Constructs new `Symbolicator` instance.
5555 *
@@ -75,86 +75,99 @@ export class Symbolicator {
7575 ) : Promise < SymbolicatorResults > {
7676 logger . debug ( { msg : 'Filtering out unnecessary frames' } ) ;
7777
78- const frames : InputStackFrame [ ] = [ ] ;
79- for ( const frame of stack ) {
80- const { file } = frame ;
81- if ( file ?. startsWith ( 'http' ) ) {
82- frames . push ( frame as InputStackFrame ) ;
83- }
84- }
78+ const frames = stack . filter ( isSymbolicatableFrame ) ;
8579
80+ // A Symbolicator instance is shared by the route. Keep consumers local to
81+ // one request so concurrent call-stack and component-stack requests cannot
82+ // destroy or read each other's source maps.
83+ const sourceMapConsumers = new Map < string , SourceMapConsumer > ( ) ;
8684 try {
8785 logger . debug ( { msg : 'Processing frames' , frames } ) ;
8886
8987 const processedFrames : StackFrame [ ] = [ ] ;
9088 for ( const frame of frames ) {
91- if ( ! this . sourceMapConsumerCache [ frame . file ] ) {
92- logger . debug ( {
93- msg : 'Loading raw source map data' ,
94- fileUrl : frame . file ,
95- } ) ;
89+ try {
90+ if ( ! sourceMapConsumers . has ( frame . file ) ) {
91+ logger . debug ( {
92+ msg : 'Loading raw source map data' ,
93+ fileUrl : frame . file ,
94+ } ) ;
95+
96+ const rawSourceMap = await this . delegate . getSourceMap ( frame . file ) ;
9697
97- const rawSourceMap = await this . delegate . getSourceMap ( frame . file ) ;
98+ logger . debug ( {
99+ msg : 'Creating source map instance' ,
100+ fileUrl : frame . file ,
101+ sourceMapLength : rawSourceMap . length ,
102+ } ) ;
103+ const sourceMapConsumer = await new SourceMapConsumer (
104+ normalizeInvalidWebpackSourceUrls ( rawSourceMap )
105+ ) ;
106+
107+ logger . debug ( {
108+ msg : 'Saving source map instance into cache' ,
109+ fileUrl : frame . file ,
110+ } ) ;
111+ sourceMapConsumers . set ( frame . file , sourceMapConsumer ) ;
112+ }
98113
99114 logger . debug ( {
100- msg : 'Creating source map instance' ,
101- fileUrl : frame . file ,
102- sourceMapLength : rawSourceMap . length ,
115+ msg : 'Symbolicating frame' ,
116+ frame,
103117 } ) ;
104- const sourceMapConsumer = await new SourceMapConsumer (
105- rawSourceMap . toString ( )
106- ) ;
118+ const processedFrame = this . processFrame ( frame , sourceMapConsumers ) ;
107119
108120 logger . debug ( {
109- msg : 'Saving source map instance into cache' ,
121+ msg : 'Finished symbolicating frame' ,
122+ frame,
123+ } ) ;
124+ processedFrames . push ( processedFrame ) ;
125+ } catch ( error ) {
126+ // Match Metro's best-effort behavior: one unavailable or malformed
127+ // source map must not discard frames that can still be symbolicated.
128+ logger . debug ( {
129+ msg : 'Failed to symbolicate frame' ,
110130 fileUrl : frame . file ,
131+ error : ( error as Error ) . message ,
111132 } ) ;
112- this . sourceMapConsumerCache [ frame . file ] = sourceMapConsumer ;
133+ processedFrames . push ( { ... frame , collapse : false } ) ;
113134 }
114-
115- logger . debug ( {
116- msg : 'Symbolicating frame' ,
117- frame,
118- } ) ;
119- const processedFrame = this . processFrame ( frame ) ;
120-
121- logger . debug ( {
122- msg : 'Finished symbolicating frame' ,
123- frame,
124- } ) ;
125- processedFrames . push ( processedFrame ) ;
126135 }
127136
128137 const codeFrame =
129- ( await this . getCodeFrame ( logger , processedFrames ) ) ?? null ;
138+ ( await this . getCodeFrame (
139+ logger ,
140+ processedFrames ,
141+ frames ,
142+ sourceMapConsumers
143+ ) ) ?? null ;
130144
131145 logger . debug ( {
132146 msg : 'Finished symbolicating frames' ,
133147 processedFrames,
134148 codeFrame,
135149 } ) ;
136150
137- return {
138- stack : processedFrames ,
139- codeFrame,
140- } ;
151+ return { stack : processedFrames , codeFrame } ;
141152 } finally {
142- for ( const key in this . sourceMapConsumerCache ) {
143- this . sourceMapConsumerCache [ key ] . destroy ( ) ;
144- delete this . sourceMapConsumerCache [ key ] ;
153+ for ( const consumer of sourceMapConsumers . values ( ) ) {
154+ consumer . destroy ( ) ;
145155 }
146156 }
147157 }
148158
149- private processFrame ( frame : InputStackFrame ) : StackFrame {
150- if ( ! frame . lineNumber || ! frame . column ) {
159+ private processFrame (
160+ frame : InputStackFrame ,
161+ sourceMapConsumers : Map < string , SourceMapConsumer >
162+ ) : StackFrame {
163+ if ( frame . lineNumber == null || frame . column == null ) {
151164 return {
152165 ...frame ,
153166 collapse : false ,
154167 } ;
155168 }
156169
157- const consumer = this . sourceMapConsumerCache [ frame . file ] ;
170+ const consumer = sourceMapConsumers . get ( frame . file ) ;
158171 if ( ! consumer ) {
159172 return {
160173 ...frame ,
@@ -186,8 +199,8 @@ export class Symbolicator {
186199 }
187200
188201 return {
189- lineNumber : lookup . line || frame . lineNumber ,
190- column : lookup . column || frame . column ,
202+ lineNumber : lookup . line ?? frame . lineNumber ,
203+ column : lookup . column ?? frame . column ,
191204 file : lookup . source ,
192205 methodName : lookup . name || frame . methodName ,
193206 collapse : false ,
@@ -196,10 +209,16 @@ export class Symbolicator {
196209
197210 private async getCodeFrame (
198211 logger : FastifyBaseLogger ,
199- processedFrames : StackFrame [ ]
212+ processedFrames : StackFrame [ ] ,
213+ inputFrames : InputStackFrame [ ] ,
214+ sourceMapConsumers : Map < string , SourceMapConsumer >
200215 ) : Promise < CodeFrame | undefined > {
201- for ( const frame of processedFrames ) {
202- if ( frame . collapse || ! frame . lineNumber || ! frame . column ) {
216+ for ( const [ index , frame ] of processedFrames . entries ( ) ) {
217+ if ( frame . collapse || frame . lineNumber == null || frame . column == null ) {
218+ continue ;
219+ }
220+
221+ if ( isGeneratedBundleFrame ( frame ) ) {
203222 continue ;
204223 }
205224
@@ -213,9 +232,15 @@ export class Symbolicator {
213232 } ) ;
214233
215234 try {
235+ const consumer = sourceMapConsumers . get ( inputFrames [ index ] ?. file ) ;
236+ const embeddedSource = consumer ?. sourceContentFor ( frame . file , true ) ;
237+ const source =
238+ embeddedSource ??
239+ ( await this . delegate . getSource ( frame . file ) ) . toString ( ) ;
240+
216241 return {
217242 content : codeFrameColumns (
218- ( await this . delegate . getSource ( frame . file ) ) . toString ( ) ,
243+ source ,
219244 {
220245 start : { column : frame . column , line : frame . lineNumber } ,
221246 } ,
0 commit comments