import {transformFromAstSync} from '@babel/core';
import {parse as babelParse} from '@babel/parser';
import {File} from '@babel/types';
import BabelPluginReactCompiler, {
parsePluginOptions,
validateEnvironmentConfig,
type PluginOptions,
} from 'babel-plugin-react-compiler/src';
import {Logger, LoggerEvent} from 'babel-plugin-react-compiler/src/Entrypoint';
import type {SourceCode} from 'eslint';
import * as HermesParser from 'hermes-parser';
import {isDeepStrictEqual} from 'util';
import type {ParseResult} from '@babel/parser';
const COMPILER_OPTIONS: PluginOptions = {
outputMode: 'lint',
panicThreshold: 'none',
flowSuppressions: false,
environment: validateEnvironmentConfig({
validateRefAccessDuringRender: true,
validateNoSetStateInRender: true,
validateNoSetStateInEffects: true,
validateNoJSXInTryStatements: true,
validateNoImpureFunctionsInRender: true,
validateStaticComponents: true,
validateNoFreezingKnownMutableFunctions: true,
validateNoVoidUseMemo: true,
validateNoCapitalizedCalls: [],
validateHooksUsage: true,
validateNoDerivedComputationsInEffects: true,
}),
};
export type RunCacheEntry = {
sourceCode: string;
filename: string;
userOpts: PluginOptions;
flowSuppressions: Array<{line: number; code: string}>;
events: Array<LoggerEvent>;
};
type RunParams = {
sourceCode: SourceCode;
filename: string;
userOpts: PluginOptions;
};
const FLOW_SUPPRESSION_REGEX = /\$FlowFixMe\[([^\]]*)\]/g;
function getFlowSuppressions(
sourceCode: SourceCode,
): Array<{line: number; code: string}> {
const comments = sourceCode.getAllComments();
const results: Array<{line: number; code: string}> = [];
for (const commentNode of comments) {
const matches = commentNode.value.matchAll(FLOW_SUPPRESSION_REGEX);
for (const match of matches) {
if (match.index != null && commentNode.loc != null) {
const code = match[1];
results.push({
line: commentNode.loc!.end.line,
code,
});
}
}
}
return results;
}
function runReactCompilerImpl({
sourceCode,
filename,
userOpts,
}: RunParams): RunCacheEntry {
const options: PluginOptions = parsePluginOptions({
...COMPILER_OPTIONS,
...userOpts,
environment: {
...COMPILER_OPTIONS.environment,
...userOpts.environment,
},
});
const results: RunCacheEntry = {
sourceCode: sourceCode.text,
filename,
userOpts,
flowSuppressions: [],
events: [],
};
const userLogger: Logger | null = options.logger;
options.logger = {
logEvent: (eventFilename, event): void => {
userLogger?.logEvent(eventFilename, event);
results.events.push(event);
},
};
try {
options.environment = validateEnvironmentConfig(options.environment ?? {});
} catch (err: unknown) {
options.logger?.logEvent(filename, err as LoggerEvent);
}
let babelAST: ParseResult<File> | null = null;
if (filename.endsWith('.tsx') || filename.endsWith('.ts')) {
try {
babelAST = babelParse(sourceCode.text, {
sourceFilename: filename,
sourceType: 'unambiguous',
plugins: ['typescript', 'jsx'],
});
} catch {
}
} else {
try {
babelAST = HermesParser.parse(sourceCode.text, {
babel: true,
enableExperimentalComponentSyntax: true,
sourceFilename: filename,
sourceType: 'module',
});
} catch {
}
}
if (babelAST != null) {
results.flowSuppressions = getFlowSuppressions(sourceCode);
try {
transformFromAstSync(babelAST, sourceCode.text, {
filename,
highlightCode: false,
retainLines: true,
plugins: [[BabelPluginReactCompiler, options]],
sourceType: 'module',
configFile: false,
babelrc: false,
});
} catch (err) {
}
}
return results;
}
const SENTINEL = Symbol();
class LRUCache<K, T> {
#values: Array<[K, T | Error] | [typeof SENTINEL, void]>;
#headIdx: number = 0;
constructor(size: number) {
this.#values = new Array(size).fill(SENTINEL);
}
get(key: K): T | null {
let idx = this.#values.findIndex(entry => entry[0] === key);
if (idx === this.#headIdx) {
return this.#values[this.#headIdx][1] as T;
} else if (idx < 0) {
return null;
}
const entry: [K, T] = this.#values[idx] as [K, T];
const len = this.#values.length;
for (let i = 0; i < Math.min(idx, len - 1); i++) {
this.#values[(this.#headIdx + i + 1) % len] =
this.#values[(this.#headIdx + i) % len];
}
this.#values[this.#headIdx] = entry;
return entry[1];
}
push(key: K, value: T): void {
this.#headIdx =
(this.#headIdx - 1 + this.#values.length) % this.#values.length;
this.#values[this.#headIdx] = [key, value];
}
}
const cache = new LRUCache<string, RunCacheEntry>(10);
export default function runReactCompiler({
sourceCode,
filename,
userOpts,
}: RunParams): RunCacheEntry {
const entry = cache.get(filename);
if (
entry != null &&
entry.sourceCode === sourceCode.text &&
isDeepStrictEqual(entry.userOpts, userOpts)
) {
return entry;
} else if (entry != null) {
if (process.env['DEBUG']) {
console.log(
`Cache hit for ${filename}, but source code or options changed, recomputing`,
);
}
}
const runEntry = runReactCompilerImpl({
sourceCode,
filename,
userOpts,
});
if (entry != null) {
Object.assign(entry, runEntry);
} else {
cache.push(filename, runEntry);
}
return {...runEntry};
}