0% found this document useful (0 votes)
9 views

Source Map Metadata Consumer

This class consumes React source map metadata to allow querying for Hook names and metadata by source location. It handles decoding and caching of Hook maps, as well as normalization of source names for lookup.

Uploaded by

mahoraga
Copyright
© © All Rights Reserved
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
9 views

Source Map Metadata Consumer

This class consumes React source map metadata to allow querying for Hook names and metadata by source location. It handles decoding and caching of Hook maps, as well as normalization of source names for lookup.

Uploaded by

mahoraga
Copyright
© © All Rights Reserved
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
You are on page 1/ 4

/*

* Copyright (c) Meta Platforms, Inc. and 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-js/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 [email protected]'s SourceMapConsumer.
* By default, source names are normalized using the same logic that the `source-
[email protected]` 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;

source = String(source);
// Some source maps produce relative source paths like "./foo.js" instead of
// "foo.js". Normalize these first so that future comparisons will succeed.
// See bugzil.la/1090768.
source = util.normalize(source);
// Always ensure that absolute sources are internally stored relative to
// the source root, if the source root is absolute. Not doing this would
// be particularly problematic when the source root is a prefix of the
// source (valid, but why??). See github issue #199 and bugzil.la/1188982.
source =
sourceRoot != null && util.isAbsolute(sourceRoot) && util.isAbsolute(source)
? util.relative(sourceRoot, source)
: 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): boolean {


if (source == null) {
return false;
}
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<string, ?ReactSourceMetadata>();
indexSourceMap.sections.forEach(section => {
const metadataMapForIndexMap = this._getMetadataObjectsBySourceNames(
section.map,
);
metadataMapForIndexMap.forEach((value, key) => {
metadataMap.set(key, value);
});
});
return metadataMap;
}

const metadataMap: 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;
}
}

You might also like