mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Render error markers in playground
Additionally with this commit we now support multiple top level functions  
This commit is contained in:
@@ -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),
|
||||
[]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user