From 81e5cf94d26854e0c09e129b25b57869a4356858 Mon Sep 17 00:00:00 2001 From: Mike Vitousek Date: Sun, 30 Jun 2024 15:47:20 -0700 Subject: [PATCH] [compiler] Flow support for playground Summary: The playground currently has limited support for Flow files--it tries to parse them if the // flow sigil is on the fist line, but this is often not the case for files one would like to inspect in practice. more importantly, component syntax isn't supported even then, because it depends on the Hermes parser. This diff improves the state of flow support in the playground to make it more useful: when we see `flow` anywhere in the file, we'll assume it's a flow file, parse it with the Hermes parser, and disable typescript-specific features of Monaco editor. ghstack-source-id: b99b1568d7de602dd70d8cf1d8110d62530cf43b Pull Request resolved: https://github.com/facebook/react/pull/30150 --- .../components/Editor/EditorImpl.tsx | 55 ++++++++++++------- .../playground/components/Editor/Input.tsx | 43 +++++++++++---- compiler/apps/playground/lib/types.d.ts | 20 +++++++ compiler/apps/playground/next.config.js | 5 ++ compiler/apps/playground/package.json | 7 ++- compiler/yarn.lock | 17 ++++++ 6 files changed, 112 insertions(+), 35 deletions(-) create mode 100644 compiler/apps/playground/lib/types.d.ts diff --git a/compiler/apps/playground/components/Editor/EditorImpl.tsx b/compiler/apps/playground/components/Editor/EditorImpl.tsx index e8a1177b4e439..312492b48c3c1 100644 --- a/compiler/apps/playground/components/Editor/EditorImpl.tsx +++ b/compiler/apps/playground/components/Editor/EditorImpl.tsx @@ -5,7 +5,8 @@ * LICENSE file in the root directory of this source tree. */ -import { parse, ParserPlugin } from "@babel/parser"; +import { parse as babelParse, ParserPlugin } from "@babel/parser"; +import * as HermesParser from "hermes-parser"; import traverse, { NodePath } from "@babel/traverse"; import * as t from "@babel/types"; import { @@ -42,8 +43,26 @@ import { PrintedCompilerPipelineValue, } from "./Output"; +function parseInput(input: string, language: "flow" | "typescript") { + // 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", + }); + } +} + function parseFunctions( - source: string + source: string, + language: "flow" | "typescript" ): Array< NodePath< t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression @@ -55,20 +74,7 @@ function parseFunctions( > > = []; try { - const isFlow = source - .trim() - .split("\n", 1)[0] - .match(/\s*\/\/\s*\@flow\s*/); - let type_transform: ParserPlugin; - if (isFlow) { - type_transform = "flow"; - } else { - type_transform = "typescript"; - } - const ast = parse(source, { - plugins: [type_transform, "jsx"], - sourceType: "module", - }); + const ast = parseInput(source, language); traverse(ast, { FunctionDeclaration(nodePath) { items.push(nodePath); @@ -163,7 +169,7 @@ function getReactFunctionType( return "Other"; } -function compile(source: string): CompilerOutput { +function compile(source: string): [CompilerOutput, "flow" | "typescript"] { const results = new Map(); const error = new CompilerError(); const upsert = (result: PrintedCompilerPipelineValue) => { @@ -174,12 +180,18 @@ function compile(source: string): CompilerOutput { results.set(result.name, [result]); } }; + let language: "flow" | "typescript"; + if (source.match(/\@flow/)) { + language = "flow"; + } else { + language = "typescript"; + } try { // Extract the first line to quickly check for custom test directives const pragma = source.substring(0, source.indexOf("\n")); const config = parseConfigPragma(pragma); - for (const fn of parseFunctions(source)) { + for (const fn of parseFunctions(source, language)) { if (!fn.isFunctionDeclaration()) { error.pushErrorDetail( new CompilerErrorDetail({ @@ -279,9 +291,9 @@ function compile(source: string): CompilerOutput { } } if (error.hasErrors()) { - return { kind: "err", results, error: error }; + return [{ kind: "err", results, error: error }, language]; } - return { kind: "ok", results }; + return [{ kind: "ok", results }, language]; } export default function Editor() { @@ -289,7 +301,7 @@ export default function Editor() { const deferredStore = useDeferredValue(store); const dispatchStore = useStoreDispatch(); const { enqueueSnackbar } = useSnackbar(); - const compilerOutput = useMemo( + const [compilerOutput, language] = useMemo( () => compile(deferredStore.source), [deferredStore.source] ); @@ -321,6 +333,7 @@ export default function Editor() {
(null); const store = useStore(); const dispatchStore = useStoreDispatch(); @@ -42,6 +43,35 @@ export default function Input({ errors }: Props) { model.updateOptions({ tabSize: 2 }); }, [monaco, errors]); + const flowDiagnosticDisable = [ + 7028 /* unused label */, 6133 /* var declared but not read */, + ]; + useEffect(() => { + // Ignore "can only be used in TypeScript files." errors, since + // we want to support syntax highlighting for Flow (*.js) files + // and Flow is not a built-in language. + if (!monaco) return; + monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ + diagnosticCodesToIgnore: [ + 8002, + 8003, + 8004, + 8005, + 8006, + 8008, + 8009, + 8010, + 8011, + 8012, + 8013, + ...(language === "flow" ? flowDiagnosticDisable : []), + ], + noSemanticValidation: true, + // Monaco can't validate Flow component syntax + noSyntaxValidation: language === "flow", + }); + }, [monaco, language]); + const handleChange = (value: string | undefined) => { if (!value) return; @@ -56,17 +86,6 @@ export default function Input({ errors }: Props) { const handleMount = (_: editor.IStandaloneCodeEditor, monaco: Monaco) => { setMonaco(monaco); - // Ignore "can only be used in TypeScript files." errors, since - // we want to support syntax highlighting for Flow (*.js) files - // and Flow is not a built-in language. - monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ - diagnosticCodesToIgnore: [ - 8002, 8003, 8004, 8005, 8006, 8008, 8009, 8010, 8011, 8012, 8013, - ], - noSemanticValidation: true, - noSyntaxValidation: false, - }); - const tscOptions = { allowNonTsExtensions: true, target: monaco.languages.typescript.ScriptTarget.ES2015, diff --git a/compiler/apps/playground/lib/types.d.ts b/compiler/apps/playground/lib/types.d.ts new file mode 100644 index 0000000000000..40771d183c06b --- /dev/null +++ b/compiler/apps/playground/lib/types.d.ts @@ -0,0 +1,20 @@ +/** + * 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. + */ + +// v0.17.1 +declare module "hermes-parser" { + type HermesParserOptions = { + allowReturnOutsideFunction?: boolean; + babel?: boolean; + flow?: "all" | "detect"; + enableExperimentalComponentSyntax?: boolean; + sourceFilename?: string; + sourceType?: "module" | "script" | "unambiguous"; + tokens?: boolean; + }; + export function parse(code: string, options: Partial); +} diff --git a/compiler/apps/playground/next.config.js b/compiler/apps/playground/next.config.js index ddb2f958caedf..d288d71153124 100644 --- a/compiler/apps/playground/next.config.js +++ b/compiler/apps/playground/next.config.js @@ -34,6 +34,11 @@ const nextConfig = { "../../packages/react-compiler-runtime" ), }; + config.resolve.fallback = { + fs: false, + path: false, + os: false, + }; return config; }, diff --git a/compiler/apps/playground/package.json b/compiler/apps/playground/package.json index c5cd50e24b1b0..55f146fe2591c 100644 --- a/compiler/apps/playground/package.json +++ b/compiler/apps/playground/package.json @@ -24,7 +24,9 @@ "@monaco-editor/react": "^4.4.6", "@playwright/test": "^1.42.1", "@use-gesture/react": "^10.2.22", + "fs": "^0.0.1-security", "hermes-eslint": "^0.14.0", + "hermes-parser": "^0.22.0", "invariant": "^2.2.4", "lz-string": "^1.5.0", "monaco-editor": "^0.34.1", @@ -34,8 +36,8 @@ "pretty-format": "^29.3.1", "re-resizable": "^6.9.16", "react": "18.2.0", - "react-dom": "18.2.0", - "react-compiler-runtime": "*" + "react-compiler-runtime": "*", + "react-dom": "18.2.0" }, "devDependencies": { "@types/node": "18.11.9", @@ -46,6 +48,7 @@ "clsx": "^1.2.1", "eslint": "^8.28.0", "eslint-config-next": "^13.5.6", + "hermes-parser": "^0.22.0", "monaco-editor-webpack-plugin": "^7.1.0", "postcss": "^8.4.31", "tailwindcss": "^3.2.4" diff --git a/compiler/yarn.lock b/compiler/yarn.lock index 6df7539281795..f54b9be27d474 100644 --- a/compiler/yarn.lock +++ b/compiler/yarn.lock @@ -5410,6 +5410,11 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== +fs@^0.0.1-security: + version "0.0.1-security" + resolved "https://registry.yarnpkg.com/fs/-/fs-0.0.1-security.tgz#8a7bd37186b6dddf3813f23858b57ecaaf5e41d4" + integrity sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w== + fsevents@2.3.2, fsevents@^2.3.2, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" @@ -5773,6 +5778,11 @@ hermes-estree@0.20.1: resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.20.1.tgz#0b9a544cf883a779a8e1444b915fa365bef7f72d" integrity sha512-SQpZK4BzR48kuOg0v4pb3EAGNclzIlqMj3Opu/mu7bbAoFw6oig6cEt/RAi0zTFW/iW6Iz9X9ggGuZTAZ/yZHg== +hermes-estree@0.22.0: + version "0.22.0" + resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.22.0.tgz#38559502b119f728901d2cfe2ef422f277802a1d" + integrity sha512-FLBt5X9OfA8BERUdc6aZS36Xz3rRuB0Y/mfocSADWEJfomc1xfene33GdyAmtTkKTBXTN/EgAy+rjTKkkZJHlw== + hermes-parser@0.14.0: version "0.14.0" resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.14.0.tgz#edb2e7172fce996d2c8bbba250d140b70cc1aaaf" @@ -5808,6 +5818,13 @@ hermes-parser@^0.20.1: dependencies: hermes-estree "0.20.1" +hermes-parser@^0.22.0: + version "0.22.0" + resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.22.0.tgz#fc8e0e6c7bfa8db85b04c9f9544a102c4fcb4040" + integrity sha512-gn5RfZiEXCsIWsFGsKiykekktUoh0PdFWYocXsUdZIyWSckT6UIyPcyyUIPSR3kpnELWeK3n3ztAse7Mat6PSA== + dependencies: + hermes-estree "0.22.0" + html-encoding-sniffer@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9"