/* * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict-local */ import type {Position} from './astUtils'; import type { ReactSourceMetadata, IndexSourceMap, BasicSourceMap, MixedSourceMap, } from './SourceMapTypes'; import type {HookMap} from './generateHookMap'; import * as util from 'source-map/lib/util'; import {decodeHookMap} from './generateHookMap'; import {getHookNameForLocation} from './getHookNameForLocation'; type MetadataMap = Map; const HOOK_MAP_INDEX_IN_REACT_METADATA = 0; const REACT_METADATA_INDEX_IN_FB_METADATA = 1; const REACT_SOURCES_EXTENSION_KEY = 'x_react_sources'; const FB_SOURCES_EXTENSION_KEY = 'x_facebook_sources'; /** * Extracted from the logic in source-map@0.8.0-beta.0's SourceMapConsumer. * By default, source names are normalized using the same logic that the * `source-map@0.8.0-beta.0` package uses internally. This is crucial for keeping the * sources list in sync with a `SourceMapConsumer` instance. */ function normalizeSourcePath( sourceInput: string, map: {+sourceRoot?: ?string, ...}, ): string { const {sourceRoot} = map; let source = sourceInput; // eslint-disable-next-line react-internal/no-primitive-constructors source = String(source); return util.computeSourceURL(sourceRoot, source); } /** * Consumes the `x_react_sources` or `x_facebook_sources` metadata field from a * source map and exposes ways to query the React DevTools specific metadata * included in those fields. */ export class SourceMapMetadataConsumer { _sourceMap: MixedSourceMap; _decodedHookMapCache: Map; _metadataBySource: ?MetadataMap; constructor(sourcemap: MixedSourceMap) { this._sourceMap = sourcemap; this._decodedHookMapCache = new Map(); this._metadataBySource = null; } /** * Returns the Hook name assigned to a given location in the source code, * and a HookMap extracted from an extended source map. * See `getHookNameForLocation` for more details on implementation. * * When used with the `source-map` package, you'll first use * `SourceMapConsumer#originalPositionFor` to retrieve a source location, * then pass that location to `hookNameFor`. */ hookNameFor({ line, column, source, }: {| ...Position, +source: ?string, |}): ?string { if (source == null) { return null; } const hookMap = this._getHookMapForSource(source); if (hookMap == null) { return null; } return getHookNameForLocation({line, column}, hookMap); } hasHookMap(source: ?string): boolean { if (source == null) { return false; } return this._getHookMapForSource(source) != null; } /** * Prepares and caches a lookup table of metadata by source name. */ _getMetadataBySource(): MetadataMap { if (this._metadataBySource == null) { this._metadataBySource = this._getMetadataObjectsBySourceNames( this._sourceMap, ); } return this._metadataBySource; } /** * Collects source metadata from the given map using the current source name * normalization function. Handles both index maps (with sections) and plain * maps. * * NOTE: If any sources are repeated in the map (which shouldn't usually happen, * but is technically possible because of index maps) we only keep the * metadata from the last occurrence of any given source. */ _getMetadataObjectsBySourceNames(sourcemap: MixedSourceMap): MetadataMap { if (sourcemap.mappings === undefined) { const indexSourceMap: IndexSourceMap = sourcemap; const metadataMap = new Map(); indexSourceMap.sections.forEach(section => { const metadataMapForIndexMap = this._getMetadataObjectsBySourceNames( section.map, ); metadataMapForIndexMap.forEach((value, key) => { metadataMap.set(key, value); }); }); return metadataMap; } const metadataMap = new Map(); const basicMap: BasicSourceMap = sourcemap; const updateMap = (metadata: ReactSourceMetadata, sourceIndex: number) => { let source = basicMap.sources[sourceIndex]; if (source != null) { source = normalizeSourcePath(source, basicMap); metadataMap.set(source, metadata); } }; if ( sourcemap.hasOwnProperty(REACT_SOURCES_EXTENSION_KEY) && sourcemap[REACT_SOURCES_EXTENSION_KEY] != null ) { const reactMetadataArray = sourcemap[REACT_SOURCES_EXTENSION_KEY]; reactMetadataArray.filter(Boolean).forEach(updateMap); } else if ( sourcemap.hasOwnProperty(FB_SOURCES_EXTENSION_KEY) && sourcemap[FB_SOURCES_EXTENSION_KEY] != null ) { const fbMetadataArray = sourcemap[FB_SOURCES_EXTENSION_KEY]; if (fbMetadataArray != null) { fbMetadataArray.forEach((fbMetadata, sourceIndex) => { // When extending source maps with React metadata using the // x_facebook_sources field, the position at index 1 on the // metadata tuple is reserved for React metadata const reactMetadata = fbMetadata != null ? fbMetadata[REACT_METADATA_INDEX_IN_FB_METADATA] : null; if (reactMetadata != null) { updateMap(reactMetadata, sourceIndex); } }); } } return metadataMap; } /** * Decodes the function name mappings for the given source if needed, and * retrieves a sorted, searchable array of mappings. */ _getHookMapForSource(source: string): ?HookMap { if (this._decodedHookMapCache.has(source)) { return this._decodedHookMapCache.get(source); } let hookMap = null; const metadataBySource = this._getMetadataBySource(); const normalized = normalizeSourcePath(source, this._sourceMap); const metadata = metadataBySource.get(normalized); if (metadata != null) { const encodedHookMap = metadata[HOOK_MAP_INDEX_IN_REACT_METADATA]; hookMap = encodedHookMap != null ? decodeHookMap(encodedHookMap) : null; } if (hookMap != null) { this._decodedHookMapCache.set(source, hookMap); } return hookMap; } }