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
- */
9
-
10
-import {parse} from '@babel/parser';
11
-import LRU from 'lru-cache';
12
-import {SourceMapConsumer} from 'source-map-js';
13
-import {getHookName} from '../astUtils';
14
-import {areSourceMapsAppliedToErrors} from '../ErrorTester';
15
-import {__DEBUG__} from 'react-devtools-shared/src/constants';
16
-import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache';
17
-import {sourceMapIncludesSource} from '../SourceMapUtils';
18
-import {SourceMapMetadataConsumer} from '../SourceMapMetadataConsumer';
19
-import {
20
- withAsyncPerformanceMark,
21
- withCallbackPerformanceMark,
22
- withSyncPerformanceMark,
23
-} from 'react-devtools-shared/src/PerformanceMarks';
24
-
25
-import type {
26
- HooksNode,
27
- HookSource,
28
- HooksTree,
29
-} from 'react-debug-tools/src/ReactDebugHooks';
30
-import type {HookNames, LRUCache} from 'react-devtools-shared/src/types';
31
-import type {SourceConsumer} from '../astUtils';
32
-
33
-const MAX_SOURCE_LENGTH = 100_000_000;
34
-
35
-type AST = mixed;
36
-
37
-type HookSourceData = {|
38
- // Generated by react-debug-tools.
39
- hookSource: HookSource,
40
-
41
- // API for consuming metadfata present in extended source map.
42
- metadataConsumer: SourceMapMetadataConsumer | null,
43
-
44
- // AST for original source code; typically comes from a consumed source map.
45
- originalSourceAST: AST | null,
46
-
47
- // Source code (React components or custom hooks) containing primitive hook calls.
48
- // If no source map has been provided, this code will be the same as runtimeSourceCode.
49
- originalSourceCode: string | null,
50
-
51
- // Original source URL if there is a source map, or the same as runtimeSourceURL.
52
- originalSourceURL: string | null,
53
-
54
- // Compiled code (React components or custom hooks) containing primitive hook calls.
55
- runtimeSourceCode: string | null,
56
-
57
- // Same as hookSource.fileName but guaranteed to be non-null.
58
- runtimeSourceURL: string,
59
-
60
- // APIs from source-map for parsing source maps (if detected).
61
- sourceConsumer: SourceConsumer | null,
62
-
63
- // External URL of source map.
64
- // Sources without source maps (or with inline source maps) won't have this.
65
- sourceMapURL: string | null,
66
-|};
67
-
68
-type CachedRuntimeCodeMetadata = {|
69
- sourceConsumer: SourceConsumer | null,
70
- metadataConsumer: SourceMapMetadataConsumer | null,
71
-|};
72
-
73
-const runtimeURLToMetadataCache: LRUCache<
74
- string,
75
- CachedRuntimeCodeMetadata,
76
-> = new LRU({
77
- max: 50,
78
- dispose: (runtimeSourceURL: string, metadata: CachedRuntimeCodeMetadata) => {
79
- if (__DEBUG__) {
80
- console.log(
81
- `runtimeURLToMetadataCache.dispose() Evicting cached metadata for "${runtimeSourceURL}"`,
82
- );
83
- }
84
-
85
- const sourceConsumer = metadata.sourceConsumer;
86
- if (sourceConsumer !== null) {
87
- sourceConsumer.destroy();
88
- }
89
- },
90
-});
91
-
92
-type CachedSourceCodeMetadata = {|
93
- originalSourceAST: AST,
94
- originalSourceCode: string,
95
-|};
96
-
97
-const originalURLToMetadataCache: LRUCache<
98
- string,
99
- CachedSourceCodeMetadata,
100
-> = new LRU({
101
- max: 50,
102
- dispose: (originalSourceURL: string, metadata: CachedSourceCodeMetadata) => {
103
- if (__DEBUG__) {
104
- console.log(
105
- `originalURLToMetadataCache.dispose() Evicting cached metadata for "${originalSourceURL}"`,
106
- );
107
- }
108
- },
109
-});
110
-
111
-export async function parseHookNames(
112
- hooksTree: HooksTree,
113
-): Promise<HookNames | null> {
114
- const hooksList: Array<HooksNode> = [];
115
- withSyncPerformanceMark('flattenHooksList()', () => {
116
- flattenHooksList(hooksTree, hooksList);
117
- });
118
-
119
- return withAsyncPerformanceMark('parseHookNames()', () =>
120
- parseHookNamesImpl(hooksList),
121
- );
122
-}
123
-
124
-async function parseHookNamesImpl(
125
- hooksList: HooksNode[],
126
-): Promise<HookNames | null> {
127
- if (__DEBUG__) {
128
- console.log('parseHookNames() hooksList:', hooksList);
129
- }
130
-
131
- // Create map of unique source locations (file names plus line and column numbers) to metadata about hooks.
132
- const locationKeyToHookSourceData: Map<string, HookSourceData> = new Map();
133
- for (let i = 0; i < hooksList.length; i++) {
134
- const hook = hooksList[i];
135
-
136
- const hookSource = hook.hookSource;
137
- if (hookSource == null) {
138
- // Older versions of react-debug-tools don't include this information.
139
- // In this case, we can't continue.
140
- throw Error('Hook source code location not found.');
141
- }
142
-
143
- const locationKey = getHookSourceLocationKey(hookSource);
144
- if (!locationKeyToHookSourceData.has(locationKey)) {
145
- // Can't be null because getHookSourceLocationKey() would have thrown
146
- const runtimeSourceURL = ((hookSource.fileName: any): string);
147
-
148
- const hookSourceData: HookSourceData = {
149
- hookSource,
150
- metadataConsumer: null,
151
- originalSourceAST: null,
152
- originalSourceCode: null,
153
- originalSourceURL: null,
154
- runtimeSourceCode: null,
155
- runtimeSourceURL,
156
- sourceConsumer: null,
157
- sourceMapURL: null,
158
- };
159
-
160
- // If we've already loaded the source map info for this file,
161
- // we can skip reloading it (and more importantly, re-parsing it).
162
- const runtimeMetadata = runtimeURLToMetadataCache.get(
163
- hookSourceData.runtimeSourceURL,
164
- );
165
- if (runtimeMetadata != null) {
166
- if (__DEBUG__) {
167
- console.groupCollapsed(
168
- `parseHookNames() Found cached runtime metadata for file "${hookSourceData.runtimeSourceURL}"`,
169
- );
170
- console.log(runtimeMetadata);
171
- console.groupEnd();
172
- }
173
- hookSourceData.sourceConsumer = runtimeMetadata.sourceConsumer;
174
- hookSourceData.metadataConsumer = runtimeMetadata.metadataConsumer;
175
- }
176
-
177
- locationKeyToHookSourceData.set(locationKey, hookSourceData);
178
- }
179
- }
180
-
181
- await withAsyncPerformanceMark('loadSourceFiles()', () =>
182
- loadSourceFiles(locationKeyToHookSourceData),
183
- );
184
-
185
- await withAsyncPerformanceMark('extractAndLoadSourceMaps()', () =>
186
- extractAndLoadSourceMaps(locationKeyToHookSourceData),
187
- );
188
-
189
- withSyncPerformanceMark('parseSourceAST()', () =>
190
- parseSourceAST(locationKeyToHookSourceData),
191
- );
192
-
193
- withSyncPerformanceMark('updateLruCache()', () =>
194
- updateLruCache(locationKeyToHookSourceData),
195
- );
196
-
197
- return withSyncPerformanceMark('findHookNames()', () =>
198
- findHookNames(hooksList, locationKeyToHookSourceData),
199
- );
200
-}
201
-
202
-function decodeBase64String(encoded: string): Object {
203
- if (typeof atob === 'function') {
204
- return atob(encoded);
205
- } else if (
206
- typeof Buffer !== 'undefined' &&
207
- Buffer !== null &&
208
- typeof Buffer.from === 'function'
209
- ) {
210
- return Buffer.from(encoded, 'base64');
211
- } else {
212
- throw Error('Cannot decode base64 string');
213
- }
214
-}
215
-
216
-function extractAndLoadSourceMaps(
217
- locationKeyToHookSourceData: Map<string, HookSourceData>,
218
-): Promise<*> {
219
- // SourceMapConsumer.initialize() does nothing when running in Node (aka Jest)
220
- // because the wasm file is automatically read from the file system
221
- // so we can avoid triggering a warning message about this.
222
- if (!__TEST__) {
223
- if (__DEBUG__) {
224
- console.log(
225
- 'extractAndLoadSourceMaps() Initializing source-map library ...',
226
- );
227
- }
228
- }
229
-
230
- // Deduplicate fetches, since there can be multiple location keys per source map.
231
- const fetchPromises = new Map();
232
-
233
- const setPromises = [];
234
- locationKeyToHookSourceData.forEach(hookSourceData => {
235
- if (
236
- hookSourceData.sourceConsumer != null &&
237
- hookSourceData.metadataConsumer != null
238
- ) {
239
- // Use cached source map and metadata consumers.
240
- return;
241
- }
242
-
243
- const sourceMapRegex = / ?sourceMappingURL=([^\s'"]+)/gm;
244
- const runtimeSourceCode = ((hookSourceData.runtimeSourceCode: any): string);
245
-
246
- let sourceMappingURLMatch = withSyncPerformanceMark(
247
- 'sourceMapRegex.exec(runtimeSourceCode)',
248
- () => sourceMapRegex.exec(runtimeSourceCode),
249
- );
250
-
251
- if (sourceMappingURLMatch == null) {
252
- // Maybe file has not been transformed; we'll try to parse it as-is in parseSourceAST().
253
-
254
- if (__DEBUG__) {
255
- console.log('extractAndLoadSourceMaps() No source map found');
256
- }
257
- } else {
258
- const externalSourceMapURLs = [];
259
- while (sourceMappingURLMatch != null) {
260
- const {runtimeSourceURL} = hookSourceData;
261
- const sourceMappingURL = sourceMappingURLMatch[1];
262
- const hasInlineSourceMap = sourceMappingURL.indexOf('base64,') >= 0;
263
- if (hasInlineSourceMap) {
264
- // TODO (named hooks) deduplicate parsing in this branch (similar to fetching in the other branch)
265
- // since there can be multiple location keys per source map.
266
-
267
- // Web apps like Code Sandbox embed multiple inline source maps.
268
- // In this case, we need to loop through and find the right one.
269
- // We may also need to trim any part of this string that isn't based64 encoded data.
270
- const trimmed = ((sourceMappingURL.match(
271
- /base64,([a-zA-Z0-9+\/=]+)/,
272
- ): any): Array<string>)[1];
273
- const decoded = withSyncPerformanceMark('decodeBase64String()', () =>
274
- decodeBase64String(trimmed),
275
- );
276
-
277
- const parsed = withSyncPerformanceMark('JSON.parse(decoded)', () =>
278
- JSON.parse(decoded),
279
- );
280
-
281
- if (__DEBUG__) {
282
- console.groupCollapsed(
283
- 'extractAndLoadSourceMaps() Inline source map',
284
- );
285
- console.log(parsed);
286
- console.groupEnd();
287
- }
288
-
289
- // Hook source might be a URL like "https://4syus.csb.app/src/App.js"
290
- // Parsed source map might be a partial path like "src/App.js"
291
- if (sourceMapIncludesSource(parsed, runtimeSourceURL)) {
292
- hookSourceData.metadataConsumer = withSyncPerformanceMark(
293
- 'new SourceMapMetadataConsumer(parsed)',
294
- () => new SourceMapMetadataConsumer(parsed),
295
- );
296
- hookSourceData.sourceConsumer = withSyncPerformanceMark(
297
- 'new SourceMapConsumer(parsed)',
298
- () => new SourceMapConsumer(parsed),
299
- );
300
- break;
301
- }
302
- } else {
303
- externalSourceMapURLs.push(sourceMappingURL);
304
- }
305
-
306
- sourceMappingURLMatch = withSyncPerformanceMark(
307
- 'sourceMapRegex.exec(runtimeSourceCode)',
308
- () => sourceMapRegex.exec(runtimeSourceCode),
309
- );
310
- }
311
-
312
- const foundInlineSourceMap =
313
- hookSourceData.sourceConsumer != null &&
314
- hookSourceData.metadataConsumer != null;
315
- if (!foundInlineSourceMap) {
316
- externalSourceMapURLs.forEach((sourceMappingURL, index) => {
317
- if (index !== externalSourceMapURLs.length - 1) {
318
- // Files with external source maps should only have a single source map.
319
- // More than one result might indicate an edge case,
320
- // like a string in the source code that matched our "sourceMappingURL" regex.
321
- // We should just skip over cases like this.
322
- console.warn(
323
- `More than one external source map detected in the source file; skipping "${sourceMappingURL}"`,
324
- );
325
- return;
326
- }
327
-
328
- const {runtimeSourceURL} = hookSourceData;
329
- let url = sourceMappingURL;
330
- if (!url.startsWith('http') && !url.startsWith('/')) {
331
- // Resolve paths relative to the location of the file name
332
- const lastSlashIdx = runtimeSourceURL.lastIndexOf('/');
333
- if (lastSlashIdx !== -1) {
334
- const baseURL = runtimeSourceURL.slice(
335
- 0,
336
- runtimeSourceURL.lastIndexOf('/'),
337
- );
338
- url = `${baseURL}/${url}`;
339
- }
340
- }
341
-
342
- hookSourceData.sourceMapURL = url;
343
-
344
- const fetchPromise =
345
- fetchPromises.get(url) ||
346
- fetchFile(url).then(
347
- sourceMapContents => {
348
- const parsed = withSyncPerformanceMark(
349
- 'JSON.parse(sourceMapContents)',
350
- () => JSON.parse(sourceMapContents),
351
- );
352
-
353
- const sourceConsumer = withSyncPerformanceMark(
354
- 'new SourceMapConsumer(parsed)',
355
- () => new SourceMapConsumer(parsed),
356
- );
357
-
358
- const metadataConsumer = withSyncPerformanceMark(
359
- 'new SourceMapMetadataConsumer(parsed)',
360
- () => new SourceMapMetadataConsumer(parsed),
361
- );
362
-
363
- return {sourceConsumer, metadataConsumer};
364
- },
365
- // In this case, we fall back to the assumption that the source has no source map.
366
- // This might indicate an (unlikely) edge case that had no source map,
367
- // but contained the string "sourceMappingURL".
368
- error => null,
369
- );
370
-
371
- if (__DEBUG__) {
372
- if (!fetchPromises.has(url)) {
373
- console.log(
374
- `extractAndLoadSourceMaps() External source map "${url}"`,
375
- );
376
- }
377
- }
378
-
379
- fetchPromises.set(url, fetchPromise);
380
- setPromises.push(
381
- fetchPromise.then(result => {
382
- hookSourceData.metadataConsumer =
383
- result?.metadataConsumer ?? null;
384
- hookSourceData.sourceConsumer = result?.sourceConsumer ?? null;
385
- }),
386
- );
387
- });
388
- }
389
- }
390
- });
391
- return Promise.all(setPromises);
392
-}
393
-
394
-function fetchFile(url: string): Promise<string> {
395
- return withCallbackPerformanceMark('fetchFile("' + url + '")', done => {
396
- return new Promise((resolve, reject) => {
397
- fetch(url).then(
398
- response => {
399
- if (response.ok) {
400
- response
401
- .text()
402
- .then(text => {
403
- done();
404
- resolve(text);
405
- })
406
- .catch(error => {
407
- if (__DEBUG__) {
408
- console.log(
409
- `fetchFile() Could not read text for url "${url}"`,
410
- );
411
- }
412
- done();
413
- reject(null);
414
- });
415
- } else {
416
- if (__DEBUG__) {
417
- console.log(`fetchFile() Got bad response for url "${url}"`);
418
- }
419
- done();
420
- reject(null);
421
- }
422
- },
423
- error => {
424
- if (__DEBUG__) {
425
- console.log(`fetchFile() Could not fetch file: ${error.message}`);
426
- }
427
- done();
428
- reject(null);
429
- },
430
- );
431
- });
432
- });
433
-}
434
-
435
-function findHookNames(
436
- hooksList: Array<HooksNode>,
437
- locationKeyToHookSourceData: Map<string, HookSourceData>,
438
-): HookNames {
439
- const map: HookNames = new Map();
440
-
441
- hooksList.map(hook => {
442
- // We already guard against a null HookSource in parseHookNames()
443
- const hookSource = ((hook.hookSource: any): HookSource);
444
- const fileName = hookSource.fileName;
445
- if (!fileName) {
446
- return null; // Should not be reachable.
447
- }
448
-
449
- const locationKey = getHookSourceLocationKey(hookSource);
450
- const hookSourceData = locationKeyToHookSourceData.get(locationKey);
451
- if (!hookSourceData) {
452
- return null; // Should not be reachable.
453
- }
454
-
455
- const {lineNumber, columnNumber} = hookSource;
456
- if (!lineNumber || !columnNumber) {
457
- return null; // Should not be reachable.
458
- }
459
-
460
- const {originalSourceURL, sourceConsumer} = hookSourceData;
461
-
462
- let originalSourceColumnNumber;
463
- let originalSourceLineNumber;
464
- if (areSourceMapsAppliedToErrors() || !sourceConsumer) {
465
- // Either the current environment automatically applies source maps to errors,
466
- // or the current code had no source map to begin with.
467
- // Either way, we don't need to convert the Error stack frame locations.
468
- originalSourceColumnNumber = columnNumber;
469
- originalSourceLineNumber = lineNumber;
470
- } else {
471
- const position = withSyncPerformanceMark(
472
- 'sourceConsumer.originalPositionFor()',
473
- () =>
474
- sourceConsumer.originalPositionFor({
475
- line: lineNumber,
476
-
477
- // Column numbers are represented differently between tools/engines.
478
- // Error.prototype.stack columns are 1-based (like most IDEs) but ASTs are 0-based.
479
- // For more info see https://github.com/facebook/react/issues/21792#issuecomment-873171991
480
- column: columnNumber - 1,
481
- }),
482
- );
483
-
484
- originalSourceColumnNumber = position.column;
485
- originalSourceLineNumber = position.line;
486
- }
487
-
488
- if (__DEBUG__) {
489
- console.log(
490
- `findHookNames() mapped line ${lineNumber}->${originalSourceLineNumber} and column ${columnNumber}->${originalSourceColumnNumber}`,
491
- );
492
- }
493
-
494
- if (
495
- originalSourceLineNumber == null ||
496
- originalSourceColumnNumber == null ||
497
- originalSourceURL == null
498
- ) {
499
- return null;
500
- }
501
-
502
- let name;
503
- const {metadataConsumer} = hookSourceData;
504
- if (metadataConsumer != null) {
505
- name = withSyncPerformanceMark('metadataConsumer.hookNameFor()', () =>
506
- metadataConsumer.hookNameFor({
507
- line: originalSourceLineNumber,
508
- column: originalSourceColumnNumber,
509
- source: originalSourceURL,
510
- }),
511
- );
512
- }
513
-
514
- if (name == null) {
515
- name = withSyncPerformanceMark('getHookName()', () =>
516
- getHookName(
517
- hook,
518
- hookSourceData.originalSourceAST,
519
- ((hookSourceData.originalSourceCode: any): string),
520
- ((originalSourceLineNumber: any): number),
521
- originalSourceColumnNumber,
522
- ),
523
- );
524
- }
525
-
526
- if (__DEBUG__) {
527
- console.log(`findHookNames() Found name "${name || '-'}"`);
528
- }
529
-
530
- const key = getHookSourceLocationKey(hookSource);
531
- map.set(key, name);
532
- });
533
-
534
- return map;
535
-}
536
-
537
-function loadSourceFiles(
538
- locationKeyToHookSourceData: Map<string, HookSourceData>,
539
-): Promise<*> {
540
- // Deduplicate fetches, since there can be multiple location keys per file.
541
- const fetchPromises = new Map();
542
-
543
- const setPromises = [];
544
- locationKeyToHookSourceData.forEach(hookSourceData => {
545
- const {runtimeSourceURL} = hookSourceData;
546
- const fetchPromise =
547
- fetchPromises.get(runtimeSourceURL) ||
548
- fetchFile(runtimeSourceURL).then(runtimeSourceCode => {
549
- if (runtimeSourceCode.length > MAX_SOURCE_LENGTH) {
550
- throw Error('Source code too large to parse');
551
- }
552
- if (__DEBUG__) {
553
- console.groupCollapsed(
554
- `loadSourceFiles() runtimeSourceURL "${runtimeSourceURL}"`,
555
- );
556
- console.log(runtimeSourceCode);
557
- console.groupEnd();
558
- }
559
- return runtimeSourceCode;
560
- });
561
- fetchPromises.set(runtimeSourceURL, fetchPromise);
562
- setPromises.push(
563
- fetchPromise.then(runtimeSourceCode => {
564
- hookSourceData.runtimeSourceCode = runtimeSourceCode;
565
- }),
566
- );
567
- });
568
- return Promise.all(setPromises);
569
-}
570
-
571
-function parseSourceAST(
572
- locationKeyToHookSourceData: Map<string, HookSourceData>,
573
-): void {
574
- locationKeyToHookSourceData.forEach(hookSourceData => {
575
- if (hookSourceData.originalSourceAST !== null) {
576
- // Use cached metadata.
577
- return;
578
- }
579
-
580
- const {metadataConsumer, sourceConsumer} = hookSourceData;
581
- const runtimeSourceCode = ((hookSourceData.runtimeSourceCode: any): string);
582
- let hasHookMap = false;
583
- let originalSourceURL;
584
- let originalSourceCode;
585
- if (sourceConsumer !== null) {
586
- // Parse and extract the AST from the source map.
587
- const {lineNumber, columnNumber} = hookSourceData.hookSource;
588
- if (lineNumber == null || columnNumber == null) {
589
- throw Error('Hook source code location not found.');
590
- }
591
- // Now that the source map has been loaded,
592
- // extract the original source for later.
593
- const {source} = withSyncPerformanceMark(
594
- 'sourceConsumer.originalPositionFor()',
595
- () =>
596
- sourceConsumer.originalPositionFor({
597
- line: lineNumber,
598
-
599
- // Column numbers are represented differently between tools/engines.
600
- // Error.prototype.stack columns are 1-based (like most IDEs) but ASTs are 0-based.
601
- // For more info see https://github.com/facebook/react/issues/21792#issuecomment-873171991
602
- column: columnNumber - 1,
603
- }),
604
- );
605
-
606
- if (source == null) {
607
- // TODO (named hooks) maybe fall back to the runtime source instead of throwing?
608
- throw new Error(
609
- 'Could not map hook runtime location to original source location',
610
- );
611
- }
612
-
613
- // TODO (named hooks) maybe canonicalize this URL somehow?
614
- // It can be relative if the source map specifies it that way,
615
- // but we use it as a cache key across different source maps and there can be collisions.
616
- originalSourceURL = (source: string);
617
- originalSourceCode = withSyncPerformanceMark(
618
- 'sourceConsumer.sourceContentFor()',
619
- () => (sourceConsumer.sourceContentFor(source, true): string),
620
- );
621
-
622
- if (__DEBUG__) {
623
- console.groupCollapsed(
624
- 'parseSourceAST() Extracted source code from source map',
625
- );
626
- console.log(originalSourceCode);
627
- console.groupEnd();
628
- }
629
-
630
- if (
631
- metadataConsumer != null &&
632
- metadataConsumer.hasHookMap(originalSourceURL)
633
- ) {
634
- hasHookMap = true;
635
- }
636
- } else {
637
- // There's no source map to parse here so we can just parse the original source itself.
638
- originalSourceCode = runtimeSourceCode;
639
- // TODO (named hooks) This mixes runtimeSourceURLs with source mapped URLs in the same cache key space.
640
- // Namespace them?
641
- originalSourceURL = hookSourceData.runtimeSourceURL;
642
- }
643
-
644
- hookSourceData.originalSourceCode = originalSourceCode;
645
- hookSourceData.originalSourceURL = originalSourceURL;
646
-
647
- if (hasHookMap) {
648
- // If there's a hook map present from an extended sourcemap then
649
- // we don't need to parse the source files and instead can use the
650
- // hook map to extract hook names.
651
- return;
652
- }
653
-
654
- // The cache also serves to deduplicate parsing by URL in our loop over
655
- // location keys. This may need to change if we switch to async parsing.
656
- const sourceMetadata = originalURLToMetadataCache.get(originalSourceURL);
657
- if (sourceMetadata != null) {
658
- if (__DEBUG__) {
659
- console.groupCollapsed(
660
- `parseSourceAST() Found cached source metadata for "${originalSourceURL}"`,
661
- );
662
- console.log(sourceMetadata);
663
- console.groupEnd();
664
- }
665
- hookSourceData.originalSourceAST = sourceMetadata.originalSourceAST;
666
- hookSourceData.originalSourceCode = sourceMetadata.originalSourceCode;
667
- } else {
668
- // TypeScript is the most commonly used typed JS variant so let's default to it
669
- // unless we detect explicit Flow usage via the "@flow" pragma.
670
- const plugin =
671
- originalSourceCode.indexOf('@flow') > 0 ? 'flow' : 'typescript';
672
-
673
- // TODO (named hooks) Parsing should ideally be done off of the main thread.
674
- const originalSourceAST = withSyncPerformanceMark(
675
- '[@babel/parser] parse(originalSourceCode)',
676
- () =>
677
- parse(originalSourceCode, {
678
- sourceType: 'unambiguous',
679
- plugins: ['jsx', plugin],
680
- }),
681
- );
682
- hookSourceData.originalSourceAST = originalSourceAST;
683
- if (__DEBUG__) {
684
- console.log(
685
- `parseSourceAST() Caching source metadata for "${originalSourceURL}"`,
686
- );
687
- }
688
- originalURLToMetadataCache.set(originalSourceURL, {
689
- originalSourceAST,
690
- originalSourceCode,
691
- });
692
- }
693
- });
694
-}
695
-
696
-function flattenHooksList(
697
- hooksTree: HooksTree,
698
- hooksList: Array<HooksNode>,
699
-): void {
700
- for (let i = 0; i < hooksTree.length; i++) {
701
- const hook = hooksTree[i];
702
-
703
- if (isUnnamedBuiltInHook(hook)) {
704
- // No need to load source code or do any parsing for unnamed hooks.
705
- if (__DEBUG__) {
706
- console.log('flattenHooksList() Skipping unnamed hook', hook);
707
- }
708
- continue;
709
- }
710
-
711
- hooksList.push(hook);
712
- if (hook.subHooks.length > 0) {
713
- flattenHooksList(hook.subHooks, hooksList);
714
- }
715
- }
716
-}
717
-
718
-// Determines whether incoming hook is a primitive hook that gets assigned to variables.
719
-function isUnnamedBuiltInHook(hook: HooksNode) {
720
- return ['Effect', 'ImperativeHandle', 'LayoutEffect', 'DebugValue'].includes(
721
- hook.name,
722
- );
723
-}
724
-
725
-function updateLruCache(
726
- locationKeyToHookSourceData: Map<string, HookSourceData>,
727
-): void {
728
- locationKeyToHookSourceData.forEach(
729
- ({metadataConsumer, sourceConsumer, runtimeSourceURL}) => {
730
- // Only set once to avoid triggering eviction/cleanup code.
731
- if (!runtimeURLToMetadataCache.has(runtimeSourceURL)) {
732
- if (__DEBUG__) {
733
- console.log(
734
- `updateLruCache() Caching runtime metadata for "${runtimeSourceURL}"`,
735
- );
736
- }
737
-
738
- runtimeURLToMetadataCache.set(runtimeSourceURL, {
739
- metadataConsumer,
740
- sourceConsumer,
741
- });
742
- }
743
- },
744
- );
745
-}
746
-
747
-export function purgeCachedMetadata(): void {
748
- originalURLToMetadataCache.reset();
749
- runtimeURLToMetadataCache.reset();
750
-}