1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @flow
    
  8.  */
    
  9. import {withSyncPerfMeasurements} from 'react-devtools-shared/src/PerformanceLoggingUtils';
    
  10. import {decode} from 'sourcemap-codec';
    
  11. 
    
  12. import type {
    
  13.   IndexSourceMap,
    
  14.   IndexSourceMapSection,
    
  15.   BasicSourceMap,
    
  16.   MixedSourceMap,
    
  17. } from './SourceMapTypes';
    
  18. 
    
  19. type SearchPosition = {
    
  20.   columnNumber: number,
    
  21.   lineNumber: number,
    
  22. };
    
  23. 
    
  24. type ResultPosition = {
    
  25.   column: number,
    
  26.   line: number,
    
  27.   sourceContent: string,
    
  28.   sourceURL: string,
    
  29. };
    
  30. 
    
  31. export type SourceMapConsumerType = {
    
  32.   originalPositionFor: SearchPosition => ResultPosition,
    
  33. };
    
  34. 
    
  35. type Mappings = Array<Array<Array<number>>>;
    
  36. 
    
  37. export default function SourceMapConsumer(
    
  38.   sourceMapJSON: MixedSourceMap | IndexSourceMapSection,
    
  39. ): SourceMapConsumerType {
    
  40.   if (sourceMapJSON.sections != null) {
    
  41.     return IndexedSourceMapConsumer(((sourceMapJSON: any): IndexSourceMap));
    
  42.   } else {
    
  43.     return BasicSourceMapConsumer(((sourceMapJSON: any): BasicSourceMap));
    
  44.   }
    
  45. }
    
  46. 
    
  47. function BasicSourceMapConsumer(sourceMapJSON: BasicSourceMap) {
    
  48.   const decodedMappings: Mappings = withSyncPerfMeasurements(
    
  49.     'Decoding source map mappings with sourcemap-codec',
    
  50.     () => decode(sourceMapJSON.mappings),
    
  51.   );
    
  52. 
    
  53.   function originalPositionFor({
    
  54.     columnNumber,
    
  55.     lineNumber,
    
  56.   }: SearchPosition): ResultPosition {
    
  57.     // Error.prototype.stack columns are 1-based (like most IDEs) but ASTs are 0-based.
    
  58.     const targetColumnNumber = columnNumber - 1;
    
  59. 
    
  60.     const lineMappings = decodedMappings[lineNumber - 1];
    
  61. 
    
  62.     let nearestEntry = null;
    
  63. 
    
  64.     let startIndex = 0;
    
  65.     let stopIndex = lineMappings.length - 1;
    
  66.     let index = -1;
    
  67.     while (startIndex <= stopIndex) {
    
  68.       index = Math.floor((stopIndex + startIndex) / 2);
    
  69.       nearestEntry = lineMappings[index];
    
  70. 
    
  71.       const currentColumn = nearestEntry[0];
    
  72.       if (currentColumn === targetColumnNumber) {
    
  73.         break;
    
  74.       } else {
    
  75.         if (currentColumn > targetColumnNumber) {
    
  76.           if (stopIndex - index > 0) {
    
  77.             stopIndex = index;
    
  78.           } else {
    
  79.             index = stopIndex;
    
  80.             break;
    
  81.           }
    
  82.         } else {
    
  83.           if (index - startIndex > 0) {
    
  84.             startIndex = index;
    
  85.           } else {
    
  86.             index = startIndex;
    
  87.             break;
    
  88.           }
    
  89.         }
    
  90.       }
    
  91.     }
    
  92. 
    
  93.     // We have found either the exact element, or the next-closest element.
    
  94.     // However there may be more than one such element.
    
  95.     // Make sure we always return the smallest of these.
    
  96.     while (index > 0) {
    
  97.       const previousEntry = lineMappings[index - 1];
    
  98.       const currentColumn = previousEntry[0];
    
  99.       if (currentColumn !== targetColumnNumber) {
    
  100.         break;
    
  101.       }
    
  102.       index--;
    
  103.     }
    
  104. 
    
  105.     if (nearestEntry == null) {
    
  106.       // TODO maybe fall back to the runtime source instead of throwing?
    
  107.       throw Error(
    
  108.         `Could not find runtime location for line:${lineNumber} and column:${columnNumber}`,
    
  109.       );
    
  110.     }
    
  111. 
    
  112.     const sourceIndex = nearestEntry[1];
    
  113.     const sourceContent =
    
  114.       sourceMapJSON.sourcesContent != null
    
  115.         ? sourceMapJSON.sourcesContent[sourceIndex]
    
  116.         : null;
    
  117.     const sourceURL = sourceMapJSON.sources[sourceIndex] ?? null;
    
  118.     const line = nearestEntry[2] + 1;
    
  119.     const column = nearestEntry[3];
    
  120. 
    
  121.     if (sourceContent === null || sourceURL === null) {
    
  122.       // TODO maybe fall back to the runtime source instead of throwing?
    
  123.       throw Error(
    
  124.         `Could not find original source for line:${lineNumber} and column:${columnNumber}`,
    
  125.       );
    
  126.     }
    
  127. 
    
  128.     return {
    
  129.       column,
    
  130.       line,
    
  131.       sourceContent: ((sourceContent: any): string),
    
  132.       sourceURL: ((sourceURL: any): string),
    
  133.     };
    
  134.   }
    
  135. 
    
  136.   return (({
    
  137.     originalPositionFor,
    
  138.   }: any): SourceMapConsumerType);
    
  139. }
    
  140. 
    
  141. type Section = {
    
  142.   +generatedColumn: number,
    
  143.   +generatedLine: number,
    
  144.   +map: MixedSourceMap,
    
  145. 
    
  146.   // Lazily parsed only when/as the section is needed.
    
  147.   sourceMapConsumer: SourceMapConsumerType | null,
    
  148. };
    
  149. 
    
  150. function IndexedSourceMapConsumer(sourceMapJSON: IndexSourceMap) {
    
  151.   let lastOffset = {
    
  152.     line: -1,
    
  153.     column: 0,
    
  154.   };
    
  155. 
    
  156.   const sections: Array<Section> = sourceMapJSON.sections.map(section => {
    
  157.     const offset = section.offset;
    
  158.     const offsetLine = offset.line;
    
  159.     const offsetColumn = offset.column;
    
  160. 
    
  161.     if (
    
  162.       offsetLine < lastOffset.line ||
    
  163.       (offsetLine === lastOffset.line && offsetColumn < lastOffset.column)
    
  164.     ) {
    
  165.       throw new Error('Section offsets must be ordered and non-overlapping.');
    
  166.     }
    
  167. 
    
  168.     lastOffset = offset;
    
  169. 
    
  170.     return {
    
  171.       // The offset fields are 0-based, but we use 1-based indices when encoding/decoding from VLQ.
    
  172.       generatedLine: offsetLine + 1,
    
  173.       generatedColumn: offsetColumn + 1,
    
  174.       map: section.map,
    
  175.       sourceMapConsumer: null,
    
  176.     };
    
  177.   });
    
  178. 
    
  179.   function originalPositionFor({
    
  180.     columnNumber,
    
  181.     lineNumber,
    
  182.   }: SearchPosition): ResultPosition {
    
  183.     // Error.prototype.stack columns are 1-based (like most IDEs) but ASTs are 0-based.
    
  184.     const targetColumnNumber = columnNumber - 1;
    
  185. 
    
  186.     let section = null;
    
  187. 
    
  188.     let startIndex = 0;
    
  189.     let stopIndex = sections.length - 1;
    
  190.     let index = -1;
    
  191.     while (startIndex <= stopIndex) {
    
  192.       index = Math.floor((stopIndex + startIndex) / 2);
    
  193.       section = sections[index];
    
  194. 
    
  195.       const currentLine = section.generatedLine;
    
  196.       if (currentLine === lineNumber) {
    
  197.         const currentColumn = section.generatedColumn;
    
  198.         if (currentColumn === lineNumber) {
    
  199.           break;
    
  200.         } else {
    
  201.           if (currentColumn > targetColumnNumber) {
    
  202.             if (stopIndex - index > 0) {
    
  203.               stopIndex = index;
    
  204.             } else {
    
  205.               index = stopIndex;
    
  206.               break;
    
  207.             }
    
  208.           } else {
    
  209.             if (index - startIndex > 0) {
    
  210.               startIndex = index;
    
  211.             } else {
    
  212.               index = startIndex;
    
  213.               break;
    
  214.             }
    
  215.           }
    
  216.         }
    
  217.       } else {
    
  218.         if (currentLine > lineNumber) {
    
  219.           if (stopIndex - index > 0) {
    
  220.             stopIndex = index;
    
  221.           } else {
    
  222.             index = stopIndex;
    
  223.             break;
    
  224.           }
    
  225.         } else {
    
  226.           if (index - startIndex > 0) {
    
  227.             startIndex = index;
    
  228.           } else {
    
  229.             index = startIndex;
    
  230.             break;
    
  231.           }
    
  232.         }
    
  233.       }
    
  234.     }
    
  235. 
    
  236.     if (section == null) {
    
  237.       // TODO maybe fall back to the runtime source instead of throwing?
    
  238.       throw Error(
    
  239.         `Could not find matching section for line:${lineNumber} and column:${columnNumber}`,
    
  240.       );
    
  241.     }
    
  242. 
    
  243.     if (section.sourceMapConsumer === null) {
    
  244.       // Lazily parse the section only when it's needed.
    
  245.       // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
    
  246.       section.sourceMapConsumer = new SourceMapConsumer(section.map);
    
  247.     }
    
  248. 
    
  249.     return section.sourceMapConsumer.originalPositionFor({
    
  250.       columnNumber,
    
  251.       lineNumber,
    
  252.     });
    
  253.   }
    
  254. 
    
  255.   return (({
    
  256.     originalPositionFor,
    
  257.   }: any): SourceMapConsumerType);
    
  258. }