display multiple issues in overlay (vercel/turbo#2803)

This commit is contained in:
Leah 2022-11-29 18:05:52 +01:00 committed by GitHub
parent b680327280
commit 65016c65e3
9 changed files with 210 additions and 140 deletions

View file

@ -17,7 +17,7 @@ import {
onBeforeRefresh,
onBuildOk,
onRefresh,
onTurbopackError,
onTurbopackIssues,
} from "../overlay/client";
import { addEventListener, sendMessage } from "./websocket";
import { ModuleId } from "@vercel/turbopack-runtime/types";
@ -95,18 +95,18 @@ type AggregatedUpdates = {
};
// we aggregate all updates until the issues are resolved
const chunksWithErrors: Map<ChunkPath, AggregatedUpdates> = new Map();
const chunksWithUpdates: Map<ChunkPath, AggregatedUpdates> = new Map();
function aggregateUpdates(
msg: ServerMessage,
hasErrors: boolean
hasIssues: boolean
): ServerMessage {
const key = resourceKey(msg.resource);
const aggregated = chunksWithErrors.get(key);
const aggregated = chunksWithUpdates.get(key);
if (msg.type === "issues" && aggregated != null) {
if (!hasErrors) {
chunksWithErrors.delete(key);
if (!hasIssues) {
chunksWithUpdates.delete(key);
}
return {
@ -124,8 +124,8 @@ function aggregateUpdates(
if (msg.type !== "partial") return msg;
if (aggregated == null) {
if (hasErrors) {
chunksWithErrors.set(key, {
if (hasIssues) {
chunksWithUpdates.set(key, {
added: msg.instruction.added,
modified: msg.instruction.modified,
deleted: new Set(msg.instruction.deleted),
@ -172,10 +172,10 @@ function aggregateUpdates(
aggregated.deleted.add(moduleId);
}
if (!hasErrors) {
chunksWithErrors.delete(key);
if (!hasIssues) {
chunksWithUpdates.delete(key);
} else {
chunksWithErrors.set(key, aggregated);
chunksWithUpdates.set(key, aggregated);
}
return {
@ -198,21 +198,20 @@ function compareByList(list: any[], a: any, b: any) {
}
function handleIssues(msg: ServerMessage): boolean {
let issueToReport = null;
let hasCriticalIssues = false;
for (const issue of msg.issues) {
if (CRITICAL.includes(issue.severity)) {
issueToReport = issue;
break;
console.error(stripAnsi(issue.formatted));
hasCriticalIssues = true;
}
}
if (issueToReport) {
console.error(stripAnsi(issueToReport.formatted));
onTurbopackError(issueToReport);
if (msg.issues.length > 0) {
onTurbopackIssues(msg.issues);
}
return issueToReport != null;
return hasCriticalIssues;
}
const SEVERITY_ORDER = ["bug", "fatal", "error", "warning", "info", "log"];
@ -232,20 +231,20 @@ function handleSocketMessage(msg: ServerMessage) {
return compareByList(CATEGORY_ORDER, a.category, b.category);
});
const hasErrors = handleIssues(msg);
const aggregatedMsg = aggregateUpdates(msg, hasErrors);
const hasIssues = handleIssues(msg);
const aggregatedMsg = aggregateUpdates(msg, hasIssues);
if (hasErrors) return;
if (hasIssues) return;
if (aggregatedMsg.type !== "issues") {
onBeforeRefresh();
triggerUpdate(aggregatedMsg);
if (chunksWithErrors.size === 0) {
if (chunksWithUpdates.size === 0) {
onRefresh();
}
}
if (chunksWithErrors.size === 0) {
if (chunksWithUpdates.size === 0) {
onBuildOk();
}
}

View file

@ -85,8 +85,8 @@ function onBuildOk() {
Bus.emit({ type: Bus.TYPE_BUILD_OK });
}
function onTurbopackError(issue: Issue) {
Bus.emit({ type: Bus.TYPE_TURBOPACK_ERROR, issue });
function onTurbopackIssues(issues: Issue[]) {
Bus.emit({ type: Bus.TYPE_TURBOPACK_ISSUES, issues });
}
function onBeforeRefresh() {
@ -102,7 +102,7 @@ export { getServerError } from "./internal/helpers/nodeStackFrames";
export { default as ReactDevOverlay } from "./internal/ReactDevOverlay";
export {
onBuildOk,
onTurbopackError,
onTurbopackIssues,
register,
unregister,
onBeforeRefresh,

View file

@ -4,7 +4,6 @@ import type { Issue } from "@vercel/turbopack-runtime/types/protocol";
import * as Bus from "./bus";
import { ShadowPortal } from "./components/ShadowPortal";
import { BuildError } from "./container/BuildError";
import { Errors, SupportedErrorEvent } from "./container/Errors";
import { ErrorBoundary } from "./ErrorBoundary";
import { Base } from "./styles/Base";
@ -25,7 +24,11 @@ type RefreshState =
type OverlayState = {
nextId: number;
issue: Issue | null;
// issues are from turbopack
issues: Issue[];
// errors are client side
errors: SupportedErrorEvent[];
refreshState: RefreshState;
@ -47,10 +50,10 @@ function pushErrorFilterDuplicates(
function reducer(state: OverlayState, ev: Bus.BusEvent): OverlayState {
switch (ev.type) {
case Bus.TYPE_BUILD_OK: {
return { ...state, issue: null };
return { ...state, issues: [] };
}
case Bus.TYPE_TURBOPACK_ERROR: {
return { ...state, issue: ev.issue };
case Bus.TYPE_TURBOPACK_ISSUES: {
return { ...state, issues: ev.issues };
}
case Bus.TYPE_BEFORE_REFRESH: {
return { ...state, refreshState: { type: "pending", errors: [] } };
@ -58,7 +61,7 @@ function reducer(state: OverlayState, ev: Bus.BusEvent): OverlayState {
case Bus.TYPE_REFRESH: {
return {
...state,
issue: null,
issues: [],
errors:
// Errors can come in during updates. In this case, UNHANDLED_ERROR
// and UNHANDLED_REJECTION events might be dispatched between the
@ -135,7 +138,7 @@ export default function ReactDevOverlay({
React.Reducer<OverlayState, Bus.BusEvent>
>(reducer, {
nextId: 1,
issue: null,
issues: [],
errors: [],
refreshState: {
type: "idle",
@ -156,8 +159,8 @@ export default function ReactDevOverlay({
[]
);
const hasBuildError = state.issue != null;
const hasRuntimeErrors = Boolean(state.errors.length);
const hasBuildError = state.issues.length > 0;
const hasRuntimeErrors = state.errors.length > 0;
const errorType = hasBuildError
? "build"
@ -182,14 +185,9 @@ export default function ReactDevOverlay({
<Base />
<ComponentStyles />
{shouldPreventDisplay(
errorType,
preventDisplay
) ? null : hasBuildError ? (
<BuildError issue={state.issue!} />
) : hasRuntimeErrors ? (
<Errors errors={state.errors} />
) : null}
{shouldPreventDisplay(errorType, preventDisplay) ? null : (
<Errors issues={state.issues} errors={state.errors} />
)}
</ShadowPortal>
) : null}
</React.Fragment>

View file

@ -3,16 +3,16 @@ import { StackFrame } from "stacktrace-parser";
import type { Issue } from "@vercel/turbopack-runtime/types/protocol";
export const TYPE_BUILD_OK = "build-ok";
export const TYPE_TURBOPACK_ERROR = "turbopack-error";
export const TYPE_TURBOPACK_ISSUES = "turbopack-error";
export const TYPE_BEFORE_REFRESH = "before-fast-refresh";
export const TYPE_REFRESH = "fast-refresh";
export const TYPE_UNHANDLED_ERROR = "unhandled-error";
export const TYPE_UNHANDLED_REJECTION = "unhandled-rejection";
export type BuildOk = { type: typeof TYPE_BUILD_OK };
export type TurbopackError = {
type: typeof TYPE_TURBOPACK_ERROR;
issue: Issue;
export type TurbopackIssues = {
type: typeof TYPE_TURBOPACK_ISSUES;
issues: Issue[];
};
export type BeforeFastRefresh = { type: typeof TYPE_BEFORE_REFRESH };
export type FastRefresh = { type: typeof TYPE_REFRESH };
@ -28,7 +28,7 @@ export type UnhandledRejection = {
};
export type BusEvent =
| BuildOk
| TurbopackError
| TurbopackIssues
| BeforeFastRefresh
| FastRefresh
| UnhandledError

View file

@ -19,7 +19,6 @@ type TabRefs = Record<string, HTMLElement | undefined>;
type TabsContextType = {
selectedId: string;
tabRefs: Readonly<TabRefs>;
registerTabRef: (id: string, el: HTMLElement | null) => void;
onSelectTab: (id: string) => void;
};
@ -69,6 +68,8 @@ export function Tabs({
setTimeout(() => {
tab.focus();
}, 0);
onSelectTabUnchecked(id);
},
[selectedId, onSelectTabUnchecked]
);
@ -108,7 +109,6 @@ export function Tabs({
return (
<TabsProvider
selectedId={selectedId}
tabRefs={tabRefs.current}
registerTabRef={registerTabRef}
onSelectTab={onSelectTab}
>

View file

@ -1,65 +0,0 @@
import * as React from "react";
import type { Issue } from "@vercel/turbopack-runtime/types/protocol";
import {
Dialog,
DialogBody,
DialogContent,
DialogHeader,
} from "../components/Dialog";
import { Overlay } from "../components/Overlay";
import { Terminal } from "../components/Terminal";
import { noop as css } from "../helpers/noop-template";
export type BuildErrorProps = { issue: Issue };
export function BuildError({ issue }: BuildErrorProps) {
const noop = React.useCallback(() => {}, []);
return (
<Overlay fixed>
<Dialog
aria-labelledby="nextjs__container_build_error_label"
aria-describedby="nextjs__container_build_error_desc"
onClose={noop}
>
<DialogContent>
<DialogHeader className="nextjs-container-build-error-header">
<h4 id="nextjs__container_build_error_label">
Turbopack failed to compile
</h4>
</DialogHeader>
<DialogBody className="nextjs-container-build-error-body">
<Terminal content={issue.formatted} />
<footer>
<p id="nextjs__container_build_error_desc">
<small>
This error occurred during the build process and can only be
dismissed by fixing the error.
</small>
</p>
</footer>
</DialogBody>
</DialogContent>
</Dialog>
</Overlay>
);
}
export const styles = css`
.nextjs-container-build-error-header > h4 {
line-height: 1.5;
margin: 0;
padding: 0;
}
.nextjs-container-build-error-body footer {
margin-top: var(--size-gap);
}
.nextjs-container-build-error-body footer p {
margin: 0;
}
.nextjs-container-build-error-body small {
color: #757575;
}
`;

View file

@ -1,5 +1,7 @@
import * as React from "react";
import { Issue } from "@vercel/turbopack-runtime/types/protocol";
import {
TYPE_UNHANDLED_ERROR,
TYPE_UNHANDLED_REJECTION,
@ -18,8 +20,9 @@ import { Tab, TabPanel, Tabs } from "../components/Tabs";
import { getErrorByType, ReadyRuntimeError } from "../helpers/getErrorByType";
import { getErrorSource } from "../helpers/nodeStackFrames";
import { noop as css } from "../helpers/noop-template";
import { AlertOctagon } from "../icons";
import { AlertOctagon, PackageX } from "../icons";
import { RuntimeErrorsDialogBody } from "./RuntimeError";
import { TurbopackIssuesDialogBody } from "../container/TurbopackIssue";
import { ErrorsToast } from "../container/ErrorsToast";
export type SupportedErrorEvent = {
@ -27,6 +30,7 @@ export type SupportedErrorEvent = {
event: UnhandledError | UnhandledRejection;
};
export type ErrorsProps = {
issues: Issue[];
errors: SupportedErrorEvent[];
};
@ -82,9 +86,7 @@ function useResolvedErrors(
return [ready, next];
}, [errors, lookups]);
const isLoading = React.useMemo<boolean>(() => {
return readyErrors.length < 1 && errors.length > 1;
}, [errors.length, readyErrors.length]);
const isLoading = readyErrors.length === 0 && errors.length > 1;
React.useEffect(() => {
if (nextError == null) {
@ -114,7 +116,7 @@ function useResolvedErrors(
// Reset component state when there are no errors to be displayed.
// This should never happen, but let's handle it.
React.useEffect(() => {
if (errors.length < 1) {
if (errors.length === 0) {
setLookups({});
}
}, [errors.length]);
@ -123,24 +125,18 @@ function useResolvedErrors(
}
const enum TabId {
TurbopackIssues = "turbopack-issues",
RuntimeErrors = "runtime-errors",
}
export function Errors({ errors }: ErrorsProps) {
const [displayState, setDisplayState] = React.useState<
export function Errors({ issues, errors }: ErrorsProps) {
// eslint-disable-next-line prefer-const
let [displayState, setDisplayState] = React.useState<
"minimized" | "fullscreen" | "hidden"
>("fullscreen");
const [readyErrors, isLoading] = useResolvedErrors(errors);
// Reset component state when there are no errors to be displayed.
// This should never happen, but let's handle it.
React.useEffect(() => {
if (errors.length < 1) {
setDisplayState("hidden");
}
}, [errors.length]);
const minimize = React.useCallback((e?: MouseEvent | TouchEvent) => {
e?.preventDefault();
setDisplayState("minimized");
@ -157,18 +153,37 @@ export function Errors({ errors }: ErrorsProps) {
[]
);
const hasErrors = errors.length > 0;
const hasIssues = issues.length !== 0;
const hasIssueWithError = issues.some((issue) =>
["bug", "fatal", "error"].includes(issue.severity)
);
const hasErrors = errors.length !== 0;
const hasServerError = readyErrors.some((err) =>
["server", "edge-server"].includes(getErrorSource(err.error) || "")
);
const isClosable = !isLoading && !hasServerError;
const isClosable = !isLoading && !hasIssueWithError && !hasServerError;
const defaultTab =
hasIssueWithError || !hasErrors
? TabId.TurbopackIssues
: TabId.RuntimeErrors;
const defaultTab = TabId.RuntimeErrors;
const [selectedTab, setSelectedTab] = React.useState<string>(defaultTab);
React.useEffect(() => {
if (defaultTab === TabId.TurbopackIssues) {
setSelectedTab(TabId.TurbopackIssues);
}
}, [defaultTab]);
if (!isClosable) {
displayState = "fullscreen";
}
// This component shouldn't be rendered with no errors, but if it is, let's
// handle it gracefully by rendering nothing.
if (errors.length < 1) {
if (!hasErrors && !hasIssues) {
return null;
}
@ -179,7 +194,7 @@ export function Errors({ errors }: ErrorsProps) {
if (displayState === "minimized") {
return (
<ErrorsToast
errorCount={readyErrors.length}
errorCount={readyErrors.length + issues.length}
onClick={fullscreen}
onClose={hide}
/>
@ -202,15 +217,40 @@ export function Errors({ errors }: ErrorsProps) {
close={isClosable ? minimize : undefined}
>
<DialogHeaderTabList>
{hasIssues && (
<Tab
id={TabId.TurbopackIssues}
next={hasErrors ? TabId.RuntimeErrors : undefined}
data-severity={hasIssueWithError ? "error" : "warning"}
>
<PackageX />
{issues.length} Turbopack Issue{issues.length > 1 ? "s" : ""}
</Tab>
)}
{hasErrors && (
<Tab id={TabId.RuntimeErrors} data-severity="error">
<Tab
id={TabId.RuntimeErrors}
prev={hasIssues ? TabId.TurbopackIssues : undefined}
data-severity="error"
>
<AlertOctagon />
{isLoading ? "Loading" : readyErrors.length} Runtime Errors
{isLoading ? "..." : null}
{isLoading
? "Loading Runtime Errors ..."
: `${readyErrors.length} Runtime Error${
readyErrors.length > 1 ? "s" : ""
}`}
</Tab>
)}
</DialogHeaderTabList>
</DialogHeader>
{hasIssues && (
<TabPanel
as={TurbopackIssuesDialogBody}
id={TabId.TurbopackIssues}
issues={issues}
className="errors-body"
/>
)}
{hasErrors && (
<TabPanel
as={RuntimeErrorsDialogBody}

View file

@ -0,0 +1,98 @@
import { Issue } from "@vercel/turbopack-runtime/types/protocol";
import { LeftRightDialogHeader } from "../components/LeftRightDialogHeader";
import { DialogBody, DialogBodyProps } from "../components/Dialog";
import { Terminal } from "../components/Terminal";
import { noop as css } from "../helpers/noop-template";
import { clsx } from "../helpers/clsx";
import { usePagination } from "../hooks/usePagination";
type TurbopackIssuesDialogBodyProps = {
issues: Issue[];
"data-hidden"?: boolean;
};
export function TurbopackIssuesDialogBody({
issues,
"data-hidden": hidden = false,
className,
...rest
}: TurbopackIssuesDialogBodyProps & Omit<DialogBodyProps, "children">) {
const [activeIssue, { previous, next }, activeIdx] = usePagination(issues);
const hasIssues = issues.length > 0;
const hasIssueWithError = issues.some((issue) =>
["bug", "fatal", "error"].includes(issue.severity)
);
if (!hasIssues || !activeIssue) {
return null;
}
const activeIssueIsError = ["bug", "fatal", "error"].includes(
activeIssue.severity
);
return (
<DialogBody
{...rest}
data-hidden={hidden}
className={clsx("issues-body", className)}
>
<div className="title-pagination">
<h1 id="errors_label">
{hasIssueWithError
? "Turbopack failed to compile"
: "Turbopack compiled with warnings"}
</h1>
<LeftRightDialogHeader
hidden={hidden}
previous={activeIdx > 0 ? previous : null}
next={activeIdx < issues.length - 1 ? next : null}
severity={activeIssueIsError ? "error" : "warning"}
>
<small>
<span>{activeIdx + 1}</span> of <span>{issues.length}</span>
</small>
</LeftRightDialogHeader>
</div>
<h2
id="errors_desc"
data-severity={activeIssueIsError ? "error" : "warning"}
>
{activeIssue.title}
</h2>
<Terminal content={activeIssue.formatted} />
{activeIssueIsError && (
<footer>
<p>
<small>
This error occurred during the build process and can only be
dismissed by fixing the error.
</small>
</p>
</footer>
)}
</DialogBody>
);
}
export const styles = css`
.issues-body > .terminal {
margin-top: var(--size-gap-double);
}
.issues-body > footer {
margin-top: var(--size-gap);
}
.issues-body > footer > p {
margin: 0;
}
.issues-body > footer > small {
color: #757575;
}
`;

View file

@ -7,10 +7,10 @@ import { styles as overlay } from "../components/Overlay";
import { styles as tabs } from "../components/Tabs";
import { styles as terminal } from "../components/Terminal";
import { styles as toast } from "../components/Toast";
import { styles as buildErrorStyles } from "../container/BuildError";
import { styles as containerErrorStyles } from "../container/Errors";
import { styles as containerErrorToastStyles } from "../container/ErrorsToast";
import { styles as containerRuntimeErrorStyles } from "../container/RuntimeError";
import { styles as containerTurbopackIssueStyles } from "../container/TurbopackIssue";
import { noop as css } from "../helpers/noop-template";
export function ComponentStyles() {
@ -25,10 +25,10 @@ export function ComponentStyles() {
${terminal}
${tabs}
${buildErrorStyles}
${containerErrorStyles}
${containerErrorToastStyles}
${containerRuntimeErrorStyles}
${containerTurbopackIssueStyles}
`}
</style>
);