diff --git a/packages/metro/src/Server.js b/packages/metro/src/Server.js index c63fb8dabf..488100a9c9 100644 --- a/packages/metro/src/Server.js +++ b/packages/metro/src/Server.js @@ -1358,6 +1358,17 @@ export default class Server { async _symbolicate(req: IncomingMessage, res: ServerResponse): Promise { const depGraph = await this._bundler.getBundler().getDependencyGraph(); + // A frame's file may be a virtual-prefixed path from a source map with + // server URL source paths, whose segments are percent-encoded. One that + // does not decode is taken as it is. + const filePathOfVirtualPathname = (file: string): ?string => { + try { + return this._rootUrlMap.filePathOfUrlPathname(file); + } catch { + return this._rootUrlMap.filePathOfUrlDecodedPathname(file); + } + }; + const getCodeFrame = ( urls: Set, symbolicatedStack: ReadonlyArray, @@ -1379,7 +1390,16 @@ export default class Server { continue; } - const fileAbsolute = path.resolve(this._config.projectRoot, file ?? ''); + const fileAbsolute = + file != null + ? (filePathOfVirtualPathname(file) ?? + (path.isAbsolute(file) + ? file + : path.resolve(this._config.projectRoot, file))) + : null; + if (fileAbsolute == null) { + continue; + } if (!depGraph.doesFileExist(fileAbsolute)) { debug( 'Skipping code frame for file not in dependency graph.', diff --git a/packages/metro/src/Server/__tests__/Server-test.js b/packages/metro/src/Server/__tests__/Server-test.js index 0a396499f1..f534454236 100644 --- a/packages/metro/src/Server/__tests__/Server-test.js +++ b/packages/metro/src/Server/__tests__/Server-test.js @@ -1485,6 +1485,54 @@ describe('processRequest', () => { expect(result.codeFrame).toBeNull(); }); + test.each([ + ['/[metro-project]/mybundle.js', '/root/mybundle.js'], + ['/[metro-project]/My%20App.js', '/root/My App.js'], + ['/%5Bmetro-project%5D/My%20App.js', '/root/My App.js'], + ['/[metro-project]/My App.js', '/root/My App.js'], + ['/[metro-project]/100%.js', '/root/100%.js'], + ])( + 'should return codeFrame when file is the virtual path %s', + async (file, filePath) => { + fs.writeFileSync(p(filePath), 'this\nis\nfake source'); + + const response = await makeRequest('/symbolicate', { + headers: {'content-type': 'application/json'}, + data: JSON.stringify({ + stack: [{file, lineNumber: 2, column: 0, methodName: 'test'}], + }), + }); + + const result = response._getJSON(); + expect(result.stack[0].file).toBe(file); + expect(result.codeFrame).not.toBeNull(); + expect(result.codeFrame.fileName).toBe(file); + expect(result.codeFrame.content).toEqual(expect.any(String)); + }, + ); + + test('should return codeFrame when file is a relative path (resolved against projectRoot)', async () => { + const response = await makeRequest('/symbolicate', { + headers: {'content-type': 'application/json'}, + data: JSON.stringify({ + stack: [ + { + file: 'mybundle.js', + lineNumber: 2, + column: 0, + methodName: 'test', + }, + ], + }), + }); + + const result = response._getJSON(); + expect(result.stack[0].file).toBe('mybundle.js'); + expect(result.codeFrame).not.toBeNull(); + expect(result.codeFrame.fileName).toBe('mybundle.js'); + expect(result.codeFrame.content).toEqual(expect.any(String)); + }); + // TODO: This probably should restore the *original* file before rewrite // or normalisation. test('should leave original file and position when cannot symbolicate (after normalisation and rewriting?)', async () => {