@samitouri / QOS-React / commits / 9a6e2d078c

[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

Mike Vitousek committed Jul 1, 2024 at 09:05 UTC 9a6e2d078c8478ea57735ab8a48ded4c2177ba9c
6 files changed +112 -35
compiler/apps/playground/components/Editor/EditorImpl.tsx
+34 -21
@@ -5,7 +5,8 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 -import { parse, ParserPlugin } from "@babel/parser";
8 +import { parse as babelParse, ParserPlugin } from "@babel/parser";
9 +import * as HermesParser from "hermes-parser";
10 import traverse, { NodePath } from "@babel/traverse";
11 import * as t from "@babel/types";
12 import {
@@ -42,8 +43,26 @@ import {
43 PrintedCompilerPipelineValue,
44 } from "./Output";
45
46 +function parseInput(input: string, language: "flow" | "typescript") {
47 + // Extract the first line to quickly check for custom test directives
48 + if (language === "flow") {
49 + return HermesParser.parse(input, {
50 + babel: true,
51 + flow: "all",
52 + sourceType: "module",
53 + enableExperimentalComponentSyntax: true,
54 + });
55 + } else {
56 + return babelParse(input, {
57 + plugins: ["typescript", "jsx"],
58 + sourceType: "module",
59 + });
60 + }
61 +}
62 +
63 function parseFunctions(
46 - source: string
64 + source: string,
65 + language: "flow" | "typescript"
66 ): Array<
67 NodePath<
68 t.FunctionDeclaration | t.ArrowFunctionExpression | t.FunctionExpression
@@ -55,20 +74,7 @@ function parseFunctions(
74 >
75 > = [];
76 try {
58 - const isFlow = source
59 - .trim()
60 - .split("\n", 1)[0]
61 - .match(/\s*\/\/\s*\@flow\s*/);
62 - let type_transform: ParserPlugin;
63 - if (isFlow) {
64 - type_transform = "flow";
65 - } else {
66 - type_transform = "typescript";
67 - }
68 - const ast = parse(source, {
69 - plugins: [type_transform, "jsx"],
70 - sourceType: "module",
71 - });
77 + const ast = parseInput(source, language);
78 traverse(ast, {
79 FunctionDeclaration(nodePath) {
80 items.push(nodePath);
@@ -163,7 +169,7 @@ function getReactFunctionType(
169 return "Other";
170 }
171
166 -function compile(source: string): CompilerOutput {
172 +function compile(source: string): [CompilerOutput, "flow" | "typescript"] {
173 const results = new Map<string, PrintedCompilerPipelineValue[]>();
174 const error = new CompilerError();
175 const upsert = (result: PrintedCompilerPipelineValue) => {
@@ -174,12 +180,18 @@ function compile(source: string): CompilerOutput {
180 results.set(result.name, [result]);
181 }
182 };
183 + let language: "flow" | "typescript";
184 + if (source.match(/\@flow/)) {
185 + language = "flow";
186 + } else {
187 + language = "typescript";
188 + }
189 try {
190 // Extract the first line to quickly check for custom test directives
191 const pragma = source.substring(0, source.indexOf("\n"));
192 const config = parseConfigPragma(pragma);
193
182 - for (const fn of parseFunctions(source)) {
194 + for (const fn of parseFunctions(source, language)) {
195 if (!fn.isFunctionDeclaration()) {
196 error.pushErrorDetail(
197 new CompilerErrorDetail({
@@ -279,9 +291,9 @@ function compile(source: string): CompilerOutput {
291 }
292 }
293 if (error.hasErrors()) {
282 - return { kind: "err", results, error: error };
294 + return [{ kind: "err", results, error: error }, language];
295 }
284 - return { kind: "ok", results };
296 + return [{ kind: "ok", results }, language];
297 }
298
299 export default function Editor() {
@@ -289,7 +301,7 @@ export default function Editor() {
301 const deferredStore = useDeferredValue(store);
302 const dispatchStore = useStoreDispatch();
303 const { enqueueSnackbar } = useSnackbar();
292 - const compilerOutput = useMemo(
304 + const [compilerOutput, language] = useMemo(
305 () => compile(deferredStore.source),
306 [deferredStore.source]
307 );
@@ -321,6 +333,7 @@ export default function Editor() {
333 <div className="relative flex basis top-14">
334 <div className={clsx("relative sm:basis-1/4")}>
335 <Input
336 + language={language}
337 errors={
338 compilerOutput.kind === "err" ? compilerOutput.error.details : []
339 }
compiler/apps/playground/components/Editor/Input.tsx
+31 -12
@@ -23,9 +23,10 @@ loader.config({ monaco });
23
24 type Props = {
25 errors: CompilerErrorDetail[];
26 + language: "flow" | "typescript";
27 };
28
28 -export default function Input({ errors }: Props) {
29 +export default function Input({ errors, language }: Props) {
30 const [monaco, setMonaco] = useState<Monaco | null>(null);
31 const store = useStore();
32 const dispatchStore = useStoreDispatch();
@@ -42,6 +43,35 @@ export default function Input({ errors }: Props) {
43 model.updateOptions({ tabSize: 2 });
44 }, [monaco, errors]);
45
46 + const flowDiagnosticDisable = [
47 + 7028 /* unused label */, 6133 /* var declared but not read */,
48 + ];
49 + useEffect(() => {
50 + // Ignore "can only be used in TypeScript files." errors, since
51 + // we want to support syntax highlighting for Flow (*.js) files
52 + // and Flow is not a built-in language.
53 + if (!monaco) return;
54 + monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
55 + diagnosticCodesToIgnore: [
56 + 8002,
57 + 8003,
58 + 8004,
59 + 8005,
60 + 8006,
61 + 8008,
62 + 8009,
63 + 8010,
64 + 8011,
65 + 8012,
66 + 8013,
67 + ...(language === "flow" ? flowDiagnosticDisable : []),
68 + ],
69 + noSemanticValidation: true,
70 + // Monaco can't validate Flow component syntax
71 + noSyntaxValidation: language === "flow",
72 + });
73 + }, [monaco, language]);
74 +
75 const handleChange = (value: string | undefined) => {
76 if (!value) return;
77
@@ -56,17 +86,6 @@ export default function Input({ errors }: Props) {
86 const handleMount = (_: editor.IStandaloneCodeEditor, monaco: Monaco) => {
87 setMonaco(monaco);
88
59 - // Ignore "can only be used in TypeScript files." errors, since
60 - // we want to support syntax highlighting for Flow (*.js) files
61 - // and Flow is not a built-in language.
62 - monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
63 - diagnosticCodesToIgnore: [
64 - 8002, 8003, 8004, 8005, 8006, 8008, 8009, 8010, 8011, 8012, 8013,
65 - ],
66 - noSemanticValidation: true,
67 - noSyntaxValidation: false,
68 - });
69 -
89 const tscOptions = {
90 allowNonTsExtensions: true,
91 target: monaco.languages.typescript.ScriptTarget.ES2015,
compiler/apps/playground/lib/types.d.ts new
+20
@@ -0,0 +1,20 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + */
7 +
8 +// v0.17.1
9 +declare module "hermes-parser" {
10 + type HermesParserOptions = {
11 + allowReturnOutsideFunction?: boolean;
12 + babel?: boolean;
13 + flow?: "all" | "detect";
14 + enableExperimentalComponentSyntax?: boolean;
15 + sourceFilename?: string;
16 + sourceType?: "module" | "script" | "unambiguous";
17 + tokens?: boolean;
18 + };
19 + export function parse(code: string, options: Partial<HermesParserOptions>);
20 +}
compiler/apps/playground/next.config.js
+5
@@ -34,6 +34,11 @@ const nextConfig = {
34 "../../packages/react-compiler-runtime"
35 ),
36 };
37 + config.resolve.fallback = {
38 + fs: false,
39 + path: false,
40 + os: false,
41 + };
42
43 return config;
44 },
compiler/apps/playground/package.json
+5 -2
@@ -24,7 +24,9 @@
24 "@monaco-editor/react": "^4.4.6",
25 "@playwright/test": "^1.42.1",
26 "@use-gesture/react": "^10.2.22",
27 + "fs": "^0.0.1-security",
28 "hermes-eslint": "^0.14.0",
29 + "hermes-parser": "^0.22.0",
30 "invariant": "^2.2.4",
31 "lz-string": "^1.5.0",
32 "monaco-editor": "^0.34.1",
@@ -34,8 +36,8 @@
36 "pretty-format": "^29.3.1",
37 "re-resizable": "^6.9.16",
38 "react": "18.2.0",
37 - "react-dom": "18.2.0",
38 - "react-compiler-runtime": "*"
39 + "react-compiler-runtime": "*",
40 + "react-dom": "18.2.0"
41 },
42 "devDependencies": {
43 "@types/node": "18.11.9",
@@ -46,6 +48,7 @@
48 "clsx": "^1.2.1",
49 "eslint": "^8.28.0",
50 "eslint-config-next": "^13.5.6",
51 + "hermes-parser": "^0.22.0",
52 "monaco-editor-webpack-plugin": "^7.1.0",
53 "postcss": "^8.4.31",
54 "tailwindcss": "^3.2.4"
compiler/yarn.lock
+17
@@ -5410,6 +5410,11 @@ fs.realpath@^1.0.0:
5410 resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
5411 integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
5412
5413 +fs@^0.0.1-security:
5414 + version "0.0.1-security"
5415 + resolved "https://registry.yarnpkg.com/fs/-/fs-0.0.1-security.tgz#8a7bd37186b6dddf3813f23858b57ecaaf5e41d4"
5416 + integrity sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==
5417 +
5418 fsevents@2.3.2, fsevents@^2.3.2, fsevents@~2.3.2:
5419 version "2.3.2"
5420 resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
@@ -5773,6 +5778,11 @@ hermes-estree@0.20.1:
5778 resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.20.1.tgz#0b9a544cf883a779a8e1444b915fa365bef7f72d"
5779 integrity sha512-SQpZK4BzR48kuOg0v4pb3EAGNclzIlqMj3Opu/mu7bbAoFw6oig6cEt/RAi0zTFW/iW6Iz9X9ggGuZTAZ/yZHg==
5780
5781 +hermes-estree@0.22.0:
5782 + version "0.22.0"
5783 + resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.22.0.tgz#38559502b119f728901d2cfe2ef422f277802a1d"
5784 + integrity sha512-FLBt5X9OfA8BERUdc6aZS36Xz3rRuB0Y/mfocSADWEJfomc1xfene33GdyAmtTkKTBXTN/EgAy+rjTKkkZJHlw==
5785 +
5786 hermes-parser@0.14.0:
5787 version "0.14.0"
5788 resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.14.0.tgz#edb2e7172fce996d2c8bbba250d140b70cc1aaaf"
@@ -5808,6 +5818,13 @@ hermes-parser@^0.20.1:
5818 dependencies:
5819 hermes-estree "0.20.1"
5820
5821 +hermes-parser@^0.22.0:
5822 + version "0.22.0"
5823 + resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.22.0.tgz#fc8e0e6c7bfa8db85b04c9f9544a102c4fcb4040"
5824 + integrity sha512-gn5RfZiEXCsIWsFGsKiykekktUoh0PdFWYocXsUdZIyWSckT6UIyPcyyUIPSR3kpnELWeK3n3ztAse7Mat6PSA==
5825 + dependencies:
5826 + hermes-estree "0.22.0"
5827 +
5828 html-encoding-sniffer@^3.0.0:
5829 version "3.0.0"
5830 resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz#2cb1a8cf0db52414776e5b2a7a04d5dd98158de9"