main
js 160 lines 4.72 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
10 import SourceMapConsumer from 'react-devtools-shared/src/hooks/SourceMapConsumer';
11
12 import type {ReactFunctionLocation} from 'shared/ReactTypes';
13 import type {FetchFileWithCaching} from 'react-devtools-shared/src/devtools/views/Components/FetchFileWithCachingContext';
14
15 const symbolicationCache: Map<
16 string,
17 Promise<SourceMappedLocation | null>,
18 > = new Map();
19
20 export type SourceMappedLocation = {
21 location: ReactFunctionLocation,
22 ignored: boolean, // Whether the file for this location was ignore listed
23 };
24
25 export function symbolicateSourceWithCache(
26 fetchFileWithCaching: FetchFileWithCaching,
27 sourceURL: string,
28 line: number, // 1-based
29 column: number, // 1-based
30 ): Promise<SourceMappedLocation | null> {
31 const key = `${sourceURL}:${line}:${column}`;
32 const cachedPromise = symbolicationCache.get(key);
33 if (cachedPromise != null) {
34 return cachedPromise;
35 }
36
37 const promise = symbolicateSource(
38 fetchFileWithCaching,
39 sourceURL,
40 line,
41 column,
42 );
43 symbolicationCache.set(key, promise);
44
45 return promise;
46 }
47
48 const SOURCE_MAP_ANNOTATION_PREFIX = 'sourceMappingURL=';
49 export async function symbolicateSource(
50 fetchFileWithCaching: FetchFileWithCaching,
51 sourceURL: string,
52 lineNumber: number, // 1-based
53 columnNumber: number, // 1-based
54 ): Promise<SourceMappedLocation | null> {
55 if (!sourceURL || sourceURL.startsWith('<anonymous')) {
56 return null;
57 }
58 const resource = await fetchFileWithCaching(sourceURL).catch(() => null);
59 if (resource == null) {
60 return null;
61 }
62
63 const resourceLines = resource.split(/[\r\n]+/);
64 for (let i = resourceLines.length - 1; i >= 0; --i) {
65 const resourceLine = resourceLines[i];
66
67 // In case there is empty last line
68 if (!resourceLine) continue;
69 // Not an annotation? Stop looking for a source mapping url.
70 if (!resourceLine.startsWith('//#')) break;
71
72 if (resourceLine.includes(SOURCE_MAP_ANNOTATION_PREFIX)) {
73 const sourceMapAnnotationStartIndex = resourceLine.indexOf(
74 SOURCE_MAP_ANNOTATION_PREFIX,
75 );
76 const sourceMapAt = resourceLine.slice(
77 sourceMapAnnotationStartIndex + SOURCE_MAP_ANNOTATION_PREFIX.length,
78 resourceLine.length,
79 );
80
81 // Compute the absolute source map URL. If the base URL is invalid, gracefully bail.
82 let sourceMapURL;
83 try {
84 sourceMapURL = new URL(sourceMapAt, sourceURL).toString();
85 } catch (e) {
86 // Fallback: try if sourceMapAt is already an absolute URL; otherwise give up.
87 try {
88 sourceMapURL = new URL(sourceMapAt).toString();
89 } catch (_e) {
90 return null;
91 }
92 }
93 const sourceMap = await fetchFileWithCaching(sourceMapURL).catch(
94 () => null,
95 );
96 if (sourceMap != null) {
97 try {
98 const parsedSourceMap = JSON.parse(sourceMap);
99 const consumer = SourceMapConsumer(parsedSourceMap);
100 const functionName = ''; // TODO: Parse function name from sourceContent.
101 const {
102 sourceURL: possiblyURL,
103 line,
104 column: columnZeroBased,
105 ignored,
106 } = consumer.originalPositionFor({
107 lineNumber, // 1-based
108 columnNumber, // 1-based
109 });
110
111 const column = columnZeroBased + 1;
112
113 if (possiblyURL === null) {
114 return null;
115 }
116 try {
117 // sourceMapURL = https://react.dev/script.js.map
118 void new URL(possiblyURL); // test if it is a valid URL
119
120 return {
121 location: [functionName, possiblyURL, line, column],
122 ignored,
123 };
124 } catch (e) {
125 // This is not valid URL
126 if (
127 // sourceMapURL = /file
128 possiblyURL.startsWith('/') ||
129 // sourceMapURL = C:\\...
130 possiblyURL.slice(1).startsWith(':\\\\')
131 ) {
132 // This is an absolute path
133 return {
134 location: [functionName, possiblyURL, line, column],
135 ignored,
136 };
137 }
138
139 // This is a relative path
140 // possiblyURL = x.js.map, sourceMapURL = https://react.dev/script.js.map
141 const absoluteSourcePath = new URL(
142 possiblyURL,
143 sourceMapURL,
144 ).toString();
145 return {
146 location: [functionName, absoluteSourcePath, line, column],
147 ignored,
148 };
149 }
150 } catch (e) {
151 return null;
152 }
153 }
154
155 return null;
156 }
157 }
158
159 return null;
160 }