@samitouri / QOS-React-2 / commits / 9d935db383

Render error markers in playground

Additionally with this commit we now support multiple top level functions ![Screenshot 2023-01-24 at 12 01 34 PM](https://user-images.githubusercontent.com/1390709/214359278-7160b612-094d-4498-918d-944d30d61366.png) ![Screenshot 2023-01-24 at 12 01 21 PM](https://user-images.githubusercontent.com/1390709/214359275-d776c416-badb-4989-8952-dec1b1f3f7ed.png)

Lauren Tan committed Jan 24, 2023 at 12:44 UTC 9d935db383204e1ae9a200a1fdc5deb81624c2ca
7 files changed +246 -108
compiler/forget/packages/playground/components/Editor/Input.tsx
+9 -2
@@ -3,16 +3,22 @@
3 */
4
5 import MonacoEditor, { type Monaco } from "@monaco-editor/react";
6 +import { CompilerErrorDetail } from "babel-plugin-react-forget";
7 import invariant from "invariant";
8 import type { editor } from "monaco-editor";
9 import { useEffect, useState } from "react";
10 +import { renderForgetMarkers } from "../../lib/forgetMonacoDiagnostics";
11 import { useStore, useStoreDispatch } from "../StoreContext";
12 import { monacoOptions } from "./monacoOptions";
13 // TODO: Make TS recognize .d.ts files, in addition to loading them with webpack.
14 // @ts-ignore
15 import React$Types from "../../node_modules/@types/react/index.d.ts";
16
15 -export default function Input({}: {}) {
17 +type Props = {
18 + errors: CompilerErrorDetail[];
19 +};
20 +
21 +export default function Input({ errors }: Props) {
22 const [monaco, setMonaco] = useState<Monaco | null>(null);
23 const store = useStore();
24 const dispatchStore = useStoreDispatch();
@@ -23,10 +29,11 @@ export default function Input({}: {}) {
29 const uri = monaco.Uri.parse(`file:///index.js`);
30 const model = monaco.editor.getModel(uri);
31 invariant(model, "Model must exist for the selected input file.");
32 + renderForgetMarkers({ monaco, model, details: errors });
33 // N.B. that `tabSize` is a model property, not an editor property.
34 // So, the tab size has to be set per model.
35 model.updateOptions({ tabSize: 2 });
29 - }, [monaco]);
36 + }, [monaco, errors]);
37
38 const handleChange = (value: string | undefined) => {
39 if (!value) return;
compiler/forget/packages/playground/components/Editor/Output.tsx
+82 -98
@@ -6,121 +6,104 @@
6 */
7
8 import generate from "@babel/generator";
9 -import { parse } from "@babel/parser";
10 -import traverse, { NodePath } from "@babel/traverse";
9 import * as t from "@babel/types";
10 import MonacoEditor from "@monaco-editor/react";
11 import {
12 printHIR,
13 printReactiveFunction,
16 - run,
14 + type CompilerError,
15 + type CompilerPipelineValue,
16 } from "babel-plugin-react-forget";
17 import prettier from "prettier";
18 import prettierParserBabel from "prettier/parser-babel";
19 import { memo, useMemo, useState } from "react";
21 -import type { Store } from "../../lib/stores";
20 +import { type Store } from "../../lib/stores";
21 import TabbedWindow from "../TabbedWindow";
22 import { monacoOptions } from "./monacoOptions";
23 const MemoizedOutput = memo(Output);
24
25 export default MemoizedOutput;
26
27 +export type CompilerOutput =
28 + | { kind: "ok"; results: Map<string, CompilerPipelineValue[]> }
29 + | { kind: "err"; error: CompilerError };
30 +
31 type Props = {
32 store: Store;
33 + compilerOutput: CompilerOutput;
34 };
35
32 -type CompilerError = string;
33 -
34 -function parseFunctions(
35 - source: string
36 -): Array<NodePath<t.FunctionDeclaration>> {
37 - try {
38 - const ast = parse(source, {
39 - plugins: ["typescript", "jsx"],
40 - sourceType: "module",
41 - });
42 - const items: Array<NodePath<t.FunctionDeclaration>> = [];
43 - traverse(ast, {
44 - FunctionDeclaration: {
45 - enter(nodePath) {
46 - items.push(nodePath);
47 - },
48 - },
49 - });
50 - return items;
51 - } catch (e) {
52 - return [];
53 - }
54 -}
55 -
56 -function compile(source: string): Map<string, React.ReactNode> | CompilerError {
57 - try {
58 - const astFunctions = parseFunctions(source);
59 - if (astFunctions.length === 0) {
60 - return "";
61 - }
62 -
63 - // TODO: Handle multiple functions
64 - const func = astFunctions[0];
65 - const tabs = new Map<string, React.ReactNode>();
66 - let ast: t.Function | null = null;
67 - for (const result of run(func)) {
36 +function tabify(source: string, compilerOutput: CompilerOutput) {
37 + const tabs = new Map<string, React.ReactNode>();
38 + const reorderedTabs = new Map<string, React.ReactNode>();
39 + const concattedResults = new Map<string, string>();
40 + let topLevelFnDecls: Array<t.FunctionDeclaration> = [];
41 + if (compilerOutput.kind === "err") return reorderedTabs;
42 + // Concat all top level function declaration results into a single tab for each pass
43 + for (const [passName, results] of compilerOutput.results) {
44 + for (const result of results) {
45 switch (result.kind) {
69 - case "ast": {
70 - ast = result.value;
71 - break;
72 - }
46 case "hir": {
74 - const text = printHIR(result.value.body);
75 - tabs.set(
76 - result.name,
77 - <TextTabContent output={text}></TextTabContent>
78 - );
47 + const prev = concattedResults.get(result.name);
48 + const next = printHIR(result.value.body);
49 + const identName = `function ${result.value.id?.name}`;
50 + if (prev != null) {
51 + concattedResults.set(passName, `${prev}\n\n${identName}\n${next}`);
52 + } else {
53 + concattedResults.set(passName, `${identName}\n${next}`);
54 + }
55 break;
56 }
57 case "reactive": {
82 - const text = printReactiveFunction(result.value);
83 - tabs.set(
84 - result.name,
85 - <TextTabContent output={text}></TextTabContent>
86 - );
58 + const prev = concattedResults.get(passName);
59 + const next = printReactiveFunction(result.value);
60 + if (prev != null) {
61 + concattedResults.set(passName, `${prev}\n\n${next}`);
62 + } else {
63 + concattedResults.set(passName, next);
64 + }
65 break;
66 }
67 + case "ast":
68 + topLevelFnDecls.push(result.value);
69 + break;
70 default: {
71 throw new Error("Unexpected result kind");
72 }
73 }
74 }
94 - // Ensure that JS and the JS source map come first
95 - const reorderedTabs = new Map();
96 - if (ast !== null) {
97 - const { code, sourceMapUrl } = codegen(ast, source);
98 - reorderedTabs.set("JS", <TextTabContent output={code}></TextTabContent>);
99 - if (sourceMapUrl) {
100 - reorderedTabs.set(
101 - "SourceMap",
102 - <>
103 - <iframe
104 - src={sourceMapUrl}
105 - className="w-full h-96"
106 - title="Generated Code"
107 - />
108 - </>
109 - );
110 - }
75 + }
76 + for (const [passName, text] of concattedResults) {
77 + tabs.set(passName, <TextTabContent output={text}></TextTabContent>);
78 + }
79 + // Ensure that JS and the JS source map come first
80 + if (topLevelFnDecls.length > 0) {
81 + // Make a synthetic Program so we can have a single AST with all the top level
82 + // FunctionDeclarations
83 + const ast = t.program(topLevelFnDecls);
84 + const { code, sourceMapUrl } = codegen(ast, source);
85 + reorderedTabs.set("JS", <TextTabContent output={code}></TextTabContent>);
86 + if (sourceMapUrl) {
87 + reorderedTabs.set(
88 + "SourceMap",
89 + <>
90 + <iframe
91 + src={sourceMapUrl}
92 + className="w-full h-96"
93 + title="Generated Code"
94 + />
95 + </>
96 + );
97 }
112 - tabs.forEach((tab, name) => {
113 - reorderedTabs.set(name, tab);
114 - });
115 - return reorderedTabs;
116 - } catch (e: any) {
117 - console.error(e);
118 - return e.toString();
98 }
99 + tabs.forEach((tab, name) => {
100 + reorderedTabs.set(name, tab);
101 + });
102 + return reorderedTabs;
103 }
104
105 function codegen(
123 - ast: any,
106 + ast: t.Program,
107 source: string
108 ): { code: any; sourceMapUrl: string | null } {
109 const generated = generate(
@@ -140,16 +123,29 @@ function codegen(
123 return { code: codegenOutput, sourceMapUrl };
124 }
125
143 -// TODO(gsn: Update diagnostics ƒrom HIR output
144 -function Output({ store }: Props) {
126 +function utf16ToUTF8(s: string): string {
127 + return unescape(encodeURIComponent(s));
128 +}
129 +
130 +function getSourceMapUrl(code: string, map: string): string | null {
131 + code = utf16ToUTF8(code);
132 + map = utf16ToUTF8(map);
133 + return `https://evanw.github.io/source-map-visualization/#${btoa(
134 + `${code.length}\0${code}${map.length}\0${map}`
135 + )}`;
136 +}
137 +
138 +function Output({ store, compilerOutput }: Props) {
139 const [tabsOpen, setTabsOpen] = useState<Set<string>>(() => new Set());
146 - const compilerOutput = useMemo(() => compile(store.source), [store.source]);
140 + const tabs = useMemo(
141 + () => tabify(store.source, compilerOutput),
142 + [store.source, compilerOutput]
143 + );
144
148 - if (typeof compilerOutput === "string") {
149 - if (compilerOutput === "") return <></>;
145 + if (compilerOutput.kind === "err") {
146 return (
147 <pre>
152 - <code>${compilerOutput}</code>
148 + <code>{compilerOutput.error.toString()}</code>
149 </pre>
150 );
151 }
@@ -159,23 +155,11 @@ function Output({ store }: Props) {
155 defaultTab="HIR"
156 setTabsOpen={setTabsOpen}
157 tabsOpen={tabsOpen}
162 - tabs={compilerOutput}
158 + tabs={tabs}
159 />
160 );
161 }
162
167 -function utf16ToUTF8(s: string): string {
168 - return unescape(encodeURIComponent(s));
169 -}
170 -
171 -function getSourceMapUrl(code: string, map: string): string | null {
172 - code = utf16ToUTF8(code);
173 - map = utf16ToUTF8(map);
174 - return `https://evanw.github.io/source-map-visualization/#${btoa(
175 - `${code.length}\0${code}${map.length}\0${map}`
176 - )}`;
177 -}
178 -
163 function TextTabContent({ output }: { output: string }) {
164 return (
165 // Restrict MonacoEditor's height, since the config autoLayout:true
compiler/forget/packages/playground/components/Editor/index.tsx
+61 -4
@@ -5,10 +5,14 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 +import { parse } from "@babel/parser";
9 +import traverse, { NodePath } from "@babel/traverse";
10 +import * as t from "@babel/types";
11 +import { CompilerPipelineValue, run } from "babel-plugin-react-forget";
12 import clsx from "clsx";
13 import invariant from "invariant";
14 import { useSnackbar } from "notistack";
11 -import { useDeferredValue } from "react";
15 +import { useDeferredValue, useMemo } from "react";
16 import { useMountEffect } from "../../hooks";
17 import { defaultStore } from "../../lib/defaultStore";
18 import {
@@ -20,13 +24,62 @@ import {
24 } from "../../lib/stores";
25 import { useStore, useStoreDispatch } from "../StoreContext";
26 import Input from "./Input";
23 -import Output from "./Output";
27 +import { CompilerOutput, default as Output } from "./Output";
28 +
29 +function parseFunctions(
30 + source: string
31 +): Array<NodePath<t.FunctionDeclaration>> {
32 + const items: Array<NodePath<t.FunctionDeclaration>> = [];
33 + try {
34 + const ast = parse(source, {
35 + plugins: ["typescript", "jsx"],
36 + sourceType: "module",
37 + });
38 + traverse(ast, {
39 + FunctionDeclaration: {
40 + enter(nodePath) {
41 + items.push(nodePath);
42 + },
43 + },
44 + });
45 + } catch (e) {
46 + console.error(e);
47 + }
48 + return items;
49 +}
50 +
51 +function compile(source: string): CompilerOutput {
52 + try {
53 + const results = new Map<string, CompilerPipelineValue[]>();
54 + const upsert = (result: CompilerPipelineValue) => {
55 + const entry = results.get(result.name);
56 + if (Array.isArray(entry)) {
57 + entry.push(result);
58 + } else {
59 + results.set(result.name, [result]);
60 + }
61 + };
62 + for (const fn of parseFunctions(source)) {
63 + for (const result of run(fn)) {
64 + upsert(result);
65 + }
66 + }
67 + return { kind: "ok", results };
68 + } catch (error: any) {
69 + console.error(error);
70 + return { kind: "err", error };
71 + }
72 +}
73
74 export default function Editor() {
75 const store = useStore();
76 const deferredStore = useDeferredValue(store);
77 const dispatchStore = useStoreDispatch();
78 const { enqueueSnackbar } = useSnackbar();
79 + const compilerOutput = useMemo(
80 + () => compile(deferredStore.source),
81 + [deferredStore.source]
82 + );
83
84 useMountEffect(() => {
85 let mountStore: Store;
@@ -57,10 +110,14 @@ export default function Editor() {
110 style={{ minWidth: 650 }}
111 className={clsx("relative sm:basis-1/4")}
112 >
60 - <Input />
113 + <Input
114 + errors={
115 + compilerOutput.kind === "err" ? compilerOutput.error.details : []
116 + }
117 + />
118 </div>
119 <div className={clsx("flex sm:flex")}>
63 - <Output store={deferredStore} />
120 + <Output store={deferredStore} compilerOutput={compilerOutput} />
121 </div>
122 </div>
123 </>
compiler/forget/packages/playground/lib/forgetMonacoDiagnostics.ts new
+84
@@ -0,0 +1,84 @@
1 +/*
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + */
4 +
5 +import { Monaco } from "@monaco-editor/react";
6 +import { CompilerErrorDetail, ErrorSeverity } from "babel-plugin-react-forget";
7 +import { MarkerSeverity, type editor } from "monaco-editor";
8 +
9 +function mapForgetSeverityToMonaco(
10 + level: ErrorSeverity,
11 + monaco: Monaco
12 +): MarkerSeverity {
13 + switch (level) {
14 + case ErrorSeverity.Todo:
15 + case ErrorSeverity.InvalidInput:
16 + return monaco.MarkerSeverity.Error;
17 + default:
18 + return monaco.MarkerSeverity.Warning;
19 + }
20 +}
21 +
22 +function mapForgetDiagnosticToMonacoMarker(
23 + detail: CompilerErrorDetail,
24 + monaco: Monaco
25 +): editor.IMarkerData | null {
26 + if (detail.nodePath == null || detail.nodePath.node?.loc == null) {
27 + return null;
28 + }
29 + const severity = mapForgetSeverityToMonaco(detail.severity, monaco);
30 + let message = detail.errorMessage;
31 + return {
32 + severity,
33 + message,
34 + startLineNumber: detail.nodePath.node.loc.start.line,
35 + startColumn: detail.nodePath.node.loc.start.column + 1,
36 + endLineNumber: detail.nodePath.node.loc.end.line,
37 + endColumn: detail.nodePath.node.loc.end.column + 1,
38 + };
39 +}
40 +
41 +type ForgetMarkerConfig = {
42 + monaco: Monaco;
43 + model: editor.ITextModel;
44 + details: CompilerErrorDetail[];
45 +};
46 +let decorations: string[] = [];
47 +export function renderForgetMarkers({
48 + monaco,
49 + model,
50 + details,
51 +}: ForgetMarkerConfig): void {
52 + let markers = [];
53 + for (const detail of details) {
54 + const marker = mapForgetDiagnosticToMonacoMarker(detail, monaco);
55 + if (marker == null) {
56 + continue;
57 + }
58 + markers.push(marker);
59 + }
60 + if (markers.length > 0) {
61 + monaco.editor.setModelMarkers(model, "owner", markers);
62 + const newDecorations = markers.map((marker) => {
63 + return {
64 + range: new monaco.Range(
65 + marker.startLineNumber,
66 + marker.startColumn,
67 + marker.endLineNumber,
68 + marker.endColumn
69 + ),
70 + options: {
71 + isWholeLine: true,
72 + glyphMarginClassName: "bg-red-300",
73 + },
74 + };
75 + });
76 + decorations = model.deltaDecorations(decorations, newDecorations);
77 + } else {
78 + monaco.editor.setModelMarkers(model, "owner", []);
79 + decorations = model.deltaDecorations(
80 + model.getAllDecorations().map((d) => d.id),
81 + []
82 + );
83 + }
84 +}
compiler/forget/src/CompilerPipeline.ts
+1 -1
@@ -39,7 +39,7 @@ import { logHIRFunction, logReactiveFunction } from "./Utils/logger";
39 import { assertExhaustive } from "./Utils/utils";
40
41 export type CompilerPipelineValue =
42 - | { kind: "ast"; name: string; value: t.Function }
42 + | { kind: "ast"; name: string; value: t.FunctionDeclaration }
43 | { kind: "hir"; name: string; value: HIRFunction }
44 | { kind: "reactive"; name: string; value: ReactiveFunction };
45
compiler/forget/src/ReactiveScopes/CodegenReactiveFunction.ts
+3 -1
@@ -28,7 +28,9 @@ import {
28 import { todoInvariant } from "../Utils/todo";
29 import { assertExhaustive } from "../Utils/utils";
30
31 -export function codegenReactiveFunction(fn: ReactiveFunction): t.Function {
31 +export function codegenReactiveFunction(
32 + fn: ReactiveFunction
33 +): t.FunctionDeclaration {
34 const cx = new Context();
35 const params = fn.params.map((param) => convertIdentifier(param.identifier));
36 const body = codegenBlock(cx, fn.body);
compiler/forget/src/index.ts
+6 -2
@@ -5,8 +5,12 @@
5 * LICENSE file in the root directory of this source tree.
6 */
7 export { default as BabelPlugin } from "./Babel/BabelPlugin";
8 -export { ErrorSeverity } from "./CompilerError";
9 -export { compile, run } from "./CompilerPipeline";
8 +export {
9 + CompilerError,
10 + CompilerErrorDetail,
11 + ErrorSeverity,
12 +} from "./CompilerError";
13 +export { compile, CompilerPipelineValue, run } from "./CompilerPipeline";
14 export { printHIR } from "./HIR";
15 export { printReactiveFunction } from "./ReactiveScopes";
16