main
js 286 lines 8.32 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 JSON5 from 'json5';
11
12 import type {ReactFunctionLocation} from 'shared/ReactTypes';
13 import {ElementTypeActivity} from 'react-devtools-shared/src/frontend/types';
14 import type {
15 Element,
16 SuspenseNode,
17 } from 'react-devtools-shared/src/frontend/types';
18 import type {StateContext} from './views/Components/TreeContext';
19 import type Store from './store';
20
21 export function printElement(
22 element: Element,
23 includeWeight: boolean = false,
24 ): string {
25 let prefix = ' ';
26 if (element.children.length > 0) {
27 prefix = element.isCollapsed ? '' : '';
28 }
29
30 let key = '';
31 if (element.key !== null) {
32 key = ` key="${element.key}"`;
33 }
34
35 let name = '';
36 if (element.nameProp !== null) {
37 name = ` name="${element.nameProp}"`;
38 }
39
40 let hocDisplayNames = null;
41 if (element.hocDisplayNames !== null) {
42 hocDisplayNames = [...element.hocDisplayNames];
43 }
44
45 const hocs =
46 hocDisplayNames === null ? '' : ` [${hocDisplayNames.join('][')}]`;
47
48 let mode = '';
49 if (element.type === ElementTypeActivity) {
50 mode = ` mode="${element.isActivityHidden ? 'hidden' : 'visible'}"`;
51 }
52
53 let suffix = '';
54 if (includeWeight) {
55 suffix = ` (${element.isCollapsed ? 1 : element.weight})`;
56 }
57
58 return `${' '.repeat(element.depth + 1)}${prefix} <${
59 element.displayName || 'null'
60 }${key}${name}${mode}>${hocs}${suffix}`;
61 }
62
63 function printRects(rects: SuspenseNode['rects']): string {
64 if (rects === null) {
65 return ' rects={null}';
66 } else {
67 return ` rects={[${rects.map(rect => `{x:${rect.x},y:${rect.y},width:${rect.width},height:${rect.height}}`).join(', ')}]}`;
68 }
69 }
70
71 function printSuspense(suspense: SuspenseNode): string {
72 const name = ` name="${suspense.name || 'Unknown'}"`;
73 const hasUniqueSuspenders = ` uniqueSuspenders={${suspense.hasUniqueSuspenders ? 'true' : 'false'}}`;
74 const printedRects = printRects(suspense.rects);
75
76 return `<Suspense${name}${hasUniqueSuspenders}${printedRects}>`;
77 }
78
79 function printSuspenseWithChildren(
80 store: Store,
81 suspense: SuspenseNode,
82 depth: number,
83 ): Array<string> {
84 const lines = [' '.repeat(depth) + printSuspense(suspense)];
85 for (let i = 0; i < suspense.children.length; i++) {
86 const childID = suspense.children[i];
87 const child = store.getSuspenseByID(childID);
88 if (child === null) {
89 throw new Error(`Could not find Suspense node with ID "${childID}".`);
90 }
91 lines.push(...printSuspenseWithChildren(store, child, depth + 1));
92 }
93
94 return lines;
95 }
96
97 export function printOwnersList(
98 elements: Array<Element>,
99 includeWeight: boolean = false,
100 ): string {
101 return elements
102 .map(element => printElement(element, includeWeight))
103 .join('\n');
104 }
105
106 export function printStore(
107 store: Store,
108 includeWeight: boolean = false,
109 state: StateContext | null = null,
110 includeSuspense: boolean = true,
111 ): string {
112 const snapshotLines = [];
113
114 let rootWeight = 0;
115
116 function printSelectedMarker(index: number): string {
117 if (state === null) {
118 return '';
119 }
120 return state.inspectedElementIndex === index ? `→` : ' ';
121 }
122
123 function printErrorsAndWarnings(element: Element): string {
124 const {errorCount, warningCount} =
125 store.getErrorAndWarningCountForElementID(element.id);
126 if (errorCount === 0 && warningCount === 0) {
127 return '';
128 }
129 return ` ${errorCount > 0 ? '' : ''}${warningCount > 0 ? '' : ''}`;
130 }
131
132 const ownerFlatTree = state !== null ? state.ownerFlatTree : null;
133 if (ownerFlatTree !== null) {
134 snapshotLines.push(
135 '[owners]' + (includeWeight ? ` (${ownerFlatTree.length})` : ''),
136 );
137 ownerFlatTree.forEach((element, index) => {
138 const printedSelectedMarker = printSelectedMarker(index);
139 const printedElement = printElement(element, false);
140 const printedErrorsAndWarnings = printErrorsAndWarnings(element);
141 snapshotLines.push(
142 `${printedSelectedMarker}${printedElement}${printedErrorsAndWarnings}`,
143 );
144 });
145 } else {
146 const errorsAndWarnings = store._errorsAndWarnings;
147 if (errorsAndWarnings.size > 0) {
148 let errorCount = 0;
149 let warningCount = 0;
150 errorsAndWarnings.forEach(entry => {
151 errorCount += entry.errorCount;
152 warningCount += entry.warningCount;
153 });
154
155 snapshotLines.push(`✕ ${errorCount}, ⚠ ${warningCount}`);
156 }
157
158 store.roots.forEach(rootID => {
159 const {weight} = store.getElementByID(rootID) as any as Element;
160 const maybeWeightLabel = includeWeight ? ` (${weight})` : '';
161
162 // Store does not (yet) expose a way to get errors/warnings per root.
163 snapshotLines.push(`[root]${maybeWeightLabel}`);
164
165 for (let i = rootWeight; i < rootWeight + weight; i++) {
166 const element = store.getElementAtIndex(i);
167
168 if (element == null) {
169 throw Error(`Could not find element at index "${i}"`);
170 }
171
172 const printedSelectedMarker = printSelectedMarker(i);
173 const printedElement = printElement(element, includeWeight);
174 const printedErrorsAndWarnings = printErrorsAndWarnings(element);
175 snapshotLines.push(
176 `${printedSelectedMarker}${printedElement}${printedErrorsAndWarnings}`,
177 );
178 }
179
180 rootWeight += weight;
181
182 if (includeSuspense) {
183 const root = store.getSuspenseByID(rootID);
184 // Roots from legacy renderers don't have a separate Suspense tree
185 if (root !== null) {
186 if (root.children.length > 0) {
187 snapshotLines.push('[suspense-root] ' + printRects(root.rects));
188 for (let i = 0; i < root.children.length; i++) {
189 const childID = root.children[i];
190 const child = store.getSuspenseByID(childID);
191 if (child === null) {
192 throw new Error(
193 `Could not find Suspense node with ID "${childID}".`,
194 );
195 }
196 snapshotLines.push(...printSuspenseWithChildren(store, child, 1));
197 }
198 }
199 }
200 }
201 });
202
203 // Make sure the pretty-printed test align with the Store's reported number of total rows.
204 if (rootWeight !== store.numElements) {
205 throw Error(
206 `Inconsistent Store state. Individual root weights ("${rootWeight}") do not match total weight ("${store.numElements}")`,
207 );
208 }
209
210 // If roots have been unmounted, verify that they've been removed from maps.
211 // This helps ensure the Store doesn't leak memory.
212 store.assertExpectedRootMapSizes();
213 }
214
215 return snapshotLines.join('\n');
216 }
217
218 // We use JSON.parse to parse string values
219 // e.g. 'foo' is not valid JSON but it is a valid string
220 // so this method replaces e.g. 'foo' with "foo"
221 export function sanitizeForParse(value: any): any | string {
222 if (typeof value === 'string') {
223 if (
224 value.length >= 2 &&
225 value.charAt(0) === "'" &&
226 value.charAt(value.length - 1) === "'"
227 ) {
228 return '"' + value.slice(1, value.length - 1) + '"';
229 }
230 }
231 return value;
232 }
233
234 export function smartParse(value: any): any | void | number {
235 switch (value) {
236 case 'Infinity':
237 return Infinity;
238 case '-Infinity':
239 return -Infinity;
240 case 'NaN':
241 return NaN;
242 case 'undefined':
243 return undefined;
244 default:
245 return JSON5.parse(sanitizeForParse(value));
246 }
247 }
248
249 export function smartStringify(value: any): string {
250 if (typeof value === 'number') {
251 if (Number.isNaN(value)) {
252 return 'NaN';
253 } else if (!Number.isFinite(value)) {
254 return value > 0 ? 'Infinity' : '-Infinity';
255 }
256 } else if (value === undefined) {
257 return 'undefined';
258 }
259
260 return JSON.stringify(value);
261 }
262
263 const STACK_DELIMETER = /\n\s+at /;
264 const STACK_SOURCE_LOCATION = /([^\s]+) \((.+):(.+):(.+)\)/;
265
266 export function stackToComponentLocations(
267 stack: string,
268 ): Array<[string, ?ReactFunctionLocation]> {
269 const out: Array<[string, ?ReactFunctionLocation]> = [];
270 stack
271 .split(STACK_DELIMETER)
272 .slice(1)
273 .forEach(entry => {
274 const match = STACK_SOURCE_LOCATION.exec(entry);
275 if (match) {
276 const [, component, url, row, column] = match;
277 out.push([
278 component,
279 [component, url, parseInt(row, 10), parseInt(column, 10)],
280 ]);
281 } else {
282 out.push([entry, null]);
283 }
284 });
285 return out;
286 }