-
Notifications
You must be signed in to change notification settings - Fork 47.2k
Commit
…fferent formats (#22096) ## Summary Follow up from #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 #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).
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
/* | ||
* 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<string, ?ReactSourceMetadata>; | ||
|
||
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<string, HookMap>; | ||
_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) { | ||
if (source == null) { | ||
return null; | ||
} | ||
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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
/* | ||
* 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 { | ||
BasicSourceMap, | ||
MixedSourceMap, | ||
IndexSourceMap, | ||
} from './SourceMapTypes'; | ||
|
||
export function sourceMapIncludesSource( | ||
sourcemap: MixedSourceMap, | ||
source: ?string, | ||
): boolean { | ||
if (source == null) { | ||
return false; | ||
} | ||
if (sourcemap.mappings === undefined) { | ||
const indexSourceMap: IndexSourceMap = sourcemap; | ||
return indexSourceMap.sections.some(section => { | ||
return sourceMapIncludesSource(section.map, source); | ||
}); | ||
} | ||
|
||
const basicMap: BasicSourceMap = sourcemap; | ||
return basicMap.sources.some( | ||
s => s === 'Inline Babel script' || source.endsWith(s), | ||
); | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.