Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

show turbopack warnings in error overlay #3465

Merged
merged 7 commits into from
Feb 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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