aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects/frontend/src
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2022-10-07 19:44:41 +0200
committerLibravatar Kristóf Marussy <kristof@marussy.com>2022-11-05 19:41:14 +0100
commitabe176d0888c0fdcc803ddafe71a5cee9f4b63a0 (patch)
treeea7d5722c8e981ba540e648fbacbfd0e4a55768a /subprojects/frontend/src
parentfix: test and lint failures (diff)
downloadrefinery-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.tsx44
-rw-r--r--subprojects/frontend/src/Refinery.tsx21
-rw-r--r--subprojects/frontend/src/RootStore.ts (renamed from subprojects/frontend/src/RootStore.tsx)37
-rw-r--r--subprojects/frontend/src/RootStoreProvider.tsx29
-rw-r--r--subprojects/frontend/src/ToggleDarkModeButton.tsx2
-rw-r--r--subprojects/frontend/src/TopBar.tsx2
-rw-r--r--subprojects/frontend/src/UpdateNotification.tsx2
-rw-r--r--subprojects/frontend/src/editor/EditorPane.tsx2
-rw-r--r--subprojects/frontend/src/editor/EditorStore.ts4
-rw-r--r--subprojects/frontend/src/index.tsx76
-rw-r--r--subprojects/frontend/src/theme/ThemeProvider.tsx2
-rw-r--r--subprojects/frontend/src/xtext/XtextClient.ts4
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 @@
1import Grow from '@mui/material/Grow'; 1import Box from '@mui/material/Box';
2import Stack from '@mui/material/Stack'; 2import CssBaseline from '@mui/material/CssBaseline';
3import { SnackbarProvider } from 'notistack'; 3import React, { StrictMode, Suspense, lazy } from 'react';
4import React from 'react';
5 4
6import TopBar from './TopBar'; 5import Loading from './Loading';
7import UpdateNotification from './UpdateNotification'; 6import type RootStore from './RootStore';
8import EditorPane from './editor/EditorPane'; 7import RootStoreProvider from './RootStoreProvider';
8import WindowControlsOverlayColor from './WindowControlsOverlayColor';
9import ThemeProvider from './theme/ThemeProvider';
9 10
10export default function App(): JSX.Element { 11const Refinery = lazy(() => import('./Refinery.js'));
12
13export 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 @@
1import Grow from '@mui/material/Grow';
2import Stack from '@mui/material/Stack';
3import { SnackbarProvider } from 'notistack';
4import React from 'react';
5
6import TopBar from './TopBar';
7import UpdateNotification from './UpdateNotification';
8import EditorPane from './editor/EditorPane';
9
10export 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 @@
1import { getLogger } from 'loglevel'; 1import { getLogger } from 'loglevel';
2import { makeAutoObservable, runInAction } from 'mobx'; 2import { makeAutoObservable, runInAction } from 'mobx';
3import React, { createContext, useContext } from 'react';
4 3
5import PWAStore from './PWAStore'; 4import PWAStore from './PWAStore';
6import type EditorStore from './editor/EditorStore'; 5import 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
37const StoreContext = createContext<RootStore | undefined>(undefined);
38 39
39export 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;
45export function RootStoreProvider({
46 children,
47 rootStore,
48}: RootStoreProviderProps): JSX.Element {
49 return (
50 <StoreContext.Provider value={rootStore}>{children}</StoreContext.Provider>
51 );
52}
53
54export 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 @@
1import React, { type ReactNode, createContext, useContext } from 'react';
2
3import type RootStore from './RootStore';
4
5const StoreContext = createContext<RootStore | undefined>(undefined);
6
7export 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
15export 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
27RootStoreProvider.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';
4import { observer } from 'mobx-react-lite'; 4import { observer } from 'mobx-react-lite';
5import React from 'react'; 5import React from 'react';
6 6
7import { useRootStore } from './RootStore'; 7import { useRootStore } from './RootStoreProvider';
8 8
9export default observer(function ToggleDarkModeButton(): JSX.Element { 9export 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';
7import { observer } from 'mobx-react-lite'; 7import { observer } from 'mobx-react-lite';
8import React, { useEffect, useMemo, useState } from 'react'; 8import React, { useEffect, useMemo, useState } from 'react';
9 9
10import { useRootStore } from './RootStore'; 10import { useRootStore } from './RootStoreProvider';
11import ToggleDarkModeButton from './ToggleDarkModeButton'; 11import ToggleDarkModeButton from './ToggleDarkModeButton';
12import GenerateButton from './editor/GenerateButton'; 12import 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';
2import { observer } from 'mobx-react-lite'; 2import { observer } from 'mobx-react-lite';
3import React, { useEffect } from 'react'; 3import React, { useEffect } from 'react';
4 4
5import { useRootStore } from './RootStore'; 5import { useRootStore } from './RootStoreProvider';
6import { ContrastThemeProvider } from './theme/ThemeProvider'; 6import { ContrastThemeProvider } from './theme/ThemeProvider';
7import useDelayedSnackbar from './utils/useDelayedSnackbar'; 7import 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';
7import { observer } from 'mobx-react-lite'; 7import { observer } from 'mobx-react-lite';
8import React, { useState } from 'react'; 8import React, { useState } from 'react';
9 9
10import { useRootStore } from '../RootStore'; 10import { useRootStore } from '../RootStoreProvider';
11 11
12import ConnectionStatusNotification from './ConnectionStatusNotification'; 12import ConnectionStatusNotification from './ConnectionStatusNotification';
13import EditorArea from './EditorArea'; 13import 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 @@
1import Box from '@mui/material/Box';
2import CssBaseline from '@mui/material/CssBaseline';
3import { configure } from 'mobx'; 1import { configure } from 'mobx';
4import React, { Suspense, lazy } from 'react'; 2import React from 'react';
5import { createRoot } from 'react-dom/client'; 3import { type Root, createRoot } from 'react-dom/client';
6 4
7import Loading from './Loading'; 5import App from './App';
8import RootStore, { RootStoreProvider } from './RootStore'; 6import RootStore from './RootStore';
9import WindowControlsOverlayColor from './WindowControlsOverlayColor';
10import ThemeProvider from './theme/ThemeProvider';
11import getLogger from './utils/getLogger';
12
13const log = getLogger('index');
14 7
15const initialValue = `class Family { 8const 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
60const rootStore = new RootStore(initialValue); 53let HotRootStore = RootStore;
54let HotApp = App;
55
56function createStore(): RootStore {
57 return new HotRootStore(initialValue);
58}
61 59
62const App = lazy(() => import('./App.js')); 60let rootStore = createStore();
63 61
64const app = ( 62let 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
80const rootElement = document.getElementById('app'); 64function render(): void {
81if (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
68if (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
88document.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 {
14import { observer } from 'mobx-react-lite'; 14import { observer } from 'mobx-react-lite';
15import React, { type ReactNode, createContext, useContext } from 'react'; 15import React, { type ReactNode, createContext, useContext } from 'react';
16 16
17import { useRootStore } from '../RootStore'; 17import { useRootStore } from '../RootStoreProvider';
18 18
19interface OuterPalette { 19interface 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}