Skip to content

Commit

Permalink
feat(core): specific error when dev server is stopped (#7476)
Browse files Browse the repository at this point in the history
* feat(core): when dev server stops, highlight this with own error boundary

* feat(core): simplified, and using nested error boundary for catching dev server stop error

* refactor(core): simplifying use of DevServerStatus as lazy import

* refactor(core): naming refactor

* feat(core): removing dev server stopped toast as unnecessary

* refactor(core): using built ins from vite; making more explicit this is only for default vite

* chore: moving babel vite transform to test-config package

* chore: moving babel deps to test-config

* chore: tidy to lock and named export for type used

* fix: ignoring un-identified uses of babel presets and plugins in test-config

* chore: naming of vite dev server stopped error
  • Loading branch information
jordanl17 committed Sep 30, 2024
1 parent ae1c9c2 commit 7fcf22a
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 17 deletions.
3 changes: 0 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,6 @@
},
"prettier": "@sanity/prettier-config",
"devDependencies": {
"@babel/preset-env": "^7.24.7",
"@babel/preset-react": "^7.24.7",
"@babel/preset-typescript": "^7.24.7",
"@google-cloud/storage": "^7.11.0",
"@playwright/test": "1.44.1",
"@repo/dev-aliases": "workspace:*",
Expand Down
8 changes: 8 additions & 0 deletions packages/@repo/test-config/.depcheckrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"ignores": [
"@babel/preset-env",
"@babel/preset-react",
"@babel/preset-typescript",
"babel-plugin-transform-vite-meta-hot"
]
}
7 changes: 4 additions & 3 deletions packages/@repo/test-config/jest/createJestConfig.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/* eslint-disable tsdoc/syntax */

import devAliases from '@repo/dev-aliases'

import path from 'node:path'

import devAliases from '@repo/dev-aliases'
import {escapeRegExp, omit} from 'lodash-es'

import {resolveDirName} from './resolveDirName.mjs'

const dirname = resolveDirName(import.meta.url)
Expand Down Expand Up @@ -71,7 +72,6 @@ export function createJestConfig(config = {}) {
resolver: path.resolve(dirname, './resolver.cjs'),
testEnvironment: path.resolve(dirname, './jsdom.jest.env.ts'),
setupFiles: [...setupFiles, path.resolve(dirname, './setup.ts')],
// testEnvironment: 'jsdom',
testEnvironmentOptions: {
url: 'http://localhost:3333',
},
Expand Down Expand Up @@ -103,6 +103,7 @@ export function createJestConfig(config = {}) {
'@babel/preset-typescript',
['@babel/preset-react', {runtime: 'automatic'}],
],
plugins: ['babel-plugin-transform-vite-meta-hot'],
},
],
},
Expand Down
4 changes: 4 additions & 0 deletions packages/@repo/test-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@
"./vitest": "./vitest/index.mjs"
},
"devDependencies": {
"@babel/preset-env": "^7.24.7",
"@babel/preset-react": "^7.24.7",
"@babel/preset-typescript": "^7.24.7",
"@jest/globals": "^29.7.0",
"@repo/dev-aliases": "workspace:*",
"babel-plugin-transform-vite-meta-hot": "^1.0.0",
"dotenv": "^16.4.5",
"jest-environment-jsdom": "^29.7.0",
"lodash-es": "^4.17.21",
Expand Down
4 changes: 4 additions & 0 deletions packages/sanity/src/core/error/ErrorLogger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,9 @@ function isKnownError(err: Error): boolean {
return true
}

if ('ViteDevServerStoppedError' in err && err.ViteDevServerStoppedError) {
return true
}

return false
}
24 changes: 23 additions & 1 deletion packages/sanity/src/core/studio/StudioErrorBoundary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@ import {
Stack,
Text,
} from '@sanity/ui'
import {type ComponentType, type ErrorInfo, type ReactNode, useCallback, useState} from 'react'
import {
type ComponentType,
type ErrorInfo,
lazy,
type ReactNode,
useCallback,
useState,
} from 'react'
import {ErrorActions, isDev, isProd} from 'sanity'
import {styled} from 'styled-components'
import {useHotModuleReload} from 'use-hot-module-reload'
Expand All @@ -22,6 +29,17 @@ import {CorsOriginError} from '../store'
import {isRecord} from '../util'
import {CorsOriginErrorScreen, SchemaErrorsScreen} from './screens'

/**
* The DevServerStoppedErrorScreen will always have been lazy loaded to client
* in instances where it is used, since DevServerStoppedError is only thrown
* when this module is loaded, and this screen is also conditional on this error type
*/
const DevServerStoppedErrorScreen = lazy(() =>
import('./ViteDevServerStopped').then((DevServerStopped) => ({
default: DevServerStopped.DevServerStoppedErrorScreen,
})),
)

interface StudioErrorBoundaryProps {
children: ReactNode
heading?: string
Expand Down Expand Up @@ -80,6 +98,10 @@ export const StudioErrorBoundary: ComponentType<StudioErrorBoundaryProps> = ({
return <SchemaErrorsScreen schema={error.schema} />
}

if (error && 'ViteDevServerStoppedError' in error && error.ViteDevServerStoppedError) {
return <DevServerStoppedErrorScreen />
}

if (!error) {
return <ErrorBoundary onCatch={handleCatchError}>{children}</ErrorBoundary>
}
Expand Down
11 changes: 10 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,14 @@ import {
import {StudioErrorBoundary} from './StudioErrorBoundary'
import {useWorkspace} from './workspace'

const DetectViteDevServerStopped = lazy(() =>
import('./ViteDevServerStopped').then((DevServerStopped) => ({
default: DevServerStopped.DetectViteDevServerStopped,
})),
)

const detectViteDevServerStopped = import.meta.hot && process.env.NODE_ENV === 'development'

const SearchFullscreenPortalCard = styled(Card)`
height: 100%;
left: 0;
Expand Down Expand Up @@ -173,6 +181,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`}>
{detectViteDevServerStopped && <DetectViteDevServerStopped />}
<Card flex={1} hidden={searchFullscreenOpen}>
{activeTool && activeToolName && (
<RouteScope
Expand Down
69 changes: 69 additions & 0 deletions packages/sanity/src/core/studio/ViteDevServerStopped.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/* eslint-disable i18next/no-literal-string -- will not support i18n in error boundaries */
import {Card, Container, Heading, Stack, Text} from '@sanity/ui'
import {type ReactNode, useCallback, useEffect, useState} from 'react'
import {type ViteHotContext} from 'vite/types/hot.js'

const ERROR_TITLE = 'Dev server stopped'
const ERROR_DESCRIPTION =
'The development server has stopped. You may need to restart it to continue working.'

class ViteDevServerStoppedError extends Error {
ViteDevServerStoppedError: boolean

constructor() {
super(ERROR_TITLE)
this.name = 'ViteDevServerStoppedError'
this.ViteDevServerStoppedError = true
}
}
const serverHot = import.meta.hot
const isViteServer = (hot: unknown): hot is ViteHotContext => Boolean(hot)

const useDetectViteDevServerStopped = () => {
const [devServerStopped, setDevServerStopped] = useState(false)

const markDevServerStopped = useCallback(() => setDevServerStopped(true), [])

useEffect(() => {
// no early return to optimize tree-shaking
if (isViteServer(serverHot)) {
serverHot.on('vite:ws:disconnect', markDevServerStopped)
}
}, [markDevServerStopped])

return {devServerStopped}
}

const ThrowViteServerStopped = () => {
const {devServerStopped} = useDetectViteDevServerStopped()

if (devServerStopped) throw new ViteDevServerStoppedError()

return null
}

export const DetectViteDevServerStopped = (): ReactNode =>
isViteServer(serverHot) ? <ThrowViteServerStopped /> : null

export const DevServerStoppedErrorScreen = (): ReactNode => (
<Card
height="fill"
overflow="auto"
paddingY={[4, 5, 6, 7]}
paddingX={4}
sizing="border"
tone="critical"
>
<Container width={3}>
<Stack space={4}>
<Heading>{ERROR_TITLE}</Heading>

<Card border radius={2} overflow="auto" padding={4} tone="inherit">
<Stack space={4}>
<Text size={2}>{ERROR_DESCRIPTION}</Text>
</Stack>
</Card>
</Stack>
</Container>
</Card>
)
29 changes: 20 additions & 9 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 7fcf22a

Please sign in to comment.