Render error markers in playground
Additionally with this commit we now support multiple top level functions  
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