Repository: vercel/next.js
Pull request: [dev-tools] add restart dev server button to error overlay

### Why?

When the user reloads on a specific error, and that error persists, we show the restart server button as an option. This is because some errors are recoverable by restarting the server and rebuilding the app.

https://github.com/user-attachments/assets/4e65d26c-8344-47b8-a6b9-1151008c271c

When Turbopack persistent cache is enabled, it will also clear the bundler cache. This improves DX by replacing the need to run `rm -rf .next` manually.

https://github.com/user-attachments/assets/e3f08176-5e9e-41fc-a8ce-9e731a45a6f2

Closes NEXT-4477

Changed files:
filename: packages/next/src/build/define-env.ts
patch: @@ -286,6 +286,8 @@ export function getDefineEnv({
       : {}),
     'process.env.__NEXT_DEVTOOL_SEGMENT_EXPLORER':
       config.experimental.devtoolSegmentExplorer ?? false,
+    'process.env.__NEXT_TURBOPACK_PERSISTENT_CACHE':
+      config.experimental.turbopackPersistentCaching ?? false,
   }
 
   const userDefines = config.compiler?.define ?? {}

filename: packages/next/src/client/components/react-dev-overlay/ui/components/errors/error-overlay-toolbar/error-overlay-toolbar.tsx
patch: @@ -2,6 +2,7 @@ import type { DebugInfo } from '../../../../types'
 import { NodejsInspectorButton } from './nodejs-inspector-button'
 import { CopyStackTraceButton } from './copy-stack-trace-button'
 import { DocsLinkButton } from './docs-link-button'
+import { RestartServerButton } from './restart-server-button'
 
 type ErrorOverlayToolbarProps = {
   error: Error
@@ -14,6 +15,7 @@ export function ErrorOverlayToolbar({
 }: ErrorOverlayToolbarProps) {
   return (
     <span className="error-overlay-toolbar">
+      <RestartServerButton error={error} />
       <CopyStackTraceButton error={error} />
       <DocsLinkButton errorMessage={error.message} />
       <NodejsInspectorButton

filename: packages/next/src/client/components/react-dev-overlay/ui/components/errors/error-overlay-toolbar/restart-server-button.tsx
patch: @@ -0,0 +1,79 @@
+import { useState, useEffect } from 'react'
+import { RefreshClockWise } from '../../../icons/refresh-clock-wise'
+
+/**
+ * When the user reloads on a specific error and that error persists, we show
+ * the restart server button as an option. This is because some errors are
+ * recoverable by restarting the server and rebuilding the app.
+ *
+ * When Turbopack persistent cache is enabled, it will also clear the bundler
+ * cache. This improves DX by replacing the need to run `rm -rf .next` manually.
+ */
+export function RestartServerButton({ error }: { error: Error }) {
+  const [showButton, setShowButton] = useState(false)
+
+  useEffect(() => {
+    const ERROR_KEY = `__next_error_overlay:${window.location.pathname}:${error.message}`
+
+    setShowButton(sessionStorage.getItem(ERROR_KEY) === '1')
+
+    // When the user tries to reload, set the error key to the session storage.
+    const handleBeforeUnload = () => {
+      sessionStorage.setItem(ERROR_KEY, '1')
+    }
+    window.addEventListener('beforeunload', handleBeforeUnload)
+    return () => {
+      window.removeEventListener('beforeunload', handleBeforeUnload)
+    }
+  }, [error.message])
+
+  if (!showButton) {
+    return null
+  }
+
+  function handleClick() {
+    let endpoint = '/__nextjs_restart_dev'
+
+    if (process.env.__NEXT_TURBOPACK_PERSISTENT_CACHE) {
+      endpoint = '/__nextjs_restart_dev?invalidatePersistentCache'
+    }
+
+    // TODO: Use Client Action for transition indicator when DevTools is isolated.
+    fetch(endpoint, {
+      method: 'POST',
+    }).then(() => {
+      // TODO: poll server status and reload when the server is back up.
+      // https://github.com/vercel/next.js/pull/80005
+    })
+  }
+
+  return (
+    <button className="restart-dev-server-button" onClick={handleClick}>
+      <RefreshClockWise width={14} height={14} />
+      Restart Dev Server
+    </button>
+  )
+}
+
+export const RESTART_SERVER_BUTTON_STYLES = `
+  .restart-dev-server-button {
+    -webkit-font-smoothing: antialiased;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    gap: 4px;
+
+    height: var(--size-26);
+    padding: 6px 8px 6px 6px;
+    background: var(--color-amber-100);
+    background-clip: padding-box;
+    border: 1px solid var(--color-gray-alpha-400);
+    box-shadow: var(--shadow-small);
+    border-radius: var(--rounded-full);
+
+    color: var(--color-amber-900);
+    font-size: var(--size-12);
+    font-weight: 500;
+    line-height: var(--size-16);
+  }
+`

filename: packages/next/src/client/components/react-dev-overlay/ui/icons/refresh-clock-wise.tsx
patch: @@ -0,0 +1,10 @@
+export function RefreshClockWise(props: React.SVGProps<SVGSVGElement>) {
+  return (
+    <svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" {...props}>
+      <path
+        d="M4.30969 8.37891H2.38782C2.9805 10.3647 4.82188 11.8124 7.00012 11.8125C8.81977 11.8124 10.4043 10.8024 11.2228 9.30957L11.5382 8.73438L12.6896 9.36523L12.3741 9.94043L12.1681 10.2891C11.0815 11.993 9.17324 13.1249 7.00012 13.125C4.42207 13.1249 2.21747 11.5322 1.3136 9.27734V11.375H0.00109863V7.72266L0.0147705 7.58984C0.0760304 7.29089 0.340278 7.06641 0.657349 7.06641H4.30969V8.37891ZM7.00012 0.875C9.57685 0.875118 11.7819 2.46569 12.6866 4.71875V2.625H13.9991V6.27734C13.9991 6.63974 13.7053 6.93354 13.3429 6.93359H9.68958V5.62109H11.6115C11.0186 3.6356 9.17813 2.18763 7.00012 2.1875C5.17131 2.18757 3.57959 3.20771 2.76477 4.71289L2.45227 5.29004L1.29797 4.66504L1.61047 4.08789C2.64547 2.17605 4.67052 0.875068 7.00012 0.875Z"
+        fill="#A35200"
+      />
+    </svg>
+  )
+}

filename: packages/next/src/client/components/react-dev-overlay/ui/styles/component-styles.tsx
patch: @@ -23,6 +23,7 @@ import { DEV_TOOLS_INFO_ROUTE_INFO_STYLES } from '../components/errors/dev-tools
 import { DEV_TOOLS_INFO_USER_PREFERENCES_STYLES } from '../components/errors/dev-tools-indicator/dev-tools-info/user-preferences'
 import { DEV_TOOLS_INFO_RENDER_FILES_STYLES } from '../components/overview/segment-explorer'
 import { FADER_STYLES } from '../components/fader'
+import { RESTART_SERVER_BUTTON_STYLES } from '../components/errors/error-overlay-toolbar/restart-server-button'
 
 export function ComponentStyles() {
   return (
@@ -45,6 +46,7 @@ export function ComponentStyles() {
         ${containerErrorStyles}
         ${containerRuntimeErrorStyles}
         ${versionStaleness}
+        ${RESTART_SERVER_BUTTON_STYLES}
         ${DEV_TOOLS_INDICATOR_STYLES}
         ${DEV_TOOLS_INFO_STYLES}
         ${DEV_TOOLS_INFO_TURBOPACK_INFO_STYLES}