import {parse} from '@babel/parser';
import LRU from 'lru-cache';
import {getHookName} from '../astUtils';
import {areSourceMapsAppliedToErrors} from '../ErrorTester';
import {__DEBUG__} from 'react-devtools-shared/src/constants';
import {getHookSourceLocationKey} from 'react-devtools-shared/src/hookNamesCache';
import {SourceMapMetadataConsumer} from '../SourceMapMetadataConsumer';
import {
withAsyncPerfMeasurements,
withSyncPerfMeasurements,
} from 'react-devtools-shared/src/PerformanceLoggingUtils';
import SourceMapConsumer from '../SourceMapConsumer';
import type {SourceMapConsumerType} from '../SourceMapConsumer';
import type {
HooksList,
LocationKeyToHookSourceAndMetadata,
} from './loadSourceAndMetadata';
import type {HookSource} from 'react-debug-tools/src/ReactDebugHooks';
import type {
HookNames,
LRUCache,
} from 'react-devtools-shared/src/frontend/types';
type AST = mixed;
type HookParsedMetadata = {
metadataConsumer: SourceMapMetadataConsumer | null,
originalSourceAST: AST | null,
originalSourceCode: string | null,
originalSourceURL: string | null,
originalSourceLineNumber: number | null,
originalSourceColumnNumber: number | null,
sourceMapConsumer: SourceMapConsumerType | null,
};
type LocationKeyToHookParsedMetadata = Map<string, HookParsedMetadata>;
type CachedRuntimeCodeMetadata = {
metadataConsumer: SourceMapMetadataConsumer | null,
sourceMapConsumer: SourceMapConsumerType | null,
};
const runtimeURLToMetadataCache: LRUCache<string, CachedRuntimeCodeMetadata> =
new LRU({max: 50});
type CachedSourceCodeMetadata = {
originalSourceAST: AST,
originalSourceCode: string,
};
const originalURLToMetadataCache: LRUCache<string, CachedSourceCodeMetadata> =
new LRU({
max: 50,
dispose: (
originalSourceURL: string,
metadata: CachedSourceCodeMetadata,
) => {
if (__DEBUG__) {
console.log(
`originalURLToMetadataCache.dispose() Evicting cached metadata for "${originalSourceURL}"`,
);
}
},
});
export async function parseSourceAndMetadata(
hooksList: HooksList,
locationKeyToHookSourceAndMetadata: LocationKeyToHookSourceAndMetadata,
): Promise<HookNames | null> {
return withAsyncPerfMeasurements('parseSourceAndMetadata()', async () => {
const locationKeyToHookParsedMetadata = withSyncPerfMeasurements(
'initializeHookParsedMetadata',
() => initializeHookParsedMetadata(locationKeyToHookSourceAndMetadata),
);
withSyncPerfMeasurements('parseSourceMaps', () =>
parseSourceMaps(
locationKeyToHookSourceAndMetadata,
locationKeyToHookParsedMetadata,
),
);
withSyncPerfMeasurements('parseSourceAST()', () =>
parseSourceAST(
locationKeyToHookSourceAndMetadata,
locationKeyToHookParsedMetadata,
),
);
return withSyncPerfMeasurements('findHookNames()', () =>
findHookNames(hooksList, locationKeyToHookParsedMetadata),
);
});
}
function findHookNames(
hooksList: HooksList,
locationKeyToHookParsedMetadata: LocationKeyToHookParsedMetadata,
): HookNames {
const map: HookNames = new Map();
hooksList.map(hook => {
const hookSource = ((hook.hookSource: any): HookSource);
const fileName = hookSource.fileName;
if (!fileName) {
return null;
}
const locationKey = getHookSourceLocationKey(hookSource);
const hookParsedMetadata = locationKeyToHookParsedMetadata.get(locationKey);
if (!hookParsedMetadata) {
return null;
}
const {lineNumber, columnNumber} = hookSource;
if (!lineNumber || !columnNumber) {
return null;
}
const {
originalSourceURL,
originalSourceColumnNumber,
originalSourceLineNumber,
} = hookParsedMetadata;
if (
originalSourceLineNumber == null ||
originalSourceColumnNumber == null ||
originalSourceURL == null
) {
return null;
}
let name;
const {metadataConsumer} = hookParsedMetadata;
if (metadataConsumer != null) {
name = withSyncPerfMeasurements('metadataConsumer.hookNameFor()', () =>
metadataConsumer.hookNameFor({
line: originalSourceLineNumber,
column: originalSourceColumnNumber,
source: originalSourceURL,
}),
);
}
if (name == null) {
name = withSyncPerfMeasurements('getHookName()', () =>
getHookName(
hook,
hookParsedMetadata.originalSourceAST,
((hookParsedMetadata.originalSourceCode: any): string),
((originalSourceLineNumber: any): number),
originalSourceColumnNumber,
),
);
}
if (__DEBUG__) {
console.log(`findHookNames() Found name "${name || '-'}"`);
}
const key = getHookSourceLocationKey(hookSource);
map.set(key, name);
});
return map;
}
function initializeHookParsedMetadata(
locationKeyToHookSourceAndMetadata: LocationKeyToHookSourceAndMetadata,
) {
const locationKeyToHookParsedMetadata: LocationKeyToHookParsedMetadata =
new Map();
locationKeyToHookSourceAndMetadata.forEach(
(hookSourceAndMetadata, locationKey) => {
const hookParsedMetadata: HookParsedMetadata = {
metadataConsumer: null,
originalSourceAST: null,
originalSourceCode: null,
originalSourceURL: null,
originalSourceLineNumber: null,
originalSourceColumnNumber: null,
sourceMapConsumer: null,
};
locationKeyToHookParsedMetadata.set(locationKey, hookParsedMetadata);
},
);
return locationKeyToHookParsedMetadata;
}
function parseSourceAST(
locationKeyToHookSourceAndMetadata: LocationKeyToHookSourceAndMetadata,
locationKeyToHookParsedMetadata: LocationKeyToHookParsedMetadata,
): void {
locationKeyToHookSourceAndMetadata.forEach(
(hookSourceAndMetadata, locationKey) => {
const hookParsedMetadata =
locationKeyToHookParsedMetadata.get(locationKey);
if (hookParsedMetadata == null) {
throw Error(`Expected to find HookParsedMetadata for "${locationKey}"`);
}
if (hookParsedMetadata.originalSourceAST !== null) {
return;
}
if (
hookParsedMetadata.originalSourceURL != null &&
hookParsedMetadata.originalSourceCode != null &&
hookParsedMetadata.originalSourceColumnNumber != null &&
hookParsedMetadata.originalSourceLineNumber != null
) {
return;
}
const {lineNumber, columnNumber} = hookSourceAndMetadata.hookSource;
if (lineNumber == null || columnNumber == null) {
throw Error('Hook source code location not found.');
}
const {metadataConsumer, sourceMapConsumer} = hookParsedMetadata;
const runtimeSourceCode =
((hookSourceAndMetadata.runtimeSourceCode: any): string);
let hasHookMap = false;
let originalSourceURL;
let originalSourceCode;
let originalSourceColumnNumber;
let originalSourceLineNumber;
if (areSourceMapsAppliedToErrors() || sourceMapConsumer === null) {
originalSourceColumnNumber = columnNumber;
originalSourceLineNumber = lineNumber;
originalSourceCode = runtimeSourceCode;
originalSourceURL = hookSourceAndMetadata.runtimeSourceURL;
} else {
const {column, line, sourceContent, sourceURL} =
sourceMapConsumer.originalPositionFor({
columnNumber,
lineNumber,
});
if (sourceContent === null || sourceURL === null) {
throw Error(
`Could not find original source for line:${lineNumber} and column:${columnNumber}`,
);
}
originalSourceColumnNumber = column;
originalSourceLineNumber = line;
originalSourceCode = sourceContent;
originalSourceURL = sourceURL;
}
hookParsedMetadata.originalSourceCode = originalSourceCode;
hookParsedMetadata.originalSourceURL = originalSourceURL;
hookParsedMetadata.originalSourceLineNumber = originalSourceLineNumber;
hookParsedMetadata.originalSourceColumnNumber =
originalSourceColumnNumber;
if (
metadataConsumer != null &&
metadataConsumer.hasHookMap(originalSourceURL)
) {
hasHookMap = true;
}
if (__DEBUG__) {
console.log(
`parseSourceAST() mapped line ${lineNumber}->${originalSourceLineNumber} and column ${columnNumber}->${originalSourceColumnNumber}`,
);
}
if (hasHookMap) {
if (__DEBUG__) {
console.log(
`parseSourceAST() Found hookMap and skipping parsing for "${originalSourceURL}"`,
);
}
return;
}
if (__DEBUG__) {
console.log(
`parseSourceAST() Did not find hook map for "${originalSourceURL}"`,
);
}
const sourceMetadata = originalURLToMetadataCache.get(originalSourceURL);
if (sourceMetadata != null) {
if (__DEBUG__) {
console.groupCollapsed(
`parseSourceAST() Found cached source metadata for "${originalSourceURL}"`,
);
console.log(sourceMetadata);
console.groupEnd();
}
hookParsedMetadata.originalSourceAST = sourceMetadata.originalSourceAST;
hookParsedMetadata.originalSourceCode =
sourceMetadata.originalSourceCode;
} else {
try {
const plugin =
originalSourceCode.indexOf('@flow') > 0 ? 'flow' : 'typescript';
const originalSourceAST = withSyncPerfMeasurements(
'[@babel/parser] parse(originalSourceCode)',
() =>
parse(originalSourceCode, {
sourceType: 'unambiguous',
plugins: ['jsx', plugin],
}),
);
hookParsedMetadata.originalSourceAST = originalSourceAST;
if (__DEBUG__) {
console.log(
`parseSourceAST() Caching source metadata for "${originalSourceURL}"`,
);
}
originalURLToMetadataCache.set(originalSourceURL, {
originalSourceAST,
originalSourceCode,
});
} catch (error) {
throw new Error(
`Failed to parse source file: ${originalSourceURL}\n\n` +
`Original error: ${error}`,
);
}
}
},
);
}
function parseSourceMaps(
locationKeyToHookSourceAndMetadata: LocationKeyToHookSourceAndMetadata,
locationKeyToHookParsedMetadata: LocationKeyToHookParsedMetadata,
) {
locationKeyToHookSourceAndMetadata.forEach(
(hookSourceAndMetadata, locationKey) => {
const hookParsedMetadata =
locationKeyToHookParsedMetadata.get(locationKey);
if (hookParsedMetadata == null) {
throw Error(`Expected to find HookParsedMetadata for "${locationKey}"`);
}
const {runtimeSourceURL, sourceMapJSON} = hookSourceAndMetadata;
const runtimeMetadata = runtimeURLToMetadataCache.get(runtimeSourceURL);
if (runtimeMetadata != null) {
if (__DEBUG__) {
console.groupCollapsed(
`parseHookNames() Found cached runtime metadata for file "${runtimeSourceURL}"`,
);
console.log(runtimeMetadata);
console.groupEnd();
}
hookParsedMetadata.metadataConsumer = runtimeMetadata.metadataConsumer;
hookParsedMetadata.sourceMapConsumer =
runtimeMetadata.sourceMapConsumer;
} else {
if (sourceMapJSON != null) {
const sourceMapConsumer = withSyncPerfMeasurements(
'new SourceMapConsumer(sourceMapJSON)',
() => SourceMapConsumer(sourceMapJSON),
);
const metadataConsumer = withSyncPerfMeasurements(
'new SourceMapMetadataConsumer(sourceMapJSON)',
() => new SourceMapMetadataConsumer(sourceMapJSON),
);
hookParsedMetadata.metadataConsumer = metadataConsumer;
hookParsedMetadata.sourceMapConsumer = sourceMapConsumer;
runtimeURLToMetadataCache.set(runtimeSourceURL, {
metadataConsumer: metadataConsumer,
sourceMapConsumer: sourceMapConsumer,
});
}
}
},
);
}
export function purgeCachedMetadata(): void {
originalURLToMetadataCache.reset();
runtimeURLToMetadataCache.reset();
}