main
js 207 lines 6.81 KB
Raw
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 }