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)
This commit is contained in:
Lauren Tan
2023-01-24 12:44:51 -05:00
parent 109d7ba980
commit 9d935db383
7 changed files with 252 additions and 114 deletions
@@ -3,16 +3,22 @@
*/
import MonacoEditor, { type Monaco } from "@monaco-editor/react";
import { CompilerErrorDetail } from "babel-plugin-react-forget";
import invariant from "invariant";
import type { editor } from "monaco-editor";
import { useEffect, useState } from "react";
import { renderForgetMarkers } from "../../lib/forgetMonacoDiagnostics";
import { useStore, useStoreDispatch } from "../StoreContext";
import { monacoOptions } from "./monacoOptions";
// TODO: Make TS recognize .d.ts files, in addition to loading them with webpack.
// @ts-ignore
import React$Types from "../../node_modules/@types/react/index.d.ts";
export default function Input({}: {}) {
type Props = {
errors: CompilerErrorDetail[];
};
export default function Input({ errors }: Props) {
const [monaco, setMonaco] = useState<Monaco | null>(null);
const store = useStore();
const dispatchStore = useStoreDispatch();
@@ -23,10 +29,11 @@ export default function Input({}: {}) {
const uri = monaco.Uri.parse(`file:///index.js`);
const model = monaco.editor.getModel(uri);
invariant(model, "Model must exist for the selected input file.");
renderForgetMarkers({ monaco, model, details: errors });
// N.B. that `tabSize` is a model property, not an editor property.
// So, the tab size has to be set per model.
model.updateOptions({ tabSize: 2 });
}, [monaco]);
}, [monaco, errors]);
const handleChange = (value: string | undefined) => {
if (!value) return;
@@ -6,121 +6,104 @@
*/
import generate from "@babel/generator";
import { parse } from "@babel/parser";
import traverse, { NodePath } from "@babel/traverse";
import * as t from "@babel/types";
import MonacoEditor from "@monaco-editor/react";
import {
printHIR,
printReactiveFunction,
run,
type CompilerError,
type CompilerPipelineValue,
} from "babel-plugin-react-forget";
import prettier from "prettier";
import prettierParserBabel from "prettier/parser-babel";
import { memo, useMemo, useState } from "react";
import type { Store } from "../../lib/stores";
import { type Store } from "../../lib/stores";
import TabbedWindow from "../TabbedWindow";
import { monacoOptions } from "./monacoOptions";
const MemoizedOutput = memo(Output);
export default MemoizedOutput;
export type CompilerOutput =
| { kind: "ok"; results: Map<string, CompilerPipelineValue[]> }
| { kind: "err"; error: CompilerError };
type Props = {
store: Store;
compilerOutput: CompilerOutput;
};
type CompilerError = string;
function parseFunctions(
source: string
): Array<NodePath<t.FunctionDeclaration>> {
try {
const ast = parse(source, {
plugins: ["typescript", "jsx"],
sourceType: "module",
});
const items: Array<NodePath<t.FunctionDeclaration>> = [];
traverse(ast, {
FunctionDeclaration: {
enter(nodePath) {
items.push(nodePath);
},
},
});
return items;
} catch (e) {
return [];
}
}
function compile(source: string): Map<string, React.ReactNode> | CompilerError {
try {
const astFunctions = parseFunctions(source);
if (astFunctions.length === 0) {
return "";
}
// TODO: Handle multiple functions
const func = astFunctions[0];
const tabs = new Map<string, React.ReactNode>();
let ast: t.Function | null = null;
for (const result of run(func)) {
function tabify(source: string, compilerOutput: CompilerOutput) {
const tabs = new Map<string, React.ReactNode>();
const reorderedTabs = new Map<string, React.ReactNode>();
const concattedResults = new Map<string, string>();
let topLevelFnDecls: Array<t.FunctionDeclaration> = [];
if (compilerOutput.kind === "err") return reorderedTabs;
// Concat all top level function declaration results into a single tab for each pass
for (const [passName, results] of compilerOutput.results) {
for (const result of results) {
switch (result.kind) {
case "ast": {
ast = result.value;
break;
}
case "hir": {
const text = printHIR(result.value.body);
tabs.set(
result.name,
<TextTabContent output={text}></TextTabContent>
);
const prev = concattedResults.get(result.name);
const next = printHIR(result.value.body);
const identName = `function ${result.value.id?.name}`;
if (prev != null) {
concattedResults.set(passName, `${prev}\n\n${identName}\n${next}`);
} else {
concattedResults.set(passName, `${identName}\n${next}`);
}
break;
}
case "reactive": {
const text = printReactiveFunction(result.value);
tabs.set(
result.name,
<TextTabContent output={text}></TextTabContent>
);
const prev = concattedResults.get(passName);
const next = printReactiveFunction(result.value);
if (prev != null) {
concattedResults.set(passName, `${prev}\n\n${next}`);
} else {
concattedResults.set(passName, next);
}
break;
}
case "ast":
topLevelFnDecls.push(result.value);
break;
default: {
throw new Error("Unexpected result kind");
}
}
}
// Ensure that JS and the JS source map come first
const reorderedTabs = new Map();
if (ast !== null) {
const { code, sourceMapUrl } = codegen(ast, source);
reorderedTabs.set("JS", <TextTabContent output={code}></TextTabContent>);
if (sourceMapUrl) {
reorderedTabs.set(
"SourceMap",
<>
<iframe
src={sourceMapUrl}
className="w-full h-96"
title="Generated Code"
/>
</>
);
}
}
tabs.forEach((tab, name) => {
reorderedTabs.set(name, tab);
});
return reorderedTabs;
} catch (e: any) {
console.error(e);
return e.toString();
}
for (const [passName, text] of concattedResults) {
tabs.set(passName, <TextTabContent output={text}></TextTabContent>);
}
// Ensure that JS and the JS source map come first
if (topLevelFnDecls.length > 0) {
// Make a synthetic Program so we can have a single AST with all the top level
// FunctionDeclarations
const ast = t.program(topLevelFnDecls);
const { code, sourceMapUrl } = codegen(ast, source);
reorderedTabs.set("JS", <TextTabContent output={code}></TextTabContent>);
if (sourceMapUrl) {
reorderedTabs.set(
"SourceMap",
<>
<iframe
src={sourceMapUrl}
className="w-full h-96"
title="Generated Code"
/>
</>
);
}
}
tabs.forEach((tab, name) => {
reorderedTabs.set(name, tab);
});
return reorderedTabs;
}
function codegen(
ast: any,
ast: t.Program,
source: string
): { code: any; sourceMapUrl: string | null } {
const generated = generate(
@@ -140,30 +123,6 @@ function codegen(
return { code: codegenOutput, sourceMapUrl };
}
// TODO(gsn: Update diagnostics ƒrom HIR output
function Output({ store }: Props) {
const [tabsOpen, setTabsOpen] = useState<Set<string>>(() => new Set());
const compilerOutput = useMemo(() => compile(store.source), [store.source]);
if (typeof compilerOutput === "string") {
if (compilerOutput === "") return <></>;
return (
<pre>
<code>${compilerOutput}</code>
</pre>
);
}
return (
<TabbedWindow
defaultTab="HIR"
setTabsOpen={setTabsOpen}
tabsOpen={tabsOpen}
tabs={compilerOutput}
/>
);
}
function utf16ToUTF8(s: string): string {
return unescape(encodeURIComponent(s));
}
@@ -176,6 +135,31 @@ function getSourceMapUrl(code: string, map: string): string | null {
)}`;
}
function Output({ store, compilerOutput }: Props) {
const [tabsOpen, setTabsOpen] = useState<Set<string>>(() => new Set());
const tabs = useMemo(
() => tabify(store.source, compilerOutput),
[store.source, compilerOutput]
);
if (compilerOutput.kind === "err") {
return (
<pre>
<code>{compilerOutput.error.toString()}</code>
</pre>
);
}
return (
<TabbedWindow
defaultTab="HIR"
setTabsOpen={setTabsOpen}
tabsOpen={tabsOpen}
tabs={tabs}
/>
);
}
function TextTabContent({ output }: { output: string }) {
return (
// Restrict MonacoEditor's height, since the config autoLayout:true
@@ -5,10 +5,14 @@
* LICENSE file in the root directory of this source tree.
*/
import { parse } from "@babel/parser";
import traverse, { NodePath } from "@babel/traverse";
import * as t from "@babel/types";
import { CompilerPipelineValue, run } from "babel-plugin-react-forget";
import clsx from "clsx";
import invariant from "invariant";
import { useSnackbar } from "notistack";
import { useDeferredValue } from "react";
import { useDeferredValue, useMemo } from "react";
import { useMountEffect } from "../../hooks";
import { defaultStore } from "../../lib/defaultStore";
import {
@@ -20,13 +24,62 @@ import {
} from "../../lib/stores";
import { useStore, useStoreDispatch } from "../StoreContext";
import Input from "./Input";
import Output from "./Output";
import { CompilerOutput, default as Output } from "./Output";
function parseFunctions(
source: string
): Array<NodePath<t.FunctionDeclaration>> {
const items: Array<NodePath<t.FunctionDeclaration>> = [];
try {
const ast = parse(source, {
plugins: ["typescript", "jsx"],
sourceType: "module",
});
traverse(ast, {
FunctionDeclaration: {
enter(nodePath) {
items.push(nodePath);
},
},
});
} catch (e) {
console.error(e);
}
return items;
}
function compile(source: string): CompilerOutput {
try {
const results = new Map<string, CompilerPipelineValue[]>();
const upsert = (result: CompilerPipelineValue) => {
const entry = results.get(result.name);
if (Array.isArray(entry)) {
entry.push(result);
} else {
results.set(result.name, [result]);
}
};
for (const fn of parseFunctions(source)) {
for (const result of run(fn)) {
upsert(result);
}
}
return { kind: "ok", results };
} catch (error: any) {
console.error(error);
return { kind: "err", error };
}
}
export default function Editor() {
const store = useStore();
const deferredStore = useDeferredValue(store);
const dispatchStore = useStoreDispatch();
const { enqueueSnackbar } = useSnackbar();
const compilerOutput = useMemo(
() => compile(deferredStore.source),
[deferredStore.source]
);
useMountEffect(() => {
let mountStore: Store;
@@ -57,10 +110,14 @@ export default function Editor() {
style={{ minWidth: 650 }}
className={clsx("relative sm:basis-1/4")}
>
<Input />
<Input
errors={
compilerOutput.kind === "err" ? compilerOutput.error.details : []
}
/>
</div>
<div className={clsx("flex sm:flex")}>
<Output store={deferredStore} />
<Output store={deferredStore} compilerOutput={compilerOutput} />
</div>
</div>
</>
@@ -0,0 +1,84 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*/
import { Monaco } from "@monaco-editor/react";
import { CompilerErrorDetail, ErrorSeverity } from "babel-plugin-react-forget";
import { MarkerSeverity, type editor } from "monaco-editor";
function mapForgetSeverityToMonaco(
level: ErrorSeverity,
monaco: Monaco
): MarkerSeverity {
switch (level) {
case ErrorSeverity.Todo:
case ErrorSeverity.InvalidInput:
return monaco.MarkerSeverity.Error;
default:
return monaco.MarkerSeverity.Warning;
}
}
function mapForgetDiagnosticToMonacoMarker(
detail: CompilerErrorDetail,
monaco: Monaco
): editor.IMarkerData | null {
if (detail.nodePath == null || detail.nodePath.node?.loc == null) {
return null;
}
const severity = mapForgetSeverityToMonaco(detail.severity, monaco);
let message = detail.errorMessage;
return {
severity,
message,
startLineNumber: detail.nodePath.node.loc.start.line,
startColumn: detail.nodePath.node.loc.start.column + 1,
endLineNumber: detail.nodePath.node.loc.end.line,
endColumn: detail.nodePath.node.loc.end.column + 1,
};
}
type ForgetMarkerConfig = {
monaco: Monaco;
model: editor.ITextModel;
details: CompilerErrorDetail[];
};
let decorations: string[] = [];
export function renderForgetMarkers({
monaco,
model,
details,
}: ForgetMarkerConfig): void {
let markers = [];
for (const detail of details) {
const marker = mapForgetDiagnosticToMonacoMarker(detail, monaco);
if (marker == null) {
continue;
}
markers.push(marker);
}
if (markers.length > 0) {
monaco.editor.setModelMarkers(model, "owner", markers);
const newDecorations = markers.map((marker) => {
return {
range: new monaco.Range(
marker.startLineNumber,
marker.startColumn,
marker.endLineNumber,
marker.endColumn
),
options: {
isWholeLine: true,
glyphMarginClassName: "bg-red-300",
},
};
});
decorations = model.deltaDecorations(decorations, newDecorations);
} else {
monaco.editor.setModelMarkers(model, "owner", []);
decorations = model.deltaDecorations(
model.getAllDecorations().map((d) => d.id),
[]
);
}
}
+1 -1
View File
@@ -39,7 +39,7 @@ import { logHIRFunction, logReactiveFunction } from "./Utils/logger";
import { assertExhaustive } from "./Utils/utils";
export type CompilerPipelineValue =
| { kind: "ast"; name: string; value: t.Function }
| { kind: "ast"; name: string; value: t.FunctionDeclaration }
| { kind: "hir"; name: string; value: HIRFunction }
| { kind: "reactive"; name: string; value: ReactiveFunction };
@@ -28,7 +28,9 @@ import {
import { todoInvariant } from "../Utils/todo";
import { assertExhaustive } from "../Utils/utils";
export function codegenReactiveFunction(fn: ReactiveFunction): t.Function {
export function codegenReactiveFunction(
fn: ReactiveFunction
): t.FunctionDeclaration {
const cx = new Context();
const params = fn.params.map((param) => convertIdentifier(param.identifier));
const body = codegenBlock(cx, fn.body);
+6 -2
View File
@@ -5,8 +5,12 @@
* LICENSE file in the root directory of this source tree.
*/
export { default as BabelPlugin } from "./Babel/BabelPlugin";
export { ErrorSeverity } from "./CompilerError";
export { compile, run } from "./CompilerPipeline";
export {
CompilerError,
CompilerErrorDetail,
ErrorSeverity,
} from "./CompilerError";
export { compile, CompilerPipelineValue, run } from "./CompilerPipeline";
export { printHIR } from "./HIR";
export { printReactiveFunction } from "./ReactiveScopes";