| 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 source-map-js@0.6.2's SourceMapConsumer. |
| 31 | * By default, source names are normalized using the same logic that the `source-map-js@0.6.2` 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 | } |