main
js 237 lines 6.5 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
8 */
9 import {withSyncPerfMeasurements} from 'react-devtools-shared/src/PerformanceLoggingUtils';
10 import {decode} from '@jridgewell/sourcemap-codec';
11
12 import type {
13 IndexSourceMap,
14 IndexSourceMapSection,
15 BasicSourceMap,
16 MixedSourceMap,
17 } from './SourceMapTypes';
18
19 type SearchPosition = {
20 columnNumber: number,
21 lineNumber: number,
22 };
23
24 type ResultPosition = {
25 column: number,
26 line: number,
27 sourceContent: string | null,
28 sourceURL: string | null,
29 ignored: boolean,
30 };
31
32 export type SourceMapConsumerType = {
33 originalPositionFor: SearchPosition => ResultPosition,
34 };
35
36 type Mappings = Array<Array<Array<number>>>;
37
38 export default function SourceMapConsumer(
39 sourceMapJSON: MixedSourceMap | IndexSourceMapSection,
40 ): SourceMapConsumerType {
41 if (sourceMapJSON.sections != null) {
42 return IndexedSourceMapConsumer(sourceMapJSON as any as IndexSourceMap);
43 } else {
44 return BasicSourceMapConsumer(sourceMapJSON as any as BasicSourceMap);
45 }
46 }
47
48 function BasicSourceMapConsumer(sourceMapJSON: BasicSourceMap) {
49 const decodedMappings: Mappings = withSyncPerfMeasurements(
50 'Decoding source map mappings with @jridgewell/sourcemap-codec',
51 () => decode(sourceMapJSON.mappings),
52 );
53
54 function originalPositionFor({
55 columnNumber,
56 lineNumber,
57 }: SearchPosition): ResultPosition {
58 // Error.prototype.stack columns are 1-based (like most IDEs) but ASTs are 0-based.
59 const targetColumnNumber = columnNumber - 1;
60
61 const lineMappings = decodedMappings[lineNumber - 1];
62
63 let nearestEntry = null;
64
65 let startIndex = 0;
66 let stopIndex = lineMappings.length - 1;
67 let index = -1;
68 while (startIndex <= stopIndex) {
69 index = Math.floor((stopIndex + startIndex) / 2);
70 nearestEntry = lineMappings[index];
71
72 const currentColumn = nearestEntry[0];
73 if (currentColumn === targetColumnNumber) {
74 break;
75 } else {
76 if (currentColumn > targetColumnNumber) {
77 if (stopIndex - index > 0) {
78 stopIndex = index;
79 } else {
80 index = stopIndex;
81 break;
82 }
83 } else {
84 if (index - startIndex > 0) {
85 startIndex = index;
86 } else {
87 index = startIndex;
88 break;
89 }
90 }
91 }
92 }
93
94 // We have found either the exact element, or the next-closest element.
95 // However there may be more than one such element.
96 // Make sure we always return the smallest of these.
97 while (index > 0) {
98 const previousEntry = lineMappings[index - 1];
99 const currentColumn = previousEntry[0];
100 if (currentColumn !== targetColumnNumber) {
101 break;
102 }
103 index--;
104 }
105
106 if (nearestEntry == null) {
107 // TODO maybe fall back to the runtime source instead of throwing?
108 throw Error(
109 `Could not find runtime location for line:${lineNumber} and column:${columnNumber}`,
110 );
111 }
112
113 const sourceIndex = nearestEntry[1];
114 const sourceContent =
115 sourceMapJSON.sourcesContent != null
116 ? sourceMapJSON.sourcesContent[sourceIndex]
117 : null;
118 const sourceURL = sourceMapJSON.sources[sourceIndex] ?? null;
119 const line = nearestEntry[2] + 1;
120 const column = nearestEntry[3];
121 const ignored =
122 sourceMapJSON.ignoreList != null &&
123 sourceMapJSON.ignoreList.includes(sourceIndex);
124 return {
125 column,
126 line,
127 sourceContent: sourceContent as any as string | null,
128 sourceURL: sourceURL as any as string | null,
129 ignored,
130 };
131 }
132
133 return {
134 originalPositionFor,
135 } as any as SourceMapConsumerType;
136 }
137
138 type Section = {
139 +offsetColumn0: number,
140 +offsetLine0: number,
141 +map: BasicSourceMap,
142
143 // Lazily parsed only when/as the section is needed.
144 sourceMapConsumer: SourceMapConsumerType | null,
145 };
146
147 function IndexedSourceMapConsumer(sourceMapJSON: IndexSourceMap) {
148 let lastOffset: {
149 line: number,
150 column: number,
151 ...
152 } = {
153 line: -1,
154 column: 0,
155 };
156
157 const sections: Array<Section> = sourceMapJSON.sections.map(section => {
158 const offset: {
159 line: number,
160 column: number,
161 ...
162 } = section.offset;
163 const offsetLine0 = offset.line;
164 const offsetColumn0 = offset.column;
165
166 if (
167 offsetLine0 < lastOffset.line ||
168 (offsetLine0 === lastOffset.line && offsetColumn0 < lastOffset.column)
169 ) {
170 throw new Error('Section offsets must be ordered and non-overlapping.');
171 }
172
173 lastOffset = offset;
174
175 return {
176 offsetLine0,
177 offsetColumn0,
178 map: section.map,
179 sourceMapConsumer: null,
180 };
181 });
182
183 function originalPositionFor({
184 columnNumber,
185 lineNumber,
186 }: SearchPosition): ResultPosition {
187 // Error.prototype.stack columns are 1-based (like most IDEs) but ASTs are 0-based.
188 const column0 = columnNumber - 1;
189 const line0 = lineNumber - 1;
190
191 // Sections must not overlap and must be sorted: https://tc39.es/source-map/#section-object
192 // Therefore the last section that has an offset less than or equal to the frame is the applicable one.
193 let left = 0;
194 let right = sections.length - 1;
195 let section: Section | null = null;
196
197 while (left <= right) {
198 // fast Math.floor
199 const middle = ~~((left + right) / 2);
200 const currentSection = sections[middle];
201
202 if (
203 currentSection.offsetLine0 < line0 ||
204 (currentSection.offsetLine0 === line0 &&
205 currentSection.offsetColumn0 <= column0)
206 ) {
207 section = currentSection;
208 left = middle + 1;
209 } else {
210 right = middle - 1;
211 }
212 }
213
214 if (section == null) {
215 // TODO maybe fall back to the runtime source instead of throwing?
216 throw Error(
217 `Could not find matching section for line:${lineNumber} and column:${columnNumber}`,
218 );
219 }
220
221 if (section.sourceMapConsumer === null) {
222 // Lazily parse the section only when it's needed.
223 // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions
224 section.sourceMapConsumer = new SourceMapConsumer(section.map);
225 }
226
227 return section.sourceMapConsumer.originalPositionFor({
228 // The mappings in a Source Map section are relative to the section offset.
229 columnNumber: columnNumber - section.offsetColumn0,
230 lineNumber: lineNumber - section.offsetLine0,
231 });
232 }
233
234 return {
235 originalPositionFor,
236 } as any as SourceMapConsumerType;
237 }