Skip to content

Commit 7fcf22a

Browse files
authored
feat(core): specific error when dev server is stopped (#7476)
* 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
1 parent ae1c9c2 commit 7fcf22a

File tree

9 files changed

+142
-17
lines changed

9 files changed

+142
-17
lines changed

package.json

-3
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,6 @@
9999
},
100100
"prettier": "@sanity/prettier-config",
101101
"devDependencies": {
102-
"@babel/preset-env": "^7.24.7",
103-
"@babel/preset-react": "^7.24.7",
104-
"@babel/preset-typescript": "^7.24.7",
105102
"@google-cloud/storage": "^7.11.0",
106103
"@playwright/test": "1.44.1",
107104
"@repo/dev-aliases": "workspace:*",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"ignores": [
3+
"@babel/preset-env",
4+
"@babel/preset-react",
5+
"@babel/preset-typescript",
6+
"babel-plugin-transform-vite-meta-hot"
7+
]
8+
}

packages/@repo/test-config/jest/createJestConfig.mjs

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
/* eslint-disable tsdoc/syntax */
22

3-
import devAliases from '@repo/dev-aliases'
4-
53
import path from 'node:path'
4+
5+
import devAliases from '@repo/dev-aliases'
66
import {escapeRegExp, omit} from 'lodash-es'
7+
78
import {resolveDirName} from './resolveDirName.mjs'
89

910
const dirname = resolveDirName(import.meta.url)
@@ -71,7 +72,6 @@ export function createJestConfig(config = {}) {
7172
resolver: path.resolve(dirname, './resolver.cjs'),
7273
testEnvironment: path.resolve(dirname, './jsdom.jest.env.ts'),
7374
setupFiles: [...setupFiles, path.resolve(dirname, './setup.ts')],
74-
// testEnvironment: 'jsdom',
7575
testEnvironmentOptions: {
7676
url: 'http://localhost:3333',
7777
},
@@ -103,6 +103,7 @@ export function createJestConfig(config = {}) {
103103
'@babel/preset-typescript',
104104
['@babel/preset-react', {runtime: 'automatic'}],
105105
],
106+
plugins: ['babel-plugin-transform-vite-meta-hot'],
106107
},
107108
],
108109
},

packages/@repo/test-config/package.json

+4
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,12 @@
99
"./vitest": "./vitest/index.mjs"
1010
},
1111
"devDependencies": {
12+
"@babel/preset-env": "^7.24.7",
13+
"@babel/preset-react": "^7.24.7",
14+
"@babel/preset-typescript": "^7.24.7",
1215
"@jest/globals": "^29.7.0",
1316
"@repo/dev-aliases": "workspace:*",
17+
"babel-plugin-transform-vite-meta-hot": "^1.0.0",
1418
"dotenv": "^16.4.5",
1519
"jest-environment-jsdom": "^29.7.0",
1620
"lodash-es": "^4.17.21",

packages/sanity/src/core/error/ErrorLogger.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,9 @@ function isKnownError(err: Error): boolean {
6464
return true
6565
}
6666

67+
if ('ViteDevServerStoppedError' in err && err.ViteDevServerStoppedError) {
68+
return true
69+
}
70+
6771
return false
6872
}

packages/sanity/src/core/studio/StudioErrorBoundary.tsx

+23-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,14 @@ import {
1111
Stack,
1212
Text,
1313
} from '@sanity/ui'
14-
import {type ComponentType, type ErrorInfo, type ReactNode, useCallback, useState} from 'react'
14+
import {
15+
type ComponentType,
16+
type ErrorInfo,
17+
lazy,
18+
type ReactNode,
19+
useCallback,
20+
useState,
21+
} from 'react'
1522
import {ErrorActions, isDev, isProd} from 'sanity'
1623
import {styled} from 'styled-components'
1724
import {useHotModuleReload} from 'use-hot-module-reload'
@@ -22,6 +29,17 @@ import {CorsOriginError} from '../store'
2229
import {isRecord} from '../util'
2330
import {CorsOriginErrorScreen, SchemaErrorsScreen} from './screens'
2431

32+
/**
33+
* The DevServerStoppedErrorScreen will always have been lazy loaded to client
34+
* in instances where it is used, since DevServerStoppedError is only thrown
35+
* when this module is loaded, and this screen is also conditional on this error type
36+
*/
37+
const DevServerStoppedErrorScreen = lazy(() =>
38+
import('./ViteDevServerStopped').then((DevServerStopped) => ({
39+
default: DevServerStopped.DevServerStoppedErrorScreen,
40+
})),
41+
)
42+
2543
interface StudioErrorBoundaryProps {
2644
children: ReactNode
2745
heading?: string
@@ -80,6 +98,10 @@ export const StudioErrorBoundary: ComponentType<StudioErrorBoundaryProps> = ({
8098
return <SchemaErrorsScreen schema={error.schema} />
8199
}
82100

101+
if (error && 'ViteDevServerStoppedError' in error && error.ViteDevServerStoppedError) {
102+
return <DevServerStoppedErrorScreen />
103+
}
104+
83105
if (!error) {
84106
return <ErrorBoundary onCatch={handleCatchError}>{children}</ErrorBoundary>
85107
}

packages/sanity/src/core/studio/StudioLayout.tsx

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable i18next/no-literal-string, @sanity/i18n/no-attribute-template-literals */
22
import {Card, Flex} from '@sanity/ui'
33
import {startCase} from 'lodash'
4-
import {Suspense, useCallback, useEffect, useMemo, useState} from 'react'
4+
import {lazy, Suspense, useCallback, useEffect, useMemo, useState} from 'react'
55
import {NavbarContext} from 'sanity/_singletons'
66
import {RouteScope, useRouter, useRouterState} from 'sanity/router'
77
import {styled} from 'styled-components'
@@ -18,6 +18,14 @@ import {
1818
import {StudioErrorBoundary} from './StudioErrorBoundary'
1919
import {useWorkspace} from './workspace'
2020

21+
const DetectViteDevServerStopped = lazy(() =>
22+
import('./ViteDevServerStopped').then((DevServerStopped) => ({
23+
default: DevServerStopped.DetectViteDevServerStopped,
24+
})),
25+
)
26+
27+
const detectViteDevServerStopped = import.meta.hot && process.env.NODE_ENV === 'development'
28+
2129
const SearchFullscreenPortalCard = styled(Card)`
2230
height: 100%;
2331
left: 0;
@@ -173,6 +181,7 @@ export function StudioLayoutComponent() {
173181
{/* By using the tool name as the key on the error boundary, we force it to re-render
174182
when switching tools, which ensures we don't show the wrong tool having crashed */}
175183
<StudioErrorBoundary key={activeTool?.name} heading={`The ${activeTool?.name} tool crashed`}>
184+
{detectViteDevServerStopped && <DetectViteDevServerStopped />}
176185
<Card flex={1} hidden={searchFullscreenOpen}>
177186
{activeTool && activeToolName && (
178187
<RouteScope
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/* eslint-disable i18next/no-literal-string -- will not support i18n in error boundaries */
2+
import {Card, Container, Heading, Stack, Text} from '@sanity/ui'
3+
import {type ReactNode, useCallback, useEffect, useState} from 'react'
4+
import {type ViteHotContext} from 'vite/types/hot.js'
5+
6+
const ERROR_TITLE = 'Dev server stopped'
7+
const ERROR_DESCRIPTION =
8+
'The development server has stopped. You may need to restart it to continue working.'
9+
10+
class ViteDevServerStoppedError extends Error {
11+
ViteDevServerStoppedError: boolean
12+
13+
constructor() {
14+
super(ERROR_TITLE)
15+
this.name = 'ViteDevServerStoppedError'
16+
this.ViteDevServerStoppedError = true
17+
}
18+
}
19+
const serverHot = import.meta.hot
20+
const isViteServer = (hot: unknown): hot is ViteHotContext => Boolean(hot)
21+
22+
const useDetectViteDevServerStopped = () => {
23+
const [devServerStopped, setDevServerStopped] = useState(false)
24+
25+
const markDevServerStopped = useCallback(() => setDevServerStopped(true), [])
26+
27+
useEffect(() => {
28+
// no early return to optimize tree-shaking
29+
if (isViteServer(serverHot)) {
30+
serverHot.on('vite:ws:disconnect', markDevServerStopped)
31+
}
32+
}, [markDevServerStopped])
33+
34+
return {devServerStopped}
35+
}
36+
37+
const ThrowViteServerStopped = () => {
38+
const {devServerStopped} = useDetectViteDevServerStopped()
39+
40+
if (devServerStopped) throw new ViteDevServerStoppedError()
41+
42+
return null
43+
}
44+
45+
export const DetectViteDevServerStopped = (): ReactNode =>
46+
isViteServer(serverHot) ? <ThrowViteServerStopped /> : null
47+
48+
export const DevServerStoppedErrorScreen = (): ReactNode => (
49+
<Card
50+
height="fill"
51+
overflow="auto"
52+
paddingY={[4, 5, 6, 7]}
53+
paddingX={4}
54+
sizing="border"
55+
tone="critical"
56+
>
57+
<Container width={3}>
58+
<Stack space={4}>
59+
<Heading>{ERROR_TITLE}</Heading>
60+
61+
<Card border radius={2} overflow="auto" padding={4} tone="inherit">
62+
<Stack space={4}>
63+
<Text size={2}>{ERROR_DESCRIPTION}</Text>
64+
</Stack>
65+
</Card>
66+
</Stack>
67+
</Container>
68+
</Card>
69+
)

pnpm-lock.yaml

+20-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)