SourceMetadataMapConsumer.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  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
  8. * @format
  9. */
  10. 'use strict';
  11. const vlq = require('vlq');
  12. const {normalizeSourcePath} = require('metro-source-map');
  13. import type {
  14. MixedSourceMap,
  15. FBSourcesArray,
  16. FBSourceFunctionMap,
  17. FBSourceMetadata,
  18. BasicSourceMap,
  19. IndexMap,
  20. } from 'metro-source-map';
  21. const METADATA_FIELD_FUNCTIONS = 0;
  22. type Position = {
  23. +line: number,
  24. +column: number,
  25. ...
  26. };
  27. type FunctionMapping = {
  28. +line: number,
  29. +column: number,
  30. +name: string,
  31. ...
  32. };
  33. type SourceNameNormalizer = (string, {+sourceRoot?: ?string, ...}) => string;
  34. type MetadataMap = {[source: string]: ?FBSourceMetadata, ...};
  35. /**
  36. * Consumes the `x_facebook_sources` metadata field from a source map and
  37. * exposes various queries on it.
  38. *
  39. * By default, source names are normalized using the same logic that the
  40. * `source-map@0.5.6` package uses internally. This is crucial for keeping the
  41. * sources list in sync with a `SourceMapConsumer` instance.
  42. * If you're using this with a different source map reader (e.g. one that
  43. * doesn't normalize source names at all), you can switch out the normalization
  44. * function in the constructor, e.g.
  45. *
  46. * new SourceMetadataMapConsumer(map, source => source) // Don't normalize
  47. */
  48. class SourceMetadataMapConsumer {
  49. constructor(
  50. map: MixedSourceMap,
  51. normalizeSourceFn: SourceNameNormalizer = normalizeSourcePath,
  52. ) {
  53. this._sourceMap = map;
  54. this._decodedFunctionMapCache = new Map();
  55. this._normalizeSource = normalizeSourceFn;
  56. }
  57. _sourceMap: MixedSourceMap;
  58. _decodedFunctionMapCache: Map<string, ?$ReadOnlyArray<FunctionMapping>>;
  59. _normalizeSource: SourceNameNormalizer;
  60. _metadataBySource: ?MetadataMap;
  61. /**
  62. * Retrieves a human-readable name for the function enclosing a particular
  63. * source location.
  64. *
  65. * When used with the `source-map` package, you'll first use
  66. * `SourceMapConsumer#originalPositionFor` to retrieve a source location,
  67. * then pass that location to `functionNameFor`.
  68. */
  69. functionNameFor({
  70. line,
  71. column,
  72. source,
  73. }: Position & {+source: ?string, ...}): ?string {
  74. if (source && line != null && column != null) {
  75. const mappings = this._getFunctionMappings(source);
  76. if (mappings) {
  77. const mapping = findEnclosingMapping(mappings, {line, column});
  78. if (mapping) {
  79. return mapping.name;
  80. }
  81. }
  82. }
  83. return null;
  84. }
  85. /**
  86. * Returns this map's source metadata as a new array with the same order as
  87. * `sources`.
  88. *
  89. * This array can be used as the `x_facebook_sources` field of a map whose
  90. * `sources` field is the array that was passed into this method.
  91. */
  92. toArray(sources: $ReadOnlyArray<string>): FBSourcesArray {
  93. const metadataBySource = this._getMetadataBySource();
  94. const encoded = [];
  95. for (const source of sources) {
  96. encoded.push(metadataBySource[source] || null);
  97. }
  98. return encoded;
  99. }
  100. /**
  101. * Prepares and caches a lookup table of metadata by source name.
  102. */
  103. _getMetadataBySource(): MetadataMap {
  104. if (!this._metadataBySource) {
  105. this._metadataBySource = this._getMetadataObjectsBySourceNames(
  106. this._sourceMap,
  107. );
  108. }
  109. return this._metadataBySource;
  110. }
  111. /**
  112. * Decodes the function name mappings for the given source if needed, and
  113. * retrieves a sorted, searchable array of mappings.
  114. */
  115. _getFunctionMappings(source: string): ?$ReadOnlyArray<FunctionMapping> {
  116. if (this._decodedFunctionMapCache.has(source)) {
  117. return this._decodedFunctionMapCache.get(source);
  118. }
  119. let parsedFunctionMap = null;
  120. const metadataBySource = this._getMetadataBySource();
  121. if (Object.prototype.hasOwnProperty.call(metadataBySource, source)) {
  122. const metadata = metadataBySource[source] || [];
  123. parsedFunctionMap = decodeFunctionMap(metadata[METADATA_FIELD_FUNCTIONS]);
  124. }
  125. this._decodedFunctionMapCache.set(source, parsedFunctionMap);
  126. return parsedFunctionMap;
  127. }
  128. /**
  129. * Collects source metadata from the given map using the current source name
  130. * normalization function. Handles both index maps (with sections) and plain
  131. * maps.
  132. *
  133. * NOTE: If any sources are repeated in the map (which shouldn't happen in
  134. * Metro, but is technically possible because of index maps) we only keep the
  135. * metadata from the last occurrence of any given source.
  136. */
  137. _getMetadataObjectsBySourceNames(map: MixedSourceMap): MetadataMap {
  138. // eslint-disable-next-line lint/strictly-null
  139. if (map.mappings === undefined) {
  140. const indexMap: IndexMap = map;
  141. return Object.assign(
  142. {},
  143. ...indexMap.sections.map(section =>
  144. this._getMetadataObjectsBySourceNames(section.map),
  145. ),
  146. );
  147. }
  148. if ('x_facebook_sources' in map) {
  149. const basicMap: BasicSourceMap = map;
  150. return (basicMap.x_facebook_sources || []).reduce(
  151. (acc, metadata, index) => {
  152. let source = basicMap.sources[index];
  153. if (source != null) {
  154. source = this._normalizeSource(source, basicMap);
  155. acc[source] = metadata;
  156. }
  157. return acc;
  158. },
  159. {},
  160. );
  161. }
  162. return {};
  163. }
  164. }
  165. function decodeFunctionMap(
  166. functionMap: ?FBSourceFunctionMap,
  167. ): $ReadOnlyArray<FunctionMapping> {
  168. if (!functionMap) {
  169. return [];
  170. }
  171. const parsed = [];
  172. let line = 1;
  173. let nameIndex = 0;
  174. for (const lineMappings of functionMap.mappings.split(';')) {
  175. let column = 0;
  176. for (const mapping of lineMappings.split(',')) {
  177. const [columnDelta, nameDelta, lineDelta = 0] = vlq.decode(mapping);
  178. line += lineDelta;
  179. nameIndex += nameDelta;
  180. column += columnDelta;
  181. parsed.push({line, column, name: functionMap.names[nameIndex]});
  182. }
  183. }
  184. return parsed;
  185. }
  186. function findEnclosingMapping(
  187. mappings: $ReadOnlyArray<FunctionMapping>,
  188. target: Position,
  189. ): ?FunctionMapping {
  190. let first = 0;
  191. let it = 0;
  192. let count = mappings.length;
  193. let step;
  194. while (count > 0) {
  195. it = first;
  196. step = Math.floor(count / 2);
  197. it += step;
  198. if (comparePositions(target, mappings[it]) >= 0) {
  199. first = ++it;
  200. count -= step + 1;
  201. } else {
  202. count = step;
  203. }
  204. }
  205. return first ? mappings[first - 1] : null;
  206. }
  207. function comparePositions(a: Position, b: Position): number {
  208. if (a.line === b.line) {
  209. return a.column - b.column;
  210. }
  211. return a.line - b.line;
  212. }
  213. module.exports = SourceMetadataMapConsumer;