Skip to content

Commit

Permalink
feat(core): when dev server stops, highlight this with own error boun…
Browse files Browse the repository at this point in the history
…dary
  • Loading branch information
jordanl17 committed Sep 7, 2024
1 parent f44786c commit a5a2247
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 29 deletions.
23 changes: 19 additions & 4 deletions packages/sanity/src/core/error/ErrorLogger.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import {useToast} from '@sanity/ui'
import {useEffect} from 'react'
import {lazy, useCallback, useEffect, useState} from 'react'

import {ConfigResolutionError, SchemaError} from '../config'
import {CorsOriginError} from '../store'
import {globalScope} from '../util'

const DevServerStatusToast = lazy(() => import('../studio/DevServerStatus'))
// const devServerStatus = process.env.NODE_ENV === 'development' && <DevServerStatus />

const errorChannel = globalScope.__sanityErrorChannel

/**
Expand All @@ -13,8 +16,14 @@ const errorChannel = globalScope.__sanityErrorChannel
*
* @internal
*/
export function ErrorLogger(): null {
export function ErrorLogger() {
const {push: pushToast} = useToast()
const [isDevServerRunning, setIsDevServerRunning] = useState(false)

const handleOnServerStateChange = useCallback<(isServerRunning: boolean) => void>(
(isServerRunning) => setIsDevServerRunning(isServerRunning),
[],
)

useEffect(() => {
if (!errorChannel) return undefined
Expand All @@ -33,6 +42,10 @@ export function ErrorLogger(): null {
return
}

if (!isDevServerRunning) {
return
}

console.error(msg.error)

pushToast({
Expand All @@ -46,9 +59,11 @@ export function ErrorLogger(): null {
status: 'error',
})
})
}, [pushToast])
}, [isDevServerRunning, pushToast])

return null
return process.env.NODE_ENV === 'development' ? (
<DevServerStatusToast onServerStateChange={handleOnServerStateChange} />
) : null
}

function isKnownError(err: Error): boolean {
Expand Down
167 changes: 167 additions & 0 deletions packages/sanity/src/core/studio/DevServerStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import {
Card,
Container,
ErrorBoundary,
type ErrorBoundaryProps,
Heading,
Stack,
useToast,
} from '@sanity/ui'
import {type ErrorInfo, useCallback, useEffect, useRef, useState} from 'react'

import {errorReporter} from '../error/errorReporter'

console.log('LOADED DEV SERVER STATUS')

export const useDetectDevServerDisconnect = () => {
const [serverStopped, setServerStopped] = useState(false)
const serverIsReadyRef = useRef(false)

useEffect(() => {
const url = `ws://${window.location.hostname}:${window.location.port}/`
const ws = new WebSocket(url, 'vite-hmr')

ws.onclose = () => {
if (!serverIsReadyRef.current) return
setServerStopped(true)
}
ws.onopen = () => {
if (!serverIsReadyRef.current) {
serverIsReadyRef.current = true
}

setServerStopped(false)
}

return () => ws.close()
}, [])

return serverStopped
}

const DevServerStatusToast = ({
onServerStateChange,
}: {
onServerStateChange?: (isServerRunning: boolean) => void
}) => {
const serverStopped = useDetectDevServerDisconnect()

const toast = useToast()

useEffect(() => {
onServerStateChange?.(serverStopped)
}, [onServerStateChange, serverStopped])

useEffect(() => {
if (serverStopped) {
toast.push({
id: 'dev-server-stopped',
duration: 60000,
closable: true,
status: 'error',
title: 'Dev server stopped',
description:
'The development server has stopped. You may need to restart it to continue working.',
})
}
}, [serverStopped, toast])

return null
}

type ErrorBoundaryState =
| {
componentStack: null
error: null
eventId: null
}
| {
componentStack: ErrorInfo['componentStack']
error: Error
eventId: string | null
}

export class DevServerStopError extends Error {
constructor() {
super('DevServerStopError')
this.name = 'DevServerStopError'
}
}

const INITIAL_STATE = {
componentStack: null,
error: null,
eventId: null,
} satisfies ErrorBoundaryState

const DevServerStatusThrower = () => {
const serverStopped = useDetectDevServerDisconnect()

if (serverStopped) {
throw new DevServerStopError()
}

return null
}

export const DevServerStatusError = ({
children,
}: React.PropsWithChildren<{
onServerStateChange?: (isServerRunning: boolean) => void
}>) => {
const serverStopped = useDetectDevServerDisconnect()
const [{error, eventId}, setError] = useState<ErrorBoundaryState>(INITIAL_STATE)

const handleCatchError: ErrorBoundaryProps['onCatch'] = useCallback(
(params) => {
const report = errorReporter.reportError(params.error, {
reactErrorInfo: params.info,
errorBoundary: 'StudioErrorBoundary',
})

if (serverStopped) {
setError({
error: params.error,
componentStack: params.info.componentStack,
eventId: report?.eventId || null,
})
} else {
throw params.error
}
},
[serverStopped],
)

if (error instanceof DevServerStopError) {
return (
<Card
height="fill"
overflow="auto"
paddingY={[4, 5, 6, 7]}
paddingX={4}
sizing="border"
tone="critical"
>
<Container width={3}>
<Stack space={4}>
{/* TODO: better error boundary */}
{/* eslint-disable-next-line i18next/no-literal-string */}
<Heading>Dev server stopped</Heading>
<Card border radius={2} overflow="auto" padding={4} tone="inherit">
<Stack space={4} />
</Card>
</Stack>
</Container>
</Card>
)
}

return (
<ErrorBoundary onCatch={handleCatchError}>
<DevServerStatusThrower />
{children}
</ErrorBoundary>
)
}

export default DevServerStatusToast
10 changes: 9 additions & 1 deletion packages/sanity/src/core/studio/StudioLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable i18next/no-literal-string, @sanity/i18n/no-attribute-template-literals */
import {Card, Flex} from '@sanity/ui'
import {startCase} from 'lodash'
import {Suspense, useCallback, useEffect, useMemo, useState} from 'react'
import {lazy, Suspense, useCallback, useEffect, useMemo, useState} from 'react'
import {NavbarContext} from 'sanity/_singletons'
import {RouteScope, useRouter, useRouterState} from 'sanity/router'
import {styled} from 'styled-components'
Expand All @@ -18,6 +18,13 @@ import {
import {StudioErrorBoundary} from './StudioErrorBoundary'
import {useWorkspace} from './workspace'

// const DevServerStatus = lazy(() => import('./DevServerStatus'))
// const devServerStatus = process.env.NODE_ENV === 'development' && <DevServerStatus />

const DevServerStatusError = lazy(() =>
import('./DevServerStatus').then((module) => ({default: module.DevServerStatusError})),
)

const SearchFullscreenPortalCard = styled(Card)`
height: 100%;
left: 0;
Expand Down Expand Up @@ -173,6 +180,7 @@ export function StudioLayoutComponent() {
{/* By using the tool name as the key on the error boundary, we force it to re-render
when switching tools, which ensures we don't show the wrong tool having crashed */}
<StudioErrorBoundary key={activeTool?.name} heading={`The ${activeTool?.name} tool crashed`}>
{process.env.NODE_ENV === 'development' && <DevServerStatusError />}
<Card flex={1} hidden={searchFullscreenOpen}>
{activeTool && activeToolName && (
<RouteScope
Expand Down
62 changes: 38 additions & 24 deletions packages/sanity/src/core/studio/StudioProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {ToastProvider} from '@sanity/ui'
import {type ReactNode, useMemo} from 'react'
import {lazy, type PropsWithChildren, type ReactNode, useMemo} from 'react'
import Refractor from 'react-refractor'
import bash from 'refractor/lang/bash.js'
import javascript from 'refractor/lang/javascript.js'
Expand Down Expand Up @@ -32,12 +32,24 @@ import {StudioThemeProvider} from './StudioThemeProvider'
import {WorkspaceLoader} from './workspaceLoader'
import {WorkspacesProvider} from './workspaces'

const DevServerStatusError = lazy(() =>
import('./DevServerStatus').then((module) => ({default: module.DevServerStatusError})),
)

Refractor.registerLanguage(bash)
Refractor.registerLanguage(javascript)
Refractor.registerLanguage(json)
Refractor.registerLanguage(jsx)
Refractor.registerLanguage(typescript)

const ShouldIUseTheDev = ({children}: PropsWithChildren) => {
if (process.env.NODE_ENV === 'development') {
return <DevServerStatusError>{children}</DevServerStatusError>
}

return <>{children}</>
}

/**
* @hidden
* @beta */
Expand Down Expand Up @@ -83,29 +95,31 @@ export function StudioProvider({
<ToastProvider paddingY={7} zOffset={Z_OFFSET.toast}>
<ErrorLogger />
<StudioErrorBoundary>
<WorkspacesProvider config={config} basePath={basePath} LoadingComponent={LoadingBlock}>
<ActiveWorkspaceMatcher
unstable_history={history}
NotFoundComponent={NotFoundScreen}
LoadingComponent={LoadingBlock}
>
<StudioThemeProvider>
<UserColorManagerProvider>
{noAuthBoundary ? (
_children
) : (
<AuthBoundary
LoadingComponent={LoadingBlock}
AuthenticateComponent={AuthenticateScreen}
NotAuthenticatedComponent={NotAuthenticatedScreen}
>
{_children}
</AuthBoundary>
)}
</UserColorManagerProvider>
</StudioThemeProvider>
</ActiveWorkspaceMatcher>
</WorkspacesProvider>
<ShouldIUseTheDev>
<WorkspacesProvider config={config} basePath={basePath} LoadingComponent={LoadingBlock}>
<ActiveWorkspaceMatcher
unstable_history={history}
NotFoundComponent={NotFoundScreen}
LoadingComponent={LoadingBlock}
>
<StudioThemeProvider>
<UserColorManagerProvider>
{noAuthBoundary ? (
_children
) : (
<AuthBoundary
LoadingComponent={LoadingBlock}
AuthenticateComponent={AuthenticateScreen}
NotAuthenticatedComponent={NotAuthenticatedScreen}
>
{_children}
</AuthBoundary>
)}
</UserColorManagerProvider>
</StudioThemeProvider>
</ActiveWorkspaceMatcher>
</WorkspacesProvider>
</ShouldIUseTheDev>
</StudioErrorBoundary>
</ToastProvider>
</ColorSchemeProvider>
Expand Down

0 comments on commit a5a2247

Please sign in to comment.