import type * as BabelCore from "@babel/core";
import { transformFromAstSync } from "@babel/core";
import * as BabelParser from "@babel/parser";
import { NodePath } from "@babel/traverse";
import * as t from "@babel/types";
import assert from "assert";
import type {
CompilationMode,
Logger,
LoggerEvent,
PanicThresholdOptions,
PluginOptions,
} from "babel-plugin-react-compiler/src/Entrypoint";
import type { Effect, ValueKind } from "babel-plugin-react-compiler/src/HIR";
import type { parseConfigPragma as ParseConfigPragma } from "babel-plugin-react-compiler/src/HIR/Environment";
import * as HermesParser from "hermes-parser";
import invariant from "invariant";
import path from "path";
import prettier from "prettier";
import SproutTodoFilter from "./SproutTodoFilter";
import { isExpectError } from "./fixture-utils";
export function parseLanguage(source: string): "flow" | "typescript" {
return source.indexOf("@flow") !== -1 ? "flow" : "typescript";
}
function makePluginOptions(
firstLine: string,
parseConfigPragmaFn: typeof ParseConfigPragma
): [PluginOptions, Array<{ filename: string | null; event: LoggerEvent }>] {
let gating = null;
let enableEmitInstrumentForget = null;
let enableEmitFreeze = null;
let enableEmitHookGuards = null;
let compilationMode: CompilationMode = "all";
let runtimeModule = null;
let panicThreshold: PanicThresholdOptions = "all_errors";
let hookPattern: string | null = null;
let validatePreserveExistingMemoizationGuarantees = false;
let enableChangeDetectionForDebugging = null;
let customMacros = null;
if (firstLine.indexOf("@compilationMode(annotation)") !== -1) {
assert(
compilationMode === "all",
"Cannot set @compilationMode(..) more than once"
);
compilationMode = "annotation";
}
if (firstLine.indexOf("@compilationMode(infer)") !== -1) {
assert(
compilationMode === "all",
"Cannot set @compilationMode(..) more than once"
);
compilationMode = "infer";
}
if (firstLine.includes("@gating")) {
gating = {
source: "ReactForgetFeatureFlag",
importSpecifierName: "isForgetEnabled_Fixtures",
};
}
if (firstLine.includes("@instrumentForget")) {
enableEmitInstrumentForget = {
fn: {
source: "react-compiler-runtime",
importSpecifierName: "useRenderCounter",
},
gating: {
source: "react-compiler-runtime",
importSpecifierName: "shouldInstrument",
},
globalGating: "__DEV__",
};
}
if (firstLine.includes("@enableEmitFreeze")) {
enableEmitFreeze = {
source: "react-compiler-runtime",
importSpecifierName: "makeReadOnly",
};
}
if (firstLine.includes("@enableEmitHookGuards")) {
enableEmitHookGuards = {
source: "react-compiler-runtime",
importSpecifierName: "$dispatcherGuard",
};
}
const runtimeModuleMatch = /@runtimeModule="([^"]+)"/.exec(firstLine);
if (runtimeModuleMatch) {
runtimeModule = runtimeModuleMatch[1];
}
if (firstLine.includes("@panicThreshold(none)")) {
panicThreshold = "none";
}
let eslintSuppressionRules: Array<string> | null = null;
const eslintSuppressionMatch = /@eslintSuppressionRules\(([^)]+)\)/.exec(
firstLine
);
if (eslintSuppressionMatch != null) {
eslintSuppressionRules = eslintSuppressionMatch[1].split("|");
}
let flowSuppressions: boolean = false;
if (firstLine.includes("@enableFlowSuppressions")) {
flowSuppressions = true;
}
let ignoreUseNoForget: boolean = false;
if (firstLine.includes("@ignoreUseNoForget")) {
ignoreUseNoForget = true;
}
if (firstLine.includes("@validatePreserveExistingMemoizationGuarantees")) {
validatePreserveExistingMemoizationGuarantees = true;
}
if (firstLine.includes("@enableChangeDetectionForDebugging")) {
enableChangeDetectionForDebugging = {
source: "react-compiler-runtime",
importSpecifierName: "$structuralCheck",
};
}
const hookPatternMatch = /@hookPattern:"([^"]+)"/.exec(firstLine);
if (
hookPatternMatch &&
hookPatternMatch.length > 1 &&
hookPatternMatch[1].trim().length > 0
) {
hookPattern = hookPatternMatch[1].trim();
} else if (firstLine.includes("@hookPattern")) {
throw new Error(
'Invalid @hookPattern:"..." pragma, must contain the prefix between balanced double quotes eg @hookPattern:"pattern"'
);
}
const customMacrosMatch = /@customMacros\(([^)]+)\)/.exec(firstLine);
if (
customMacrosMatch &&
customMacrosMatch.length > 1 &&
customMacrosMatch[1].trim().length > 0
) {
customMacros = customMacrosMatch[1]
.split(" ")
.map((s) => s.trim())
.filter((s) => s.length > 0);
}
let logs: Array<{ filename: string | null; event: LoggerEvent }> = [];
let logger: Logger | null = null;
if (firstLine.includes("@logger")) {
logger = {
logEvent(filename: string | null, event: LoggerEvent): void {
logs.push({ filename, event });
},
};
}
const config = parseConfigPragmaFn(firstLine);
const options = {
environment: {
...config,
customHooks: new Map([
[
"useFreeze",
{
valueKind: "frozen" as ValueKind,
effectKind: "freeze" as Effect,
transitiveMixedData: false,
noAlias: false,
},
],
[
"useFragment",
{
valueKind: "frozen" as ValueKind,
effectKind: "freeze" as Effect,
transitiveMixedData: true,
noAlias: true,
},
],
[
"useNoAlias",
{
valueKind: "mutable" as ValueKind,
effectKind: "read" as Effect,
transitiveMixedData: false,
noAlias: true,
},
],
]),
customMacros,
enableEmitFreeze,
enableEmitInstrumentForget,
enableEmitHookGuards,
assertValidMutableRanges: true,
enableSharedRuntime__testonly: true,
hookPattern,
validatePreserveExistingMemoizationGuarantees,
enableChangeDetectionForDebugging,
},
compilationMode,
logger,
gating,
panicThreshold,
noEmit: false,
runtimeModule,
eslintSuppressionRules,
flowSuppressions,
ignoreUseNoForget,
enableReanimatedCheck: false,
};
return [options, logs];
}
export function parseInput(
input: string,
filename: string,
language: "flow" | "typescript"
): BabelCore.types.File {
if (language === "flow") {
return HermesParser.parse(input, {
babel: true,
flow: "all",
sourceFilename: filename,
sourceType: "module",
enableExperimentalComponentSyntax: true,
});
} else {
return BabelParser.parse(input, {
sourceFilename: filename,
plugins: ["typescript", "jsx"],
sourceType: "module",
});
}
}
function getEvaluatorPresets(
language: "typescript" | "flow"
): Array<BabelCore.PluginItem> {
const presets: Array<BabelCore.PluginItem> = [
{
plugins: ["babel-plugin-fbt", "babel-plugin-fbt-runtime"],
},
];
presets.push(
language === "typescript"
? [
"@babel/preset-typescript",
{
onlyRemoveTypeImports: true,
},
]
: "@babel/preset-flow"
);
presets.push({
plugins: ["@babel/plugin-syntax-jsx"],
});
presets.push(
["@babel/preset-react", { throwIfNamespace: false }],
{
plugins: ["@babel/plugin-transform-modules-commonjs"],
},
{
plugins: [
function BabelPluginRewriteRequirePath() {
return {
visitor: {
CallExpression(path: NodePath<t.CallExpression>) {
const { callee } = path.node;
if (callee.type === "Identifier" && callee.name === "require") {
const arg = path.node.arguments[0];
if (arg.type === "StringLiteral") {
if (arg.value === "shared-runtime") {
arg.value = "./shared-runtime";
} else if (arg.value === "ReactForgetFeatureFlag") {
arg.value = "./ReactForgetFeatureFlag";
}
}
}
},
},
};
},
],
}
);
return presets;
}
async function format(
inputCode: string,
language: "typescript" | "flow"
): Promise<string> {
return await prettier.format(inputCode, {
semi: true,
parser: language === "typescript" ? "babel-ts" : "flow",
});
}
const TypescriptEvaluatorPresets = getEvaluatorPresets("typescript");
const FlowEvaluatorPresets = getEvaluatorPresets("flow");
export type TransformResult = {
forgetOutput: string;
logs: string | null;
evaluatorCode: {
original: string;
forget: string;
} | null;
};
export async function transformFixtureInput(
input: string,
fixturePath: string,
parseConfigPragmaFn: typeof ParseConfigPragma,
plugin: BabelCore.PluginObj,
includeEvaluator: boolean
): Promise<
{ kind: "ok"; value: TransformResult } | { kind: "err"; msg: string }
> {
const firstLine = input.substring(0, input.indexOf("\n"));
const language = parseLanguage(firstLine);
const filename =
path.basename(fixturePath) + (language === "typescript" ? ".ts" : "");
const inputAst = parseInput(input, filename, language);
const virtualFilepath = "/" + filename;
const presets =
language === "typescript"
? TypescriptEvaluatorPresets
: FlowEvaluatorPresets;
const [options, logs] = makePluginOptions(firstLine, parseConfigPragmaFn);
const forgetResult = transformFromAstSync(inputAst, input, {
filename: virtualFilepath,
highlightCode: false,
retainLines: true,
plugins: [
[plugin, options],
"babel-plugin-fbt",
"babel-plugin-fbt-runtime",
],
sourceType: "module",
ast: includeEvaluator,
cloneInputAst: includeEvaluator,
configFile: false,
babelrc: false,
});
invariant(
forgetResult?.code != null,
"Expected BabelPluginReactForget to codegen successfully."
);
const forgetCode = forgetResult.code;
let evaluatorCode = null;
if (
includeEvaluator &&
!SproutTodoFilter.has(fixturePath) &&
!isExpectError(filename)
) {
let forgetEval: string;
try {
invariant(
forgetResult?.ast != null,
"Expected BabelPluginReactForget ast."
);
const result = transformFromAstSync(forgetResult.ast, forgetCode, {
presets,
filename: virtualFilepath,
configFile: false,
babelrc: false,
});
if (result?.code == null) {
return {
kind: "err",
msg: "Unexpected error in forget transform pipeline - no code emitted",
};
} else {
forgetEval = result.code;
}
} catch (e) {
return {
kind: "err",
msg: "Unexpected error in Forget transform pipeline: " + e.message,
};
}
let originalEval: string;
try {
const result = transformFromAstSync(inputAst, input, {
presets,
filename: virtualFilepath,
configFile: false,
babelrc: false,
});
if (result?.code == null) {
return {
kind: "err",
msg: "Unexpected error in non-forget transform pipeline - no code emitted",
};
} else {
originalEval = result.code;
}
} catch (e) {
return {
kind: "err",
msg: "Unexpected error in non-forget transform pipeline: " + e.message,
};
}
evaluatorCode = {
forget: forgetEval,
original: originalEval,
};
}
const forgetOutput = await format(forgetCode, language);
let formattedLogs = null;
if (logs.length !== 0) {
formattedLogs = logs
.map(({ event }) => {
return JSON.stringify(event);
})
.join("\n");
}
return {
kind: "ok",
value: {
forgetOutput,
logs: formattedLogs,
evaluatorCode,
},
};
}