mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Fixed two small issues with the config panel in the compiler playground: 1. Object descriptions were being confined in the config box and most of it would not be visible upon hover 2. Changed it so that "Applied Configs" would only display a valid set of configs, rather than switching between "Invalid Configs" and the set of options. This would be less visually jarring for users as the Output panel already displays errors. Additionally, if users want to see the list of config options but have a currently broken config, they would previously not know how to fix it. Object hover before: <img width="702" height="481" alt="Screenshot 2025-09-26 at 10 41 03 AM" src="https://github.com/user-attachments/assets/b2ddec2f-16ba-41a1-be1f-96211f46764c" /> Hover after: <img width="702" height="481" alt="Screenshot 2025-09-26 at 10 40 37 AM" src="https://github.com/user-attachments/assets/dc713a22-4710-46a8-a5d7-485060cc9074" /> Applied Configs always displays the last valid set of configs: https://github.com/user-attachments/assets/2fb9232f-7388-4488-9b7a-bb48bf09e4ca
309 lines
8.2 KiB
TypeScript
309 lines
8.2 KiB
TypeScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*/
|
|
|
|
import {parse as babelParse, ParseResult} from '@babel/parser';
|
|
import * as HermesParser from 'hermes-parser';
|
|
import * as t from '@babel/types';
|
|
import BabelPluginReactCompiler, {
|
|
CompilerError,
|
|
CompilerErrorDetail,
|
|
CompilerDiagnostic,
|
|
Effect,
|
|
ErrorCategory,
|
|
parseConfigPragmaForTests,
|
|
ValueKind,
|
|
type Hook,
|
|
PluginOptions,
|
|
CompilerPipelineValue,
|
|
parsePluginOptions,
|
|
printReactiveFunctionWithOutlined,
|
|
printFunctionWithOutlined,
|
|
type LoggerEvent,
|
|
} from 'babel-plugin-react-compiler';
|
|
import {transformFromAstSync} from '@babel/core';
|
|
import type {
|
|
CompilerOutput,
|
|
CompilerTransformOutput,
|
|
PrintedCompilerPipelineValue,
|
|
} from '../components/Editor/Output';
|
|
|
|
function parseInput(
|
|
input: string,
|
|
language: 'flow' | 'typescript',
|
|
): ParseResult<t.File> {
|
|
// Extract the first line to quickly check for custom test directives
|
|
if (language === 'flow') {
|
|
return HermesParser.parse(input, {
|
|
babel: true,
|
|
flow: 'all',
|
|
sourceType: 'module',
|
|
enableExperimentalComponentSyntax: true,
|
|
});
|
|
} else {
|
|
return babelParse(input, {
|
|
plugins: ['typescript', 'jsx'],
|
|
sourceType: 'module',
|
|
}) as ParseResult<t.File>;
|
|
}
|
|
}
|
|
|
|
function invokeCompiler(
|
|
source: string,
|
|
language: 'flow' | 'typescript',
|
|
options: PluginOptions,
|
|
): CompilerTransformOutput {
|
|
const ast = parseInput(source, language);
|
|
let result = transformFromAstSync(ast, source, {
|
|
filename: '_playgroundFile.js',
|
|
highlightCode: false,
|
|
retainLines: true,
|
|
plugins: [[BabelPluginReactCompiler, options]],
|
|
ast: true,
|
|
sourceType: 'module',
|
|
configFile: false,
|
|
sourceMaps: true,
|
|
babelrc: false,
|
|
});
|
|
if (result?.ast == null || result?.code == null || result?.map == null) {
|
|
throw new Error('Expected successful compilation');
|
|
}
|
|
return {
|
|
code: result.code,
|
|
sourceMaps: result.map,
|
|
language,
|
|
};
|
|
}
|
|
|
|
const COMMON_HOOKS: Array<[string, Hook]> = [
|
|
[
|
|
'useFragment',
|
|
{
|
|
valueKind: ValueKind.Frozen,
|
|
effectKind: Effect.Freeze,
|
|
noAlias: true,
|
|
transitiveMixedData: true,
|
|
},
|
|
],
|
|
[
|
|
'usePaginationFragment',
|
|
{
|
|
valueKind: ValueKind.Frozen,
|
|
effectKind: Effect.Freeze,
|
|
noAlias: true,
|
|
transitiveMixedData: true,
|
|
},
|
|
],
|
|
[
|
|
'useRefetchableFragment',
|
|
{
|
|
valueKind: ValueKind.Frozen,
|
|
effectKind: Effect.Freeze,
|
|
noAlias: true,
|
|
transitiveMixedData: true,
|
|
},
|
|
],
|
|
[
|
|
'useLazyLoadQuery',
|
|
{
|
|
valueKind: ValueKind.Frozen,
|
|
effectKind: Effect.Freeze,
|
|
noAlias: true,
|
|
transitiveMixedData: true,
|
|
},
|
|
],
|
|
[
|
|
'usePreloadedQuery',
|
|
{
|
|
valueKind: ValueKind.Frozen,
|
|
effectKind: Effect.Freeze,
|
|
noAlias: true,
|
|
transitiveMixedData: true,
|
|
},
|
|
],
|
|
];
|
|
|
|
function parseOptions(
|
|
source: string,
|
|
mode: 'compiler' | 'linter',
|
|
configOverrides: string,
|
|
): PluginOptions {
|
|
// Extract the first line to quickly check for custom test directives
|
|
const pragma = source.substring(0, source.indexOf('\n'));
|
|
|
|
const parsedPragmaOptions = parseConfigPragmaForTests(pragma, {
|
|
compilationMode: 'infer',
|
|
environment:
|
|
mode === 'linter'
|
|
? {
|
|
// enabled in compiler
|
|
validateRefAccessDuringRender: false,
|
|
// enabled in linter
|
|
validateNoSetStateInRender: true,
|
|
validateNoSetStateInEffects: true,
|
|
validateNoJSXInTryStatements: true,
|
|
validateNoImpureFunctionsInRender: true,
|
|
validateStaticComponents: true,
|
|
validateNoFreezingKnownMutableFunctions: true,
|
|
validateNoVoidUseMemo: true,
|
|
}
|
|
: {
|
|
/* use defaults for compiler mode */
|
|
},
|
|
});
|
|
|
|
// Parse config overrides from config editor
|
|
let configOverrideOptions: any = {};
|
|
const configMatch = configOverrides.match(/^\s*import.*?\n\n\((.*)\)/s);
|
|
if (configOverrides.trim()) {
|
|
if (configMatch && configMatch[1]) {
|
|
const configString = configMatch[1].replace(/satisfies.*$/, '').trim();
|
|
configOverrideOptions = new Function(`return (${configString})`)();
|
|
} else {
|
|
throw new Error('Invalid override format');
|
|
}
|
|
}
|
|
|
|
const opts: PluginOptions = parsePluginOptions({
|
|
...parsedPragmaOptions,
|
|
...configOverrideOptions,
|
|
environment: {
|
|
...parsedPragmaOptions.environment,
|
|
...configOverrideOptions.environment,
|
|
customHooks: new Map([...COMMON_HOOKS]),
|
|
},
|
|
});
|
|
|
|
return opts;
|
|
}
|
|
|
|
export function compile(
|
|
source: string,
|
|
mode: 'compiler' | 'linter',
|
|
configOverrides: string,
|
|
): [CompilerOutput, 'flow' | 'typescript', PluginOptions | null] {
|
|
const results = new Map<string, Array<PrintedCompilerPipelineValue>>();
|
|
const error = new CompilerError();
|
|
const otherErrors: Array<CompilerErrorDetail | CompilerDiagnostic> = [];
|
|
const upsert: (result: PrintedCompilerPipelineValue) => void = result => {
|
|
const entry = results.get(result.name);
|
|
if (Array.isArray(entry)) {
|
|
entry.push(result);
|
|
} else {
|
|
results.set(result.name, [result]);
|
|
}
|
|
};
|
|
let language: 'flow' | 'typescript';
|
|
if (source.match(/\@flow/)) {
|
|
language = 'flow';
|
|
} else {
|
|
language = 'typescript';
|
|
}
|
|
let transformOutput;
|
|
|
|
let baseOpts: PluginOptions | null = null;
|
|
try {
|
|
baseOpts = parseOptions(source, mode, configOverrides);
|
|
} catch (err) {
|
|
error.details.push(
|
|
new CompilerErrorDetail({
|
|
category: ErrorCategory.Config,
|
|
reason: `Unexpected failure when transforming configs! \n${err}`,
|
|
loc: null,
|
|
suggestions: null,
|
|
}),
|
|
);
|
|
}
|
|
if (baseOpts) {
|
|
try {
|
|
const logIR = (result: CompilerPipelineValue): void => {
|
|
switch (result.kind) {
|
|
case 'ast': {
|
|
break;
|
|
}
|
|
case 'hir': {
|
|
upsert({
|
|
kind: 'hir',
|
|
fnName: result.value.id,
|
|
name: result.name,
|
|
value: printFunctionWithOutlined(result.value),
|
|
});
|
|
break;
|
|
}
|
|
case 'reactive': {
|
|
upsert({
|
|
kind: 'reactive',
|
|
fnName: result.value.id,
|
|
name: result.name,
|
|
value: printReactiveFunctionWithOutlined(result.value),
|
|
});
|
|
break;
|
|
}
|
|
case 'debug': {
|
|
upsert({
|
|
kind: 'debug',
|
|
fnName: null,
|
|
name: result.name,
|
|
value: result.value,
|
|
});
|
|
break;
|
|
}
|
|
default: {
|
|
const _: never = result;
|
|
throw new Error(`Unhandled result ${result}`);
|
|
}
|
|
}
|
|
};
|
|
// Add logger options to the parsed options
|
|
const opts = {
|
|
...baseOpts,
|
|
logger: {
|
|
debugLogIRs: logIR,
|
|
logEvent: (_filename: string | null, event: LoggerEvent): void => {
|
|
if (event.kind === 'CompileError') {
|
|
otherErrors.push(event.detail);
|
|
}
|
|
},
|
|
},
|
|
};
|
|
transformOutput = invokeCompiler(source, language, opts);
|
|
} catch (err) {
|
|
/**
|
|
* error might be an invariant violation or other runtime error
|
|
* (i.e. object shape that is not CompilerError)
|
|
*/
|
|
if (err instanceof CompilerError && err.details.length > 0) {
|
|
error.merge(err);
|
|
} else {
|
|
/**
|
|
* Handle unexpected failures by logging (to get a stack trace)
|
|
* and reporting
|
|
*/
|
|
error.details.push(
|
|
new CompilerErrorDetail({
|
|
category: ErrorCategory.Invariant,
|
|
reason: `Unexpected failure when transforming input! \n${err}`,
|
|
loc: null,
|
|
suggestions: null,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
// Only include logger errors if there weren't other errors
|
|
if (!error.hasErrors() && otherErrors.length !== 0) {
|
|
otherErrors.forEach(e => error.details.push(e));
|
|
}
|
|
if (error.hasErrors()) {
|
|
return [{kind: 'err', results, error}, language, baseOpts];
|
|
}
|
|
return [
|
|
{kind: 'ok', results, transformOutput, errors: error.details},
|
|
language,
|
|
baseOpts,
|
|
];
|
|
}
|