Skip to content

Commit

Permalink
show turbopack warnings in error overlay (vercel#3465)
Browse files Browse the repository at this point in the history
  • Loading branch information
ForsakenHarmony authored Feb 1, 2023
1 parent ee35db4 commit 89d0100
Show file tree
Hide file tree
Showing 10 changed files with 161 additions and 73 deletions.
57 changes: 43 additions & 14 deletions crates/next-core/js/src/dev/hmr-client.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type {
ClientMessage,
EcmascriptChunkUpdate,
HmrUpdateEntry,
Issue,
ResourceIdentifier,
ServerMessage,
} from "@vercel/turbopack-runtime/types/protocol";
import type {
ChunkPath,
ModuleId,
UpdateCallback,
TurbopackGlobals,
} from "@vercel/turbopack-runtime/types";
Expand All @@ -19,8 +21,6 @@ import {
onTurbopackIssues,
} from "../overlay/client";
import { addEventListener, sendMessage } from "./websocket";
import { ModuleId } from "@vercel/turbopack-runtime/types";
import { HmrUpdateEntry } from "@vercel/turbopack-runtime/types/protocol";

declare var globalThis: TurbopackGlobals;

Expand Down Expand Up @@ -111,12 +111,12 @@ const chunksWithUpdates: Map<ResourceKey, AggregatedUpdates> = new Map();

function aggregateUpdates(
msg: ServerMessage,
hasIssues: boolean
hasCriticalIssues: boolean
): ServerMessage {
const key = resourceKey(msg.resource);
const aggregated = chunksWithUpdates.get(key);

if (msg.type === "issues" && aggregated == null && hasIssues) {
if (msg.type === "issues" && aggregated == null && hasCriticalIssues) {
// add an empty record to make sure we don't call `onBuildOk`
chunksWithUpdates.set(key, {
added: {},
Expand All @@ -126,7 +126,7 @@ function aggregateUpdates(
}

if (msg.type === "issues" && aggregated != null) {
if (!hasIssues) {
if (!hasCriticalIssues) {
chunksWithUpdates.delete(key);
}

Expand All @@ -145,7 +145,7 @@ function aggregateUpdates(
if (msg.type !== "partial") return msg;

if (aggregated == null) {
if (hasIssues) {
if (hasCriticalIssues) {
chunksWithUpdates.set(key, {
added: msg.instruction.added,
modified: msg.instruction.modified,
Expand Down Expand Up @@ -193,7 +193,7 @@ function aggregateUpdates(
aggregated.deleted.add(moduleId);
}

if (!hasIssues) {
if (!hasCriticalIssues) {
chunksWithUpdates.delete(key);
} else {
chunksWithUpdates.set(key, aggregated);
Expand All @@ -218,7 +218,28 @@ function compareByList(list: any[], a: any, b: any) {
return aI - bI;
}

const chunksWithIssues: Map<ResourceKey, Issue[]> = new Map();

function emitIssues() {
const issues = [];
const deduplicationSet = new Set();

for (const [_, chunkIssues] of chunksWithIssues) {
for (const chunkIssue of chunkIssues) {
if (deduplicationSet.has(chunkIssue.formatted)) continue;

issues.push(chunkIssue);
deduplicationSet.add(chunkIssue.formatted);
}
}

sortIssues(issues);

onTurbopackIssues(issues);
}

function handleIssues(msg: ServerMessage): boolean {
const key = resourceKey(msg.resource);
let hasCriticalIssues = false;

for (const issue of msg.issues) {
Expand All @@ -229,9 +250,13 @@ function handleIssues(msg: ServerMessage): boolean {
}

if (msg.issues.length > 0) {
onTurbopackIssues(msg.issues);
chunksWithIssues.set(key, msg.issues);
} else if (chunksWithIssues.has(key)) {
chunksWithIssues.delete(key);
}

emitIssues();

return hasCriticalIssues;
}

Expand All @@ -245,17 +270,21 @@ const CATEGORY_ORDER = [
"other",
];

function handleSocketMessage(msg: ServerMessage) {
msg.issues.sort((a, b) => {
function sortIssues(issues: Issue[]) {
issues.sort((a, b) => {
const first = compareByList(SEVERITY_ORDER, a.severity, b.severity);
if (first !== 0) return first;
return compareByList(CATEGORY_ORDER, a.category, b.category);
});
}

function handleSocketMessage(msg: ServerMessage) {
sortIssues(msg.issues);

const hasIssues = handleIssues(msg);
const aggregatedMsg = aggregateUpdates(msg, hasIssues);
const hasCriticalIssues = handleIssues(msg);
const aggregatedMsg = aggregateUpdates(msg, hasCriticalIssues);

if (hasIssues) return;
if (hasCriticalIssues) return;

const runHooks = chunksWithUpdates.size === 0;

Expand Down
29 changes: 8 additions & 21 deletions crates/next-core/js/src/overlay/internal/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
/* eslint-disable @next/next/no-head-element */
import React from "react";

type ErrorBoundaryProps = {
onError: (error: Error, componentStack: string | null) => void;
globalOverlay?: boolean;
isMounted?: boolean;
fallback: React.ReactNode | null;
children?: React.ReactNode;
};

type ErrorBoundaryState = { error: Error | null };

class ErrorBoundary extends React.PureComponent<
Expand All @@ -26,28 +25,16 @@ class ErrorBoundary extends React.PureComponent<
errorInfo?: { componentStack?: string | null }
) {
this.props.onError(error, errorInfo?.componentStack ?? null);
if (!this.props.globalOverlay) {
this.setState({ error });
}
}

render() {
const { error } = this.state;

const { fallback } = this.props;

// The component has to be unmounted or else it would continue to error
if (
this.state.error ||
(this.props.globalOverlay && this.props.isMounted)
) {
// When the overlay is global for the application and it wraps a component rendering `<html>`
// we have to render the html shell otherwise the shadow root will not be able to attach
if (this.props.globalOverlay) {
return (
<html>
<body></body>
</html>
);
}

return null;
if (error != null) {
return fallback;
}

return this.props.children;
Expand Down
14 changes: 10 additions & 4 deletions crates/next-core/js/src/overlay/internal/ReactDevOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function pushErrorFilterDuplicates(
function reducer(state: OverlayState, ev: Bus.BusEvent): OverlayState {
switch (ev.type) {
case Bus.TYPE_BUILD_OK: {
return { ...state, issues: [] };
return { ...state };
}
case Bus.TYPE_TURBOPACK_ISSUES: {
return { ...state, issues: ev.issues };
Expand All @@ -61,7 +61,6 @@ function reducer(state: OverlayState, ev: Bus.BusEvent): OverlayState {
case Bus.TYPE_REFRESH: {
return {
...state,
issues: [],
errors:
// Errors can come in during updates. In this case, UNHANDLED_ERROR
// and UNHANDLED_REJECTION events might be dispatched between the
Expand Down Expand Up @@ -173,9 +172,16 @@ export default function ReactDevOverlay({
return (
<React.Fragment>
<ErrorBoundary
globalOverlay={globalOverlay}
isMounted={isMounted}
onError={onComponentError}
fallback={
// When the overlay is global for the application and it wraps a component rendering `<html>`
// we have to render the html shell otherwise the shadow root will not be able to attach
globalOverlay ? (
<html>
<body></body>
</html>
) : null
}
>
{children ?? null}
</ErrorBoundary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,15 @@ export type ToastProps = React.PropsWithChildren & {
className?: string;
};

export function Toast({ onClick, children, className }: ToastProps) {
export function Toast({
onClick,
children,
className,
...rest
}: ToastProps & React.HTMLProps<HTMLDivElement>) {
return (
<div
{...rest}
data-nextjs-toast
onClick={onClick}
className={clsx("toast", className)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,19 @@ const styles = css`
padding: 16px;
border-radius: var(--size-gap-half);
font-weight: 600;
color: var(--color-text-white);
background-color: var(--color-error);
box-shadow: 0px var(--size-gap-double) var(--size-gap-quad)
rgba(0, 0, 0, 0.25);
}
.toast[data-severity="error"] > .toast-wrapper {
color: var(--color-text-white);
background-color: var(--color-error);
}
.toast[data-severity="warning"] > .toast-wrapper {
color: var(--color-text-white);
background-color: var(--color-warning);
}
`;

export { styles };
76 changes: 52 additions & 24 deletions crates/next-core/js/src/overlay/internal/container/Errors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,35 +124,55 @@ function useResolvedErrors(
return [readyErrors, isLoading];
}

const enum DisplayState {
Fullscreen,
Minimized,
Hidden,
}

type DisplayStateAction = (e?: MouseEvent | TouchEvent) => void;

type DisplayStateActions = {
fullscreen: DisplayStateAction;
minimize: DisplayStateAction;
hide: DisplayStateAction;
};

function useDisplayState(
initialState: DisplayState
): [DisplayState, DisplayStateActions] {
const [displayState, setDisplayState] =
React.useState<DisplayState>(initialState);

const actions = React.useMemo<DisplayStateActions>(
() => ({
fullscreen: (e) => {
e?.preventDefault();
setDisplayState(DisplayState.Fullscreen);
},
minimize: (e) => {
e?.preventDefault();
setDisplayState(DisplayState.Minimized);
},
hide: (e) => {
e?.preventDefault();
setDisplayState(DisplayState.Hidden);
},
}),
[]
);

return [displayState, actions];
}

const enum TabId {
TurbopackIssues = "turbopack-issues",
RuntimeErrors = "runtime-errors",
}

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);

const minimize = React.useCallback((e?: MouseEvent | TouchEvent) => {
e?.preventDefault();
setDisplayState("minimized");
}, []);
const hide = React.useCallback((e?: MouseEvent | TouchEvent) => {
e?.preventDefault();
setDisplayState("hidden");
}, []);
const fullscreen = React.useCallback(
(e?: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
e?.preventDefault();
setDisplayState("fullscreen");
},
[]
);

const hasIssues = issues.length !== 0;
const hasIssueWithError = issues.some((issue) =>
["bug", "fatal", "error"].includes(issue.severity)
Expand All @@ -177,8 +197,15 @@ export function Errors({ issues, errors }: ErrorsProps) {
}
}, [defaultTab]);

const onlyHasWarnings = !hasErrors && !hasIssueWithError;

const [stateDisplayState, { fullscreen, minimize, hide }] = useDisplayState(
onlyHasWarnings ? DisplayState.Minimized : DisplayState.Fullscreen
);
let displayState = stateDisplayState;

if (!isClosable) {
displayState = "fullscreen";
displayState = DisplayState.Fullscreen;
}

// This component shouldn't be rendered with no errors, but if it is, let's
Expand All @@ -187,14 +214,15 @@ export function Errors({ issues, errors }: ErrorsProps) {
return null;
}

if (displayState === "hidden") {
if (displayState === DisplayState.Hidden) {
return null;
}

if (displayState === "minimized") {
if (displayState === DisplayState.Minimized) {
return (
<ErrorsToast
errorCount={readyErrors.length + issues.length}
severity={onlyHasWarnings ? "warning" : "error"}
onClick={fullscreen}
onClose={hide}
/>
Expand Down
Loading

0 comments on commit 89d0100

Please sign in to comment.