Skip to content

Commit 8a8cf76

Browse files
Juanzhengjitf
Juan
authored andcommitted
[DevTools] Access metadata in source maps correctly accounting for different formats (facebook#22096)
## Summary Follow up from facebook#22010. The initial implementation of named hooks and for looking up hook name metadata in an extended source map both assumed that the source maps would always have a `sources` field available, and didn't account for the source maps in the [Index Map](https://sourcemaps.info/spec.html#h.535es3xeprgt) format, which contain a list of `sections` and don't have the `source` field available directly. In order to properly access metadata in extended source maps, this commit: - Adds a new `SourceMapMetadataConsumer` api, which is a fork / very similar in structure to the corresponding [consumer in Metro](https://github.com/facebook/metro/blob/2b44ec39b4bca93e3e1cf1f268b4be66f894924a/packages/metro-symbolicate/src/SourceMetadataMapConsumer.js#L56) (as specified by @motiz88 in facebook#21782. - Updates `parseHookNames` to use this new api ## Test Plan - yarn flow - yarn test - yarn test-build-devtools - added new regression tests covering the index map format - named hooks still work on manual test of browser extension on a few different apps (code sandbox, create-react-app, internally).
1 parent 6dcbee1 commit 8a8cf76

File tree

105 files changed

+3505
-160
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

105 files changed

+3505
-160
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
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+
}

packages/react-devtools-extensions/src/SourceMapTypes.js

+5-5
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ export type BasicSourceMap = {|
2727
+x_react_sources?: ReactSourcesArray,
2828
|};
2929

30-
export type IndexMapSection = {
31-
map: IndexMap | BasicSourceMap,
30+
export type IndexSourceMapSection = {
31+
map: IndexSourceMap | BasicSourceMap,
3232
offset: {
3333
line: number,
3434
column: number,
@@ -37,14 +37,14 @@ export type IndexMapSection = {
3737
...
3838
};
3939

40-
export type IndexMap = {|
40+
export type IndexSourceMap = {|
4141
+file?: string,
4242
+mappings?: void, // avoids SourceMap being a disjoint union
4343
+sourcesContent?: void,
44-
+sections: Array<IndexMapSection>,
44+
+sections: Array<IndexSourceMapSection>,
4545
+version: number,
4646
+x_facebook_sources?: FBSourcesArray,
4747
+x_react_sources?: ReactSourcesArray,
4848
|};
4949

50-
export type MixedSourceMap = IndexMap | BasicSourceMap;
50+
export type MixedSourceMap = IndexSourceMap | BasicSourceMap;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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 {
11+
BasicSourceMap,
12+
MixedSourceMap,
13+
IndexSourceMap,
14+
} from './SourceMapTypes';
15+
16+
export function sourceMapIncludesSource(
17+
sourcemap: MixedSourceMap,
18+
source: ?string,
19+
): boolean {
20+
if (source == null) {
21+
return false;
22+
}
23+
if (sourcemap.mappings === undefined) {
24+
const indexSourceMap: IndexSourceMap = sourcemap;
25+
return indexSourceMap.sections.some(section => {
26+
return sourceMapIncludesSource(section.map, source);
27+
});
28+
}
29+
30+
const basicMap: BasicSourceMap = sourcemap;
31+
return basicMap.sources.some(
32+
s => s === 'Inline Babel script' || source.endsWith(s),
33+
);
34+
}

packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/fb-sources-extended/index-map/ComponentUsingHooksIndirectly.js

+45
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/fb-sources-extended/index-map/ComponentUsingHooksIndirectly.js.map

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/fb-sources-extended/index-map/ComponentWithCustomHook.js

+41
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/fb-sources-extended/index-map/ComponentWithCustomHook.js.map

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/fb-sources-extended/index-map/ComponentWithExternalCustomHooks.js

+26
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/react-devtools-extensions/src/__tests__/__source__/__compiled__/external/fb-sources-extended/index-map/ComponentWithExternalCustomHooks.js.map

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)