diff options
author | 2022-10-07 19:44:41 +0200 | |
---|---|---|
committer | 2022-11-05 19:41:14 +0100 | |
commit | abe176d0888c0fdcc803ddafe71a5cee9f4b63a0 (patch) | |
tree | ea7d5722c8e981ba540e648fbacbfd0e4a55768a /subprojects/frontend/src | |
parent | fix: test and lint failures (diff) | |
download | refinery-abe176d0888c0fdcc803ddafe71a5cee9f4b63a0.tar.gz refinery-abe176d0888c0fdcc803ddafe71a5cee9f4b63a0.tar.zst refinery-abe176d0888c0fdcc803ddafe71a5cee9f4b63a0.zip |
refactor(frontend): improve HMR experience
Use a HMR acceptor as in
https://github.com/vitejs/vite/issues/10227#issuecomment-1256969751
Also updates frontend tooling to the latest version
(yarn now support typescript 4.8.4)
Diffstat (limited to 'subprojects/frontend/src')
-rw-r--r-- | subprojects/frontend/src/App.tsx | 44 | ||||
-rw-r--r-- | subprojects/frontend/src/Refinery.tsx | 21 | ||||
-rw-r--r-- | subprojects/frontend/src/RootStore.ts (renamed from subprojects/frontend/src/RootStore.tsx) | 37 | ||||
-rw-r--r-- | subprojects/frontend/src/RootStoreProvider.tsx | 29 | ||||
-rw-r--r-- | subprojects/frontend/src/ToggleDarkModeButton.tsx | 2 | ||||
-rw-r--r-- | subprojects/frontend/src/TopBar.tsx | 2 | ||||
-rw-r--r-- | subprojects/frontend/src/UpdateNotification.tsx | 2 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/EditorPane.tsx | 2 | ||||
-rw-r--r-- | subprojects/frontend/src/editor/EditorStore.ts | 4 | ||||
-rw-r--r-- | subprojects/frontend/src/index.tsx | 76 | ||||
-rw-r--r-- | subprojects/frontend/src/theme/ThemeProvider.tsx | 2 | ||||
-rw-r--r-- | subprojects/frontend/src/xtext/XtextClient.ts | 4 |
12 files changed, 145 insertions, 80 deletions
diff --git a/subprojects/frontend/src/App.tsx b/subprojects/frontend/src/App.tsx index 90514044..a471690b 100644 --- a/subprojects/frontend/src/App.tsx +++ b/subprojects/frontend/src/App.tsx | |||
@@ -1,21 +1,33 @@ | |||
1 | import Grow from '@mui/material/Grow'; | 1 | import Box from '@mui/material/Box'; |
2 | import Stack from '@mui/material/Stack'; | 2 | import CssBaseline from '@mui/material/CssBaseline'; |
3 | import { SnackbarProvider } from 'notistack'; | 3 | import React, { StrictMode, Suspense, lazy } from 'react'; |
4 | import React from 'react'; | ||
5 | 4 | ||
6 | import TopBar from './TopBar'; | 5 | import Loading from './Loading'; |
7 | import UpdateNotification from './UpdateNotification'; | 6 | import type RootStore from './RootStore'; |
8 | import EditorPane from './editor/EditorPane'; | 7 | import RootStoreProvider from './RootStoreProvider'; |
8 | import WindowControlsOverlayColor from './WindowControlsOverlayColor'; | ||
9 | import ThemeProvider from './theme/ThemeProvider'; | ||
9 | 10 | ||
10 | export default function App(): JSX.Element { | 11 | const Refinery = lazy(() => import('./Refinery.js')); |
12 | |||
13 | export default function App({ | ||
14 | rootStore, | ||
15 | }: { | ||
16 | rootStore: RootStore; | ||
17 | }): JSX.Element { | ||
11 | return ( | 18 | return ( |
12 | // @ts-expect-error -- notistack has problems with `exactOptionalPropertyTypes | 19 | <StrictMode> |
13 | <SnackbarProvider TransitionComponent={Grow}> | 20 | <RootStoreProvider rootStore={rootStore}> |
14 | <UpdateNotification /> | 21 | <ThemeProvider> |
15 | <Stack direction="column" height="100vh" overflow="auto"> | 22 | <CssBaseline enableColorScheme /> |
16 | <TopBar /> | 23 | <WindowControlsOverlayColor /> |
17 | <EditorPane /> | 24 | <Box height="100vh" overflow="auto"> |
18 | </Stack> | 25 | <Suspense fallback={<Loading />}> |
19 | </SnackbarProvider> | 26 | <Refinery /> |
27 | </Suspense> | ||
28 | </Box> | ||
29 | </ThemeProvider> | ||
30 | </RootStoreProvider> | ||
31 | </StrictMode> | ||
20 | ); | 32 | ); |
21 | } | 33 | } |
diff --git a/subprojects/frontend/src/Refinery.tsx b/subprojects/frontend/src/Refinery.tsx new file mode 100644 index 00000000..41f835b3 --- /dev/null +++ b/subprojects/frontend/src/Refinery.tsx | |||
@@ -0,0 +1,21 @@ | |||
1 | import Grow from '@mui/material/Grow'; | ||
2 | import Stack from '@mui/material/Stack'; | ||
3 | import { SnackbarProvider } from 'notistack'; | ||
4 | import React from 'react'; | ||
5 | |||
6 | import TopBar from './TopBar'; | ||
7 | import UpdateNotification from './UpdateNotification'; | ||
8 | import EditorPane from './editor/EditorPane'; | ||
9 | |||
10 | export default function Refinery(): JSX.Element { | ||
11 | return ( | ||
12 | // @ts-expect-error -- notistack has problems with `exactOptionalPropertyTypes | ||
13 | <SnackbarProvider TransitionComponent={Grow}> | ||
14 | <UpdateNotification /> | ||
15 | <Stack direction="column" height="100vh" overflow="auto"> | ||
16 | <TopBar /> | ||
17 | <EditorPane /> | ||
18 | </Stack> | ||
19 | </SnackbarProvider> | ||
20 | ); | ||
21 | } | ||
diff --git a/subprojects/frontend/src/RootStore.tsx b/subprojects/frontend/src/RootStore.ts index c1674a3c..54a80501 100644 --- a/subprojects/frontend/src/RootStore.tsx +++ b/subprojects/frontend/src/RootStore.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import { getLogger } from 'loglevel'; | 1 | import { getLogger } from 'loglevel'; |
2 | import { makeAutoObservable, runInAction } from 'mobx'; | 2 | import { makeAutoObservable, runInAction } from 'mobx'; |
3 | import React, { createContext, useContext } from 'react'; | ||
4 | 3 | ||
5 | import PWAStore from './PWAStore'; | 4 | import PWAStore from './PWAStore'; |
6 | import type EditorStore from './editor/EditorStore'; | 5 | import type EditorStore from './editor/EditorStore'; |
@@ -15,6 +14,8 @@ export default class RootStore { | |||
15 | 14 | ||
16 | readonly themeStore: ThemeStore; | 15 | readonly themeStore: ThemeStore; |
17 | 16 | ||
17 | disposed = false; | ||
18 | |||
18 | constructor(initialValue: string) { | 19 | constructor(initialValue: string) { |
19 | this.pwaStore = new PWAStore(); | 20 | this.pwaStore = new PWAStore(); |
20 | this.themeStore = new ThemeStore(); | 21 | this.themeStore = new ThemeStore(); |
@@ -25,6 +26,9 @@ export default class RootStore { | |||
25 | import('./editor/EditorStore') | 26 | import('./editor/EditorStore') |
26 | .then(({ default: EditorStore }) => { | 27 | .then(({ default: EditorStore }) => { |
27 | runInAction(() => { | 28 | runInAction(() => { |
29 | if (this.disposed) { | ||
30 | return; | ||
31 | } | ||
28 | this.editorStore = new EditorStore(initialValue, this.pwaStore); | 32 | this.editorStore = new EditorStore(initialValue, this.pwaStore); |
29 | }); | 33 | }); |
30 | }) | 34 | }) |
@@ -32,29 +36,12 @@ export default class RootStore { | |||
32 | log.error('Failed to load EditorStore', error); | 36 | log.error('Failed to load EditorStore', error); |
33 | }); | 37 | }); |
34 | } | 38 | } |
35 | } | ||
36 | |||
37 | const StoreContext = createContext<RootStore | undefined>(undefined); | ||
38 | 39 | ||
39 | export interface RootStoreProviderProps { | 40 | dispose(): void { |
40 | children: JSX.Element; | 41 | if (this.disposed) { |
41 | 42 | return; | |
42 | rootStore: RootStore; | 43 | } |
43 | } | 44 | this.editorStore?.dispose(); |
44 | 45 | this.disposed = true; | |
45 | export function RootStoreProvider({ | ||
46 | children, | ||
47 | rootStore, | ||
48 | }: RootStoreProviderProps): JSX.Element { | ||
49 | return ( | ||
50 | <StoreContext.Provider value={rootStore}>{children}</StoreContext.Provider> | ||
51 | ); | ||
52 | } | ||
53 | |||
54 | export const useRootStore = (): RootStore => { | ||
55 | const rootStore = useContext(StoreContext); | ||
56 | if (!rootStore) { | ||
57 | throw new Error('useRootStore must be used within RootStoreProvider'); | ||
58 | } | 46 | } |
59 | return rootStore; | 47 | } |
60 | }; | ||
diff --git a/subprojects/frontend/src/RootStoreProvider.tsx b/subprojects/frontend/src/RootStoreProvider.tsx new file mode 100644 index 00000000..70ac7776 --- /dev/null +++ b/subprojects/frontend/src/RootStoreProvider.tsx | |||
@@ -0,0 +1,29 @@ | |||
1 | import React, { type ReactNode, createContext, useContext } from 'react'; | ||
2 | |||
3 | import type RootStore from './RootStore'; | ||
4 | |||
5 | const StoreContext = createContext<RootStore | undefined>(undefined); | ||
6 | |||
7 | export function useRootStore(): RootStore { | ||
8 | const rootStore = useContext(StoreContext); | ||
9 | if (!rootStore) { | ||
10 | throw new Error('useRootStore must be used within RootStoreProvider'); | ||
11 | } | ||
12 | return rootStore; | ||
13 | } | ||
14 | |||
15 | export default function RootStoreProvider({ | ||
16 | children, | ||
17 | rootStore, | ||
18 | }: { | ||
19 | children?: ReactNode; | ||
20 | rootStore: RootStore; | ||
21 | }): JSX.Element { | ||
22 | return ( | ||
23 | <StoreContext.Provider value={rootStore}>{children}</StoreContext.Provider> | ||
24 | ); | ||
25 | } | ||
26 | |||
27 | RootStoreProvider.defaultProps = { | ||
28 | children: undefined, | ||
29 | }; | ||
diff --git a/subprojects/frontend/src/ToggleDarkModeButton.tsx b/subprojects/frontend/src/ToggleDarkModeButton.tsx index 5602f3bc..52606242 100644 --- a/subprojects/frontend/src/ToggleDarkModeButton.tsx +++ b/subprojects/frontend/src/ToggleDarkModeButton.tsx | |||
@@ -4,7 +4,7 @@ import IconButton from '@mui/material/IconButton'; | |||
4 | import { observer } from 'mobx-react-lite'; | 4 | import { observer } from 'mobx-react-lite'; |
5 | import React from 'react'; | 5 | import React from 'react'; |
6 | 6 | ||
7 | import { useRootStore } from './RootStore'; | 7 | import { useRootStore } from './RootStoreProvider'; |
8 | 8 | ||
9 | export default observer(function ToggleDarkModeButton(): JSX.Element { | 9 | export default observer(function ToggleDarkModeButton(): JSX.Element { |
10 | const { themeStore } = useRootStore(); | 10 | const { themeStore } = useRootStore(); |
diff --git a/subprojects/frontend/src/TopBar.tsx b/subprojects/frontend/src/TopBar.tsx index a060ff9e..79ea0b65 100644 --- a/subprojects/frontend/src/TopBar.tsx +++ b/subprojects/frontend/src/TopBar.tsx | |||
@@ -7,7 +7,7 @@ import { throttle } from 'lodash-es'; | |||
7 | import { observer } from 'mobx-react-lite'; | 7 | import { observer } from 'mobx-react-lite'; |
8 | import React, { useEffect, useMemo, useState } from 'react'; | 8 | import React, { useEffect, useMemo, useState } from 'react'; |
9 | 9 | ||
10 | import { useRootStore } from './RootStore'; | 10 | import { useRootStore } from './RootStoreProvider'; |
11 | import ToggleDarkModeButton from './ToggleDarkModeButton'; | 11 | import ToggleDarkModeButton from './ToggleDarkModeButton'; |
12 | import GenerateButton from './editor/GenerateButton'; | 12 | import GenerateButton from './editor/GenerateButton'; |
13 | 13 | ||
diff --git a/subprojects/frontend/src/UpdateNotification.tsx b/subprojects/frontend/src/UpdateNotification.tsx index d260e3b7..8c46186a 100644 --- a/subprojects/frontend/src/UpdateNotification.tsx +++ b/subprojects/frontend/src/UpdateNotification.tsx | |||
@@ -2,7 +2,7 @@ import Button from '@mui/material/Button'; | |||
2 | import { observer } from 'mobx-react-lite'; | 2 | import { observer } from 'mobx-react-lite'; |
3 | import React, { useEffect } from 'react'; | 3 | import React, { useEffect } from 'react'; |
4 | 4 | ||
5 | import { useRootStore } from './RootStore'; | 5 | import { useRootStore } from './RootStoreProvider'; |
6 | import { ContrastThemeProvider } from './theme/ThemeProvider'; | 6 | import { ContrastThemeProvider } from './theme/ThemeProvider'; |
7 | import useDelayedSnackbar from './utils/useDelayedSnackbar'; | 7 | import useDelayedSnackbar from './utils/useDelayedSnackbar'; |
8 | 8 | ||
diff --git a/subprojects/frontend/src/editor/EditorPane.tsx b/subprojects/frontend/src/editor/EditorPane.tsx index 199a8e1d..0db4b892 100644 --- a/subprojects/frontend/src/editor/EditorPane.tsx +++ b/subprojects/frontend/src/editor/EditorPane.tsx | |||
@@ -7,7 +7,7 @@ import useMediaQuery from '@mui/material/useMediaQuery'; | |||
7 | import { observer } from 'mobx-react-lite'; | 7 | import { observer } from 'mobx-react-lite'; |
8 | import React, { useState } from 'react'; | 8 | import React, { useState } from 'react'; |
9 | 9 | ||
10 | import { useRootStore } from '../RootStore'; | 10 | import { useRootStore } from '../RootStoreProvider'; |
11 | 11 | ||
12 | import ConnectionStatusNotification from './ConnectionStatusNotification'; | 12 | import ConnectionStatusNotification from './ConnectionStatusNotification'; |
13 | import EditorArea from './EditorArea'; | 13 | import EditorArea from './EditorArea'; |
diff --git a/subprojects/frontend/src/editor/EditorStore.ts b/subprojects/frontend/src/editor/EditorStore.ts index d13f644c..4ee24779 100644 --- a/subprojects/frontend/src/editor/EditorStore.ts +++ b/subprojects/frontend/src/editor/EditorStore.ts | |||
@@ -263,4 +263,8 @@ export default class EditorStore { | |||
263 | this.client.formatText(); | 263 | this.client.formatText(); |
264 | return true; | 264 | return true; |
265 | } | 265 | } |
266 | |||
267 | dispose(): void { | ||
268 | this.client.dispose(); | ||
269 | } | ||
266 | } | 270 | } |
diff --git a/subprojects/frontend/src/index.tsx b/subprojects/frontend/src/index.tsx index f7fe61b9..2c0259bf 100644 --- a/subprojects/frontend/src/index.tsx +++ b/subprojects/frontend/src/index.tsx | |||
@@ -1,16 +1,9 @@ | |||
1 | import Box from '@mui/material/Box'; | ||
2 | import CssBaseline from '@mui/material/CssBaseline'; | ||
3 | import { configure } from 'mobx'; | 1 | import { configure } from 'mobx'; |
4 | import React, { Suspense, lazy } from 'react'; | 2 | import React from 'react'; |
5 | import { createRoot } from 'react-dom/client'; | 3 | import { type Root, createRoot } from 'react-dom/client'; |
6 | 4 | ||
7 | import Loading from './Loading'; | 5 | import App from './App'; |
8 | import RootStore, { RootStoreProvider } from './RootStore'; | 6 | import RootStore from './RootStore'; |
9 | import WindowControlsOverlayColor from './WindowControlsOverlayColor'; | ||
10 | import ThemeProvider from './theme/ThemeProvider'; | ||
11 | import getLogger from './utils/getLogger'; | ||
12 | |||
13 | const log = getLogger('index'); | ||
14 | 7 | ||
15 | const initialValue = `class Family { | 8 | const initialValue = `class Family { |
16 | contains Person[] members | 9 | contains Person[] members |
@@ -57,30 +50,45 @@ configure({ | |||
57 | enforceActions: 'always', | 50 | enforceActions: 'always', |
58 | }); | 51 | }); |
59 | 52 | ||
60 | const rootStore = new RootStore(initialValue); | 53 | let HotRootStore = RootStore; |
54 | let HotApp = App; | ||
55 | |||
56 | function createStore(): RootStore { | ||
57 | return new HotRootStore(initialValue); | ||
58 | } | ||
61 | 59 | ||
62 | const App = lazy(() => import('./App.js')); | 60 | let rootStore = createStore(); |
63 | 61 | ||
64 | const app = ( | 62 | let root: Root | undefined; |
65 | <React.StrictMode> | ||
66 | <RootStoreProvider rootStore={rootStore}> | ||
67 | <ThemeProvider> | ||
68 | <CssBaseline enableColorScheme /> | ||
69 | <WindowControlsOverlayColor /> | ||
70 | <Box height="100vh" overflow="auto"> | ||
71 | <Suspense fallback={<Loading />}> | ||
72 | <App /> | ||
73 | </Suspense> | ||
74 | </Box> | ||
75 | </ThemeProvider> | ||
76 | </RootStoreProvider> | ||
77 | </React.StrictMode> | ||
78 | ); | ||
79 | 63 | ||
80 | const rootElement = document.getElementById('app'); | 64 | function render(): void { |
81 | if (rootElement === null) { | 65 | root?.render(<HotApp rootStore={rootStore} />); |
82 | log.error('Root element not found'); | ||
83 | } else { | ||
84 | const root = createRoot(rootElement); | ||
85 | root.render(app); | ||
86 | } | 66 | } |
67 | |||
68 | if (import.meta.hot) { | ||
69 | import.meta.hot.accept('./App', (module) => { | ||
70 | if (module === undefined) { | ||
71 | return; | ||
72 | } | ||
73 | ({ default: HotApp } = module as unknown as typeof import('./App')); | ||
74 | render(); | ||
75 | }); | ||
76 | import.meta.hot.accept('./RootStore', (module) => { | ||
77 | if (module === undefined) { | ||
78 | return; | ||
79 | } | ||
80 | ({ default: HotRootStore } = | ||
81 | module as unknown as typeof import('./RootStore')); | ||
82 | rootStore.dispose(); | ||
83 | rootStore = createStore(); | ||
84 | render(); | ||
85 | }); | ||
86 | } | ||
87 | |||
88 | document.addEventListener('DOMContentLoaded', () => { | ||
89 | const rootElement = document.getElementById('app'); | ||
90 | if (rootElement !== null) { | ||
91 | root = createRoot(rootElement); | ||
92 | render(); | ||
93 | } | ||
94 | }); | ||
diff --git a/subprojects/frontend/src/theme/ThemeProvider.tsx b/subprojects/frontend/src/theme/ThemeProvider.tsx index d990fd5d..060a5e9a 100644 --- a/subprojects/frontend/src/theme/ThemeProvider.tsx +++ b/subprojects/frontend/src/theme/ThemeProvider.tsx | |||
@@ -14,7 +14,7 @@ import { | |||
14 | import { observer } from 'mobx-react-lite'; | 14 | import { observer } from 'mobx-react-lite'; |
15 | import React, { type ReactNode, createContext, useContext } from 'react'; | 15 | import React, { type ReactNode, createContext, useContext } from 'react'; |
16 | 16 | ||
17 | import { useRootStore } from '../RootStore'; | 17 | import { useRootStore } from '../RootStoreProvider'; |
18 | 18 | ||
19 | interface OuterPalette { | 19 | interface OuterPalette { |
20 | background: string; | 20 | background: string; |
diff --git a/subprojects/frontend/src/xtext/XtextClient.ts b/subprojects/frontend/src/xtext/XtextClient.ts index cd5d280d..e7d26ae6 100644 --- a/subprojects/frontend/src/xtext/XtextClient.ts +++ b/subprojects/frontend/src/xtext/XtextClient.ts | |||
@@ -120,4 +120,8 @@ export default class XtextClient { | |||
120 | log.error('Error while formatting text', e); | 120 | log.error('Error while formatting text', e); |
121 | }); | 121 | }); |
122 | } | 122 | } |
123 | |||
124 | dispose(): void { | ||
125 | this.webSocketClient.disconnect(); | ||
126 | } | ||
123 | } | 127 | } |