|
| 1 | +/* |
| 2 | + * Copyright (c) Facebook, Inc. and its 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/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 source-map@0.8.0-beta.0's SourceMapConsumer. |
| 31 | + * By default, source names are normalized using the same logic that the |
| 32 | + * `source-map@0.8.0-beta.0` package uses internally. This is crucial for keeping the |
| 33 | + * sources list in sync with a `SourceMapConsumer` instance. |
| 34 | + */ |
| 35 | +function normalizeSourcePath( |
| 36 | + sourceInput: string, |
| 37 | + map: {+sourceRoot?: ?string, ...}, |
| 38 | +): string { |
| 39 | + const {sourceRoot} = map; |
| 40 | + let source = sourceInput; |
| 41 | + |
| 42 | + // eslint-disable-next-line react-internal/no-primitive-constructors |
| 43 | + source = String(source); |
| 44 | + return util.computeSourceURL(sourceRoot, source); |
| 45 | +} |
| 46 | + |
| 47 | +/** |
| 48 | + * Consumes the `x_react_sources` or `x_facebook_sources` metadata field from a |
| 49 | + * source map and exposes ways to query the React DevTools specific metadata |
| 50 | + * included in those fields. |
| 51 | + */ |
| 52 | +export class SourceMapMetadataConsumer { |
| 53 | + _sourceMap: MixedSourceMap; |
| 54 | + _decodedHookMapCache: Map<string, HookMap>; |
| 55 | + _metadataBySource: ?MetadataMap; |
| 56 | + |
| 57 | + constructor(sourcemap: MixedSourceMap) { |
| 58 | + this._sourceMap = sourcemap; |
| 59 | + this._decodedHookMapCache = new Map(); |
| 60 | + this._metadataBySource = null; |
| 61 | + } |
| 62 | + |
| 63 | + /** |
| 64 | + * Returns the Hook name assigned to a given location in the source code, |
| 65 | + * and a HookMap extracted from an extended source map. |
| 66 | + * See `getHookNameForLocation` for more details on implementation. |
| 67 | + * |
| 68 | + * When used with the `source-map` package, you'll first use |
| 69 | + * `SourceMapConsumer#originalPositionFor` to retrieve a source location, |
| 70 | + * then pass that location to `hookNameFor`. |
| 71 | + */ |
| 72 | + hookNameFor({ |
| 73 | + line, |
| 74 | + column, |
| 75 | + source, |
| 76 | + }: {| |
| 77 | + ...Position, |
| 78 | + +source: ?string, |
| 79 | + |}): ?string { |
| 80 | + if (source == null) { |
| 81 | + return null; |
| 82 | + } |
| 83 | + |
| 84 | + const hookMap = this._getHookMapForSource(source); |
| 85 | + if (hookMap == null) { |
| 86 | + return null; |
| 87 | + } |
| 88 | + |
| 89 | + return getHookNameForLocation({line, column}, hookMap); |
| 90 | + } |
| 91 | + |
| 92 | + hasHookMap(source: ?string) { |
| 93 | + if (source == null) { |
| 94 | + return null; |
| 95 | + } |
| 96 | + return this._getHookMapForSource(source) != null; |
| 97 | + } |
| 98 | + |
| 99 | + /** |
| 100 | + * Prepares and caches a lookup table of metadata by source name. |
| 101 | + */ |
| 102 | + _getMetadataBySource(): MetadataMap { |
| 103 | + if (this._metadataBySource == null) { |
| 104 | + this._metadataBySource = this._getMetadataObjectsBySourceNames( |
| 105 | + this._sourceMap, |
| 106 | + ); |
| 107 | + } |
| 108 | + |
| 109 | + return this._metadataBySource; |
| 110 | + } |
| 111 | + |
| 112 | + /** |
| 113 | + * Collects source metadata from the given map using the current source name |
| 114 | + * normalization function. Handles both index maps (with sections) and plain |
| 115 | + * maps. |
| 116 | + * |
| 117 | + * NOTE: If any sources are repeated in the map (which shouldn't usually happen, |
| 118 | + * but is technically possible because of index maps) we only keep the |
| 119 | + * metadata from the last occurrence of any given source. |
| 120 | + */ |
| 121 | + _getMetadataObjectsBySourceNames(sourcemap: MixedSourceMap): MetadataMap { |
| 122 | + if (sourcemap.mappings === undefined) { |
| 123 | + const indexSourceMap: IndexSourceMap = sourcemap; |
| 124 | + const metadataMap = new Map(); |
| 125 | + indexSourceMap.sections.forEach(section => { |
| 126 | + const metadataMapForIndexMap = this._getMetadataObjectsBySourceNames( |
| 127 | + section.map, |
| 128 | + ); |
| 129 | + metadataMapForIndexMap.forEach((value, key) => { |
| 130 | + metadataMap.set(key, value); |
| 131 | + }); |
| 132 | + }); |
| 133 | + return metadataMap; |
| 134 | + } |
| 135 | + |
| 136 | + const metadataMap = new Map(); |
| 137 | + const basicMap: BasicSourceMap = sourcemap; |
| 138 | + const updateMap = (metadata: ReactSourceMetadata, sourceIndex: number) => { |
| 139 | + let source = basicMap.sources[sourceIndex]; |
| 140 | + if (source != null) { |
| 141 | + source = normalizeSourcePath(source, basicMap); |
| 142 | + metadataMap.set(source, metadata); |
| 143 | + } |
| 144 | + }; |
| 145 | + |
| 146 | + if ( |
| 147 | + sourcemap.hasOwnProperty(REACT_SOURCES_EXTENSION_KEY) && |
| 148 | + sourcemap[REACT_SOURCES_EXTENSION_KEY] != null |
| 149 | + ) { |
| 150 | + const reactMetadataArray = sourcemap[REACT_SOURCES_EXTENSION_KEY]; |
| 151 | + reactMetadataArray.filter(Boolean).forEach(updateMap); |
| 152 | + } else if ( |
| 153 | + sourcemap.hasOwnProperty(FB_SOURCES_EXTENSION_KEY) && |
| 154 | + sourcemap[FB_SOURCES_EXTENSION_KEY] != null |
| 155 | + ) { |
| 156 | + const fbMetadataArray = sourcemap[FB_SOURCES_EXTENSION_KEY]; |
| 157 | + if (fbMetadataArray != null) { |
| 158 | + fbMetadataArray.forEach((fbMetadata, sourceIndex) => { |
| 159 | + // When extending source maps with React metadata using the |
| 160 | + // x_facebook_sources field, the position at index 1 on the |
| 161 | + // metadata tuple is reserved for React metadata |
| 162 | + const reactMetadata = |
| 163 | + fbMetadata != null |
| 164 | + ? fbMetadata[REACT_METADATA_INDEX_IN_FB_METADATA] |
| 165 | + : null; |
| 166 | + if (reactMetadata != null) { |
| 167 | + updateMap(reactMetadata, sourceIndex); |
| 168 | + } |
| 169 | + }); |
| 170 | + } |
| 171 | + } |
| 172 | + |
| 173 | + return metadataMap; |
| 174 | + } |
| 175 | + |
| 176 | + /** |
| 177 | + * Decodes the function name mappings for the given source if needed, and |
| 178 | + * retrieves a sorted, searchable array of mappings. |
| 179 | + */ |
| 180 | + _getHookMapForSource(source: string): ?HookMap { |
| 181 | + if (this._decodedHookMapCache.has(source)) { |
| 182 | + return this._decodedHookMapCache.get(source); |
| 183 | + } |
| 184 | + let hookMap = null; |
| 185 | + const metadataBySource = this._getMetadataBySource(); |
| 186 | + const normalized = normalizeSourcePath(source, this._sourceMap); |
| 187 | + const metadata = metadataBySource.get(normalized); |
| 188 | + if (metadata != null) { |
| 189 | + const encodedHookMap = metadata[HOOK_MAP_INDEX_IN_REACT_METADATA]; |
| 190 | + hookMap = encodedHookMap != null ? decodeHookMap(encodedHookMap) : null; |
| 191 | + } |
| 192 | + if (hookMap != null) { |
| 193 | + this._decodedHookMapCache.set(source, hookMap); |
| 194 | + } |
| 195 | + return hookMap; |
| 196 | + } |
| 197 | +} |
0 commit comments