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 strict-local
    
  8.  */
    
  9. 
    
  10. import type {Position} from './astUtils';
    
  11. import type {
    
  12.   ReactSourceMetadata,
    
  13.   IndexSourceMap,
    
  14.   BasicSourceMap,
    
  15.   MixedSourceMap,
    
  16. } from './SourceMapTypes';
    
  17. import type {HookMap} from './generateHookMap';
    
  18. import * as util from 'source-map-js/lib/util';
    
  19. import {decodeHookMap} from './generateHookMap';
    
  20. import {getHookNameForLocation} from './getHookNameForLocation';
    
  21. 
    
  22. type MetadataMap = Map<string, ?ReactSourceMetadata>;
    
  23. 
    
  24. const HOOK_MAP_INDEX_IN_REACT_METADATA = 0;
    
  25. const REACT_METADATA_INDEX_IN_FB_METADATA = 1;
    
  26. const REACT_SOURCES_EXTENSION_KEY = 'x_react_sources';
    
  27. const FB_SOURCES_EXTENSION_KEY = 'x_facebook_sources';
    
  28. 
    
  29. /**
    
  30.  * Extracted from the logic in [email protected]'s SourceMapConsumer.
    
  31.  * By default, source names are normalized using the same logic that the `[email protected]` package uses internally.
    
  32.  * This is crucial for keeping the sources list in sync with a `SourceMapConsumer` instance.
    
  33.  */
    
  34. function normalizeSourcePath(
    
  35.   sourceInput: string,
    
  36.   map: {+sourceRoot?: ?string, ...},
    
  37. ): string {
    
  38.   const {sourceRoot} = map;
    
  39.   let source = sourceInput;
    
  40. 
    
  41.   source = String(source);
    
  42.   // Some source maps produce relative source paths like "./foo.js" instead of
    
  43.   // "foo.js".  Normalize these first so that future comparisons will succeed.
    
  44.   // See bugzil.la/1090768.
    
  45.   source = util.normalize(source);
    
  46.   // Always ensure that absolute sources are internally stored relative to
    
  47.   // the source root, if the source root is absolute. Not doing this would
    
  48.   // be particularly problematic when the source root is a prefix of the
    
  49.   // source (valid, but why??). See github issue #199 and bugzil.la/1188982.
    
  50.   source =
    
  51.     sourceRoot != null && util.isAbsolute(sourceRoot) && util.isAbsolute(source)
    
  52.       ? util.relative(sourceRoot, source)
    
  53.       : source;
    
  54.   return util.computeSourceURL(sourceRoot, source);
    
  55. }
    
  56. 
    
  57. /**
    
  58.  * Consumes the `x_react_sources` or  `x_facebook_sources` metadata field from a
    
  59.  * source map and exposes ways to query the React DevTools specific metadata
    
  60.  * included in those fields.
    
  61.  */
    
  62. export class SourceMapMetadataConsumer {
    
  63.   _sourceMap: MixedSourceMap;
    
  64.   _decodedHookMapCache: Map<string, HookMap>;
    
  65.   _metadataBySource: ?MetadataMap;
    
  66. 
    
  67.   constructor(sourcemap: MixedSourceMap) {
    
  68.     this._sourceMap = sourcemap;
    
  69.     this._decodedHookMapCache = new Map();
    
  70.     this._metadataBySource = null;
    
  71.   }
    
  72. 
    
  73.   /**
    
  74.    * Returns the Hook name assigned to a given location in the source code,
    
  75.    * and a HookMap extracted from an extended source map.
    
  76.    * See `getHookNameForLocation` for more details on implementation.
    
  77.    *
    
  78.    * When used with the `source-map` package, you'll first use
    
  79.    * `SourceMapConsumer#originalPositionFor` to retrieve a source location,
    
  80.    * then pass that location to `hookNameFor`.
    
  81.    */
    
  82.   hookNameFor({
    
  83.     line,
    
  84.     column,
    
  85.     source,
    
  86.   }: {
    
  87.     ...Position,
    
  88.     +source: ?string,
    
  89.   }): ?string {
    
  90.     if (source == null) {
    
  91.       return null;
    
  92.     }
    
  93. 
    
  94.     const hookMap = this._getHookMapForSource(source);
    
  95.     if (hookMap == null) {
    
  96.       return null;
    
  97.     }
    
  98. 
    
  99.     return getHookNameForLocation({line, column}, hookMap);
    
  100.   }
    
  101. 
    
  102.   hasHookMap(source: ?string): boolean {
    
  103.     if (source == null) {
    
  104.       return false;
    
  105.     }
    
  106.     return this._getHookMapForSource(source) != null;
    
  107.   }
    
  108. 
    
  109.   /**
    
  110.    * Prepares and caches a lookup table of metadata by source name.
    
  111.    */
    
  112.   _getMetadataBySource(): MetadataMap {
    
  113.     if (this._metadataBySource == null) {
    
  114.       this._metadataBySource = this._getMetadataObjectsBySourceNames(
    
  115.         this._sourceMap,
    
  116.       );
    
  117.     }
    
  118. 
    
  119.     return this._metadataBySource;
    
  120.   }
    
  121. 
    
  122.   /**
    
  123.    * Collects source metadata from the given map using the current source name
    
  124.    * normalization function. Handles both index maps (with sections) and plain
    
  125.    * maps.
    
  126.    *
    
  127.    * NOTE: If any sources are repeated in the map (which shouldn't usually happen,
    
  128.    * but is technically possible because of index maps) we only keep the
    
  129.    * metadata from the last occurrence of any given source.
    
  130.    */
    
  131.   _getMetadataObjectsBySourceNames(sourcemap: MixedSourceMap): MetadataMap {
    
  132.     if (sourcemap.mappings === undefined) {
    
  133.       const indexSourceMap: IndexSourceMap = sourcemap;
    
  134.       const metadataMap = new Map<string, ?ReactSourceMetadata>();
    
  135.       indexSourceMap.sections.forEach(section => {
    
  136.         const metadataMapForIndexMap = this._getMetadataObjectsBySourceNames(
    
  137.           section.map,
    
  138.         );
    
  139.         metadataMapForIndexMap.forEach((value, key) => {
    
  140.           metadataMap.set(key, value);
    
  141.         });
    
  142.       });
    
  143.       return metadataMap;
    
  144.     }
    
  145. 
    
  146.     const metadataMap: MetadataMap = new Map();
    
  147.     const basicMap: BasicSourceMap = sourcemap;
    
  148.     const updateMap = (metadata: ReactSourceMetadata, sourceIndex: number) => {
    
  149.       let source = basicMap.sources[sourceIndex];
    
  150.       if (source != null) {
    
  151.         source = normalizeSourcePath(source, basicMap);
    
  152.         metadataMap.set(source, metadata);
    
  153.       }
    
  154.     };
    
  155. 
    
  156.     if (
    
  157.       sourcemap.hasOwnProperty(REACT_SOURCES_EXTENSION_KEY) &&
    
  158.       sourcemap[REACT_SOURCES_EXTENSION_KEY] != null
    
  159.     ) {
    
  160.       const reactMetadataArray = sourcemap[REACT_SOURCES_EXTENSION_KEY];
    
  161.       reactMetadataArray.filter(Boolean).forEach(updateMap);
    
  162.     } else if (
    
  163.       sourcemap.hasOwnProperty(FB_SOURCES_EXTENSION_KEY) &&
    
  164.       sourcemap[FB_SOURCES_EXTENSION_KEY] != null
    
  165.     ) {
    
  166.       const fbMetadataArray = sourcemap[FB_SOURCES_EXTENSION_KEY];
    
  167.       if (fbMetadataArray != null) {
    
  168.         fbMetadataArray.forEach((fbMetadata, sourceIndex) => {
    
  169.           // When extending source maps with React metadata using the
    
  170.           // x_facebook_sources field, the position at index 1 on the
    
  171.           // metadata tuple is reserved for React metadata
    
  172.           const reactMetadata =
    
  173.             fbMetadata != null
    
  174.               ? fbMetadata[REACT_METADATA_INDEX_IN_FB_METADATA]
    
  175.               : null;
    
  176.           if (reactMetadata != null) {
    
  177.             updateMap(reactMetadata, sourceIndex);
    
  178.           }
    
  179.         });
    
  180.       }
    
  181.     }
    
  182. 
    
  183.     return metadataMap;
    
  184.   }
    
  185. 
    
  186.   /**
    
  187.    * Decodes the function name mappings for the given source if needed, and
    
  188.    * retrieves a sorted, searchable array of mappings.
    
  189.    */
    
  190.   _getHookMapForSource(source: string): ?HookMap {
    
  191.     if (this._decodedHookMapCache.has(source)) {
    
  192.       return this._decodedHookMapCache.get(source);
    
  193.     }
    
  194.     let hookMap = null;
    
  195.     const metadataBySource = this._getMetadataBySource();
    
  196.     const normalized = normalizeSourcePath(source, this._sourceMap);
    
  197.     const metadata = metadataBySource.get(normalized);
    
  198.     if (metadata != null) {
    
  199.       const encodedHookMap = metadata[HOOK_MAP_INDEX_IN_REACT_METADATA];
    
  200.       hookMap = encodedHookMap != null ? decodeHookMap(encodedHookMap) : null;
    
  201.     }
    
  202.     if (hookMap != null) {
    
  203.       this._decodedHookMapCache.set(source, hookMap);
    
  204.     }
    
  205.     return hookMap;
    
  206.   }
    
  207. }