diff --git a/compiler/apps/playground/components/Editor/ConfigEditor.tsx b/compiler/apps/playground/components/Editor/ConfigEditor.tsx index d922f27c97864..18f904d225f0e 100644 --- a/compiler/apps/playground/components/Editor/ConfigEditor.tsx +++ b/compiler/apps/playground/components/Editor/ConfigEditor.tsx @@ -6,7 +6,6 @@ */ import MonacoEditor, {loader, type Monaco} from '@monaco-editor/react'; -import {PluginOptions} from 'babel-plugin-react-compiler'; import type {editor} from 'monaco-editor'; import * as monaco from 'monaco-editor'; import React, { @@ -18,9 +17,8 @@ import React, { } from 'react'; import {Resizable} from 're-resizable'; import {useStore, useStoreDispatch} from '../StoreContext'; -import {monacoOptions} from './monacoOptions'; +import {monacoConfigOptions} from './monacoOptions'; import {IconChevron} from '../Icons/IconChevron'; -import prettyFormat from 'pretty-format'; import {CONFIG_PANEL_TRANSITION} from '../../lib/transitionTypes'; // @ts-expect-error - webpack asset/source loader handles .d.ts files as strings @@ -29,9 +27,9 @@ import compilerTypeDefs from 'babel-plugin-react-compiler/dist/index.d.ts'; loader.config({monaco}); export default function ConfigEditor({ - appliedOptions, + formattedAppliedConfig, }: { - appliedOptions: PluginOptions | null; + formattedAppliedConfig: string; }): React.ReactElement { const [isExpanded, setIsExpanded] = useState(false); @@ -49,7 +47,7 @@ export default function ConfigEditor({ setIsExpanded(false); }); }} - appliedOptions={appliedOptions} + formattedAppliedConfig={formattedAppliedConfig} />
void; - appliedOptions: PluginOptions | null; + onToggle: (expanded: boolean) => void; + formattedAppliedConfig: string; }): React.ReactElement { const store = useStore(); const dispatchStore = useStoreDispatch(); @@ -122,13 +120,6 @@ function ExpandedEditor({ }); }; - const formattedAppliedOptions = appliedOptions - ? prettyFormat(appliedOptions, { - printFunctionName: false, - printBasicPrototype: false, - }) - : 'Invalid configs'; - return ( @@ -158,7 +149,7 @@ function ExpandedEditor({ Config Overrides
-
+
@@ -186,23 +168,16 @@ function ExpandedEditor({ Applied Configs -
+
diff --git a/compiler/apps/playground/components/Editor/EditorImpl.tsx b/compiler/apps/playground/components/Editor/EditorImpl.tsx index 9f000f85564a2..5b39b91654e27 100644 --- a/compiler/apps/playground/components/Editor/EditorImpl.tsx +++ b/compiler/apps/playground/components/Editor/EditorImpl.tsx @@ -5,312 +5,17 @@ * 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, +import { CompilerErrorDetail, CompilerDiagnostic, - Effect, - ErrorCategory, - parseConfigPragmaForTests, - ValueKind, - type Hook, - PluginOptions, - CompilerPipelineValue, - parsePluginOptions, - printReactiveFunctionWithOutlined, - printFunctionWithOutlined, - type LoggerEvent, } from 'babel-plugin-react-compiler'; -import {useDeferredValue, useMemo} from 'react'; +import {useDeferredValue, useMemo, useState} from 'react'; import {useStore} from '../StoreContext'; import ConfigEditor from './ConfigEditor'; import Input from './Input'; -import { - CompilerOutput, - CompilerTransformOutput, - default as Output, - PrintedCompilerPipelineValue, -} from './Output'; -import {transformFromAstSync} from '@babel/core'; - -function parseInput( - input: string, - language: 'flow' | 'typescript', -): ParseResult { - // 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; - } -} - -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; -} - -function compile( - source: string, - mode: 'compiler' | 'linter', - configOverrides: string, -): [CompilerOutput, 'flow' | 'typescript', PluginOptions | null] { - const results = new Map>(); - const error = new CompilerError(); - const otherErrors: Array = []; - 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, - ]; -} +import {CompilerOutput, default as Output} from './Output'; +import {compile} from '../../lib/compilation'; +import prettyFormat from 'pretty-format'; export default function Editor(): JSX.Element { const store = useStore(); @@ -323,6 +28,7 @@ export default function Editor(): JSX.Element { () => compile(deferredStore.source, 'linter', deferredStore.config), [deferredStore.source, deferredStore.config], ); + const [formattedAppliedConfig, setFormattedAppliedConfig] = useState(''); let mergedOutput: CompilerOutput; let errors: Array; @@ -336,11 +42,22 @@ export default function Editor(): JSX.Element { mergedOutput = compilerOutput; errors = compilerOutput.error.details; } + + if (appliedOptions) { + const formatted = prettyFormat(appliedOptions, { + printFunctionName: false, + printBasicPrototype: false, + }); + if (formatted !== formattedAppliedConfig) { + setFormattedAppliedConfig(formatted); + } + } + return ( <>
- +
diff --git a/compiler/apps/playground/components/Editor/monacoOptions.ts b/compiler/apps/playground/components/Editor/monacoOptions.ts index 7fed1b7875fce..d52c8bbedfa8d 100644 --- a/compiler/apps/playground/components/Editor/monacoOptions.ts +++ b/compiler/apps/playground/components/Editor/monacoOptions.ts @@ -32,3 +32,14 @@ export const monacoOptions: Partial = { tabSize: 2, }; + +export const monacoConfigOptions: Partial = { + ...monacoOptions, + lineNumbers: 'off', + renderLineHighlight: 'none', + overviewRulerBorder: false, + overviewRulerLanes: 0, + fontSize: 12, + scrollBeyondLastLine: false, + glyphMargin: false, +}; diff --git a/compiler/apps/playground/lib/compilation.ts b/compiler/apps/playground/lib/compilation.ts new file mode 100644 index 0000000000000..10bf0164c0e77 --- /dev/null +++ b/compiler/apps/playground/lib/compilation.ts @@ -0,0 +1,308 @@ +/** + * 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 { + // 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; + } +} + +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>(); + const error = new CompilerError(); + const otherErrors: Array = []; + 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, + ]; +}