@quark-fw/plugin-react 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@quark-fw/plugin-react",
3
+ "version": "0.1.2",
4
+ "main": "src/index.ts",
5
+ "types": "src/index.ts",
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "tsx watch src/index.ts",
9
+ "build": "echo raw-src package, no build"
10
+ },
11
+ "files": [
12
+ "src"
13
+ ],
14
+ "keywords": [
15
+ "quark"
16
+ ],
17
+ "author": "",
18
+ "license": "ISC",
19
+ "description": "React SSR frontend plugin for the Quark framework (published as raw TS sources; consumers run tsx + vite)",
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "dependencies": {
24
+ "@mantine/core": "^8.3.18",
25
+ "@quark-fw/atom": "0.1.2",
26
+ "@quark-fw/plugin": "0.1.2",
27
+ "@quark-fw/store": "0.1.2",
28
+ "@trpc/client": "^11.0.0",
29
+ "@mantine/dates": "^8.3.18",
30
+ "@mantine/modals": "^8.3.18",
31
+ "@mantine/notifications": "^8.3.18",
32
+ "@types/react-dom": "^19.2.3",
33
+ "@vitejs/plugin-react": "^5.1.4",
34
+ "express": "^5.2.1",
35
+ "react": "^19.2.4",
36
+ "react-dom": "^19.2.4",
37
+ "react-router-dom": "^7.13.2",
38
+ "vite": "^7.3.1",
39
+ "vite-css-modules": "^1.13.0",
40
+ "vite-plugin-svgr": "^4.5.0",
41
+ "vite-tsconfig-paths": "^6.1.1"
42
+ },
43
+ "peerDependencies": {
44
+ "@quark-fw/core": "0.1.2"
45
+ },
46
+ "devDependencies": {
47
+ "@types/express": "^5.0.6",
48
+ "@types/react": "^19.2.14"
49
+ }
50
+ }
@@ -0,0 +1,18 @@
1
+ type ErrorPageProps = {
2
+ code?: number;
3
+ description?: string;
4
+ };
5
+ function ErrorPage({ code = 404, description }: ErrorPageProps) {
6
+ return (
7
+ <div>
8
+ <div>
9
+ <h1>{code}</h1>
10
+ </div>
11
+ <div>
12
+ <span>{description}</span>
13
+ </div>
14
+ </div>
15
+ );
16
+ }
17
+
18
+ export default ErrorPage;
@@ -0,0 +1,178 @@
1
+ import {
2
+ memo,
3
+ ReactNode,
4
+ startTransition,
5
+ Suspense,
6
+ useEffect,
7
+ useMemo,
8
+ useState,
9
+ type ComponentType,
10
+ } from "react";
11
+ import { matchPath, useLocation } from "react-router-dom";
12
+ import ErrorPage from "./ErrorPage.js";
13
+ import { getRenderPath } from "./getRenderPath.js";
14
+ import { ErrorHandler } from "../frontend/ErrorHandler.js";
15
+ import { UglyModule, ViteModulesEager } from "./gatherPages.js";
16
+ import { Loader } from "@mantine/core";
17
+
18
+ export type UglyPageProps = {
19
+ pathname: string;
20
+ params: Record<string, string>;
21
+ breadCrumbs: ReactNode | null;
22
+ };
23
+
24
+ export type UglyLayoutProps = UglyPageProps & {
25
+ children: ReactNode;
26
+ };
27
+
28
+ function PageComponentRenderer({
29
+ children,
30
+ Component,
31
+ ...restProps
32
+ }: {
33
+ children?: ReactNode;
34
+ Component: ComponentType<Record<string, unknown>>;
35
+ // остальное — пропсы страницы, их состав задаёт приложение
36
+ [key: string]: unknown;
37
+ }) {
38
+ return <Component {...restProps}>{children}</Component>;
39
+ }
40
+
41
+ const MemoPageComponentRenderer = memo(
42
+ PageComponentRenderer,
43
+ (prevProps, nextProps) =>
44
+ prevProps.pathname === nextProps.pathname &&
45
+ JSON.stringify(prevProps.params) === JSON.stringify(nextProps.params),
46
+ );
47
+
48
+ function UglyModuleRenderer({
49
+ viteModules,
50
+ renderPath,
51
+ depth,
52
+ pathname,
53
+ children,
54
+ }: {
55
+ renderPath: UglyModule[];
56
+ depth: number;
57
+ pathname: string;
58
+ viteModules?: ViteModulesEager;
59
+ children: ReactNode;
60
+ }) {
61
+ const uglyModule = renderPath[depth]!;
62
+
63
+ if (!uglyModule) return children;
64
+ const PageComponent = uglyModule?.getWrappedPromise()?.default;
65
+
66
+ if (!PageComponent) return children;
67
+
68
+ const routeProps = matchPath(
69
+ {
70
+ path: uglyModule!.urlPath
71
+ .replace("layoutreset/", "")
72
+ .replace("layout/", ""),
73
+ end: false,
74
+ caseSensitive: false,
75
+ },
76
+ pathname,
77
+ );
78
+
79
+ return (
80
+ <ErrorHandler>
81
+ <Suspense fallback={<Loader />}>
82
+ <MemoPageComponentRenderer
83
+ Component={PageComponent}
84
+ pathname={pathname}
85
+ params={routeProps?.params}
86
+ >
87
+ <ErrorHandler>
88
+ <Suspense fallback={<Loader />}>
89
+ <MemoUglyModuleRenderer
90
+ pathname={pathname}
91
+ viteModules={viteModules}
92
+ renderPath={renderPath}
93
+ depth={depth + 1}
94
+ >
95
+ <ErrorHandler>{children}</ErrorHandler>
96
+ </MemoUglyModuleRenderer>
97
+ </Suspense>
98
+ </ErrorHandler>
99
+ </MemoPageComponentRenderer>
100
+ </Suspense>
101
+ </ErrorHandler>
102
+ );
103
+ }
104
+
105
+ const MemoUglyModuleRenderer = memo(
106
+ UglyModuleRenderer,
107
+ (prevProps, nextProps) => prevProps.pathname === nextProps.pathname,
108
+ );
109
+
110
+ function PageNotFound() {
111
+ const Component = ErrorPage;
112
+ return <Component code={404} />;
113
+ }
114
+
115
+ function UglyRouter({
116
+ viteModules,
117
+ registerModule,
118
+ }: {
119
+ viteModules?: ViteModulesEager;
120
+ registerModule: (module: UglyModule) => void;
121
+ }) {
122
+ const { pathname } = useLocation();
123
+
124
+ const [newRenderPath, newRenderPathKey] = useMemo(() => {
125
+ const { path, hasPage: _hasPage } = getRenderPath(pathname);
126
+ path.forEach(registerModule);
127
+ return [
128
+ path,
129
+ `${pathname};${path.map((p) => p.urlPath).join(";")}`,
130
+ _hasPage,
131
+ ];
132
+ // registerModule приходит пропсом и пересоздаётся на каждый рендер
133
+ // родителя: с ним путь пересобирался бы вхолостую на каждый рендер
134
+ // eslint-disable-next-line react-hooks/exhaustive-deps
135
+ }, [pathname]);
136
+
137
+ const [renderPath, setRenderPath] = useState(newRenderPath);
138
+ const [currentPathname, setCurrentPathname] = useState(pathname);
139
+
140
+ useEffect(() => {
141
+ const loadPathModules = async () => {
142
+ if (newRenderPath !== renderPath) {
143
+ await Promise.all(
144
+ newRenderPath.map(async (uglyModule) => uglyModule.load()),
145
+ );
146
+ startTransition(() => {
147
+ setRenderPath(newRenderPath);
148
+ setCurrentPathname(pathname);
149
+ if (pathname !== currentPathname) {
150
+ window.scrollTo({ top: 0 /*, behavior: 'smooth'*/ });
151
+ }
152
+ });
153
+ }
154
+ };
155
+
156
+ void loadPathModules();
157
+ // Эффект намеренно завязан на КЛЮЧ пути, а не на сами значения:
158
+ // newRenderPath — новый массив на каждый рендер, а renderPath и
159
+ // currentPathname этот же эффект и меняет. С ними он перезапускал бы
160
+ // сам себя.
161
+ // eslint-disable-next-line react-hooks/exhaustive-deps
162
+ }, [newRenderPathKey]);
163
+
164
+ return (
165
+ <ErrorHandler>
166
+ <MemoUglyModuleRenderer
167
+ viteModules={viteModules}
168
+ renderPath={renderPath}
169
+ depth={0}
170
+ pathname={currentPathname}
171
+ >
172
+ <PageNotFound />
173
+ </MemoUglyModuleRenderer>
174
+ </ErrorHandler>
175
+ );
176
+ }
177
+
178
+ export { UglyRouter };
@@ -0,0 +1,165 @@
1
+ /// <reference types="vite/client" />
2
+ import React from "react";
3
+ import { usePromise } from "../utils/usePromise.js";
4
+
5
+ /*
6
+ * usePromise вызывается из класса-загрузчика, а не из компонента: чтение
7
+ * модуля происходит в теле рендера страницы, поэтому порядок вызовов совпадает
8
+ * с порядком рендеров. Ответственность за это на роутере, который читает
9
+ * страницу безусловно.
10
+ */
11
+ /* eslint-disable react-hooks/rules-of-hooks */
12
+
13
+ /**
14
+ * Модуль страницы. Пропсы страниц описывает роутер (UglyPageProps), но
15
+ * загрузчик их не разбирает — ему важно лишь, что это компонент.
16
+ */
17
+ export type TPageModule = {
18
+ default: React.ComponentType<Record<string, unknown>>;
19
+ };
20
+
21
+ type TUrlPath = string;
22
+ type TModulePath = string;
23
+ type TFilePath = string;
24
+
25
+ export type ViteModules = {
26
+ [key: TModulePath]: () => Promise<TPageModule>;
27
+ };
28
+
29
+ export type ViteModulesEager = {
30
+ [key: TModulePath]: TPageModule;
31
+ };
32
+
33
+ const pages: ViteModules = import.meta.glob<TPageModule>("/Pages/**/Page.*");
34
+ const layouts: ViteModules = import.meta.glob<TPageModule>(
35
+ "/Pages/**/Layout.*",
36
+ );
37
+ const resetLayouts: ViteModules = import.meta.glob<TPageModule>(
38
+ "/Pages/**/LayoutReset.*",
39
+ );
40
+
41
+ const getMeta = (modulePath: TModulePath): [TFilePath, TUrlPath] => {
42
+ const filePath = modulePath.replace(/^\/Pages/, "").replace(/(\.tsx)$/, "");
43
+ let urlPath = (
44
+ filePath.endsWith("/Page")
45
+ ? filePath.replace(/Page$/, "")
46
+ : `${filePath}/`
47
+ )
48
+ .replace(/\[\.{3}.+\]/, "*")
49
+ .replaceAll(/\[(.[^[]+)\]/g, ":$1")
50
+ .toLowerCase();
51
+
52
+ urlPath = urlPath === "/main/" ? "/" : urlPath;
53
+
54
+ return [filePath, urlPath];
55
+ };
56
+
57
+ export class UglyModule {
58
+ modulePath: string;
59
+
60
+ moduleImport: () => Promise<TPageModule>;
61
+
62
+ module?: TPageModule;
63
+
64
+ loadingPromise?: Promise<TPageModule>;
65
+
66
+ wrappedPromise?: () => TPageModule;
67
+
68
+ filePath: string;
69
+
70
+ urlPath: string;
71
+
72
+ constructor({
73
+ moduleImport,
74
+ modulePath,
75
+ module,
76
+ }: {
77
+ moduleImport: () => Promise<TPageModule>;
78
+ modulePath: TModulePath;
79
+ module?: TPageModule;
80
+ }) {
81
+ this.modulePath = modulePath;
82
+ this.moduleImport = moduleImport;
83
+ this.module = module;
84
+ // this.loadingPromise;
85
+ const [filePath, urlPath] = getMeta(modulePath);
86
+ this.filePath = filePath;
87
+ this.urlPath = urlPath;
88
+ }
89
+
90
+ getWrappedPromise(force?: TPageModule) {
91
+ if (!this.module && !force) {
92
+ if (!this.wrappedPromise) {
93
+ if (!this.loadingPromise) {
94
+ this.loadingPromise = new Promise((resolve) => {
95
+ try {
96
+ this.moduleImport().then((module) =>
97
+ resolve(module),
98
+ );
99
+ } catch (e) {
100
+ console.error(e);
101
+ }
102
+ });
103
+ }
104
+ this.wrappedPromise = usePromise(this.loadingPromise);
105
+ }
106
+
107
+ return this.wrappedPromise();
108
+ }
109
+ this.wrappedPromise = usePromise(
110
+ new Promise((resolve) => {
111
+ resolve(this.module! || force);
112
+ }),
113
+ );
114
+ return this.module;
115
+ }
116
+
117
+ async load() {
118
+ if (!this.module) {
119
+ if (!this.loadingPromise) {
120
+ this.loadingPromise = new Promise((resolve) => {
121
+ try {
122
+ this.moduleImport().then((module) => resolve(module));
123
+ } catch (e) {
124
+ console.error(e);
125
+ }
126
+ });
127
+ }
128
+ }
129
+
130
+ this.module = await this.loadingPromise;
131
+
132
+ return this.module;
133
+ }
134
+ }
135
+
136
+ export const createUglyModules = (modules: ViteModules | ViteModulesEager) =>
137
+ Object.keys(modules).reduce(
138
+ (acc, modulePath) => {
139
+ const moduleImport = modules[modulePath];
140
+ if (typeof moduleImport === "object") {
141
+ acc[modulePath] = new UglyModule({
142
+ modulePath,
143
+ module: moduleImport,
144
+ moduleImport: () =>
145
+ new Promise<TPageModule>((resolve) =>
146
+ resolve(moduleImport),
147
+ ),
148
+ });
149
+ } else {
150
+ acc[modulePath] = new UglyModule({
151
+ modulePath,
152
+ moduleImport: moduleImport!,
153
+ });
154
+ }
155
+
156
+ return acc;
157
+ },
158
+ {} as Record<TModulePath, UglyModule>,
159
+ );
160
+
161
+ const uglyPages = createUglyModules(pages);
162
+ const uglyLayouts = createUglyModules(layouts);
163
+ const uglyResetLayouts = createUglyModules(resetLayouts);
164
+
165
+ export { uglyPages, uglyLayouts, uglyResetLayouts };
@@ -0,0 +1,24 @@
1
+ import { matchPath } from "react-router-dom";
2
+ import { uglyPages, UglyModule } from "./gatherPages.js";
3
+
4
+ export function getMatchedRoute(
5
+ url: string,
6
+ uglyModules = uglyPages,
7
+ ): UglyModule | undefined {
8
+ let exactRoute;
9
+
10
+ Object.keys(uglyModules).find((modulePath) => {
11
+ const uglyPage = uglyModules[modulePath]!;
12
+
13
+ const isMatched = matchPath(uglyPage.urlPath, url);
14
+
15
+ if (isMatched) {
16
+ exactRoute = uglyPage;
17
+ return true;
18
+ }
19
+
20
+ return false;
21
+ });
22
+
23
+ return exactRoute;
24
+ }
@@ -0,0 +1,32 @@
1
+ import { UglyModule, uglyLayouts, uglyResetLayouts } from "./gatherPages.js";
2
+ import { getMatchedRoute } from "./getMatchedRoute.js";
3
+
4
+ export const getRenderPath = (pathname: string) => {
5
+ const parts = pathname.split("/");
6
+
7
+ const getLayouts = (depth: number): UglyModule[] => {
8
+ if (depth === parts.length) return [];
9
+ const currPath = parts.slice(0, parts.length - depth);
10
+ const joined = currPath.join("/");
11
+ const _layout = getMatchedRoute(`${joined}/layout`, uglyLayouts);
12
+ const _resetLayout = getMatchedRoute(
13
+ `${joined}/layoutReset`,
14
+ uglyResetLayouts,
15
+ );
16
+ if (_resetLayout) {
17
+ return [_resetLayout, _layout].filter(Boolean) as UglyModule[];
18
+ }
19
+
20
+ return [...getLayouts(depth + 1), _layout].filter(
21
+ Boolean,
22
+ ) as UglyModule[];
23
+ };
24
+
25
+ const layouts = getLayouts(0);
26
+ const page = getMatchedRoute(pathname);
27
+
28
+ return {
29
+ path: [...layouts, page].filter(Boolean) as UglyModule[],
30
+ hasPage: !!page,
31
+ };
32
+ };
package/src/build.ts ADDED
@@ -0,0 +1,64 @@
1
+ import { build } from "vite";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import {
5
+ appFrontendRoot,
6
+ buildPaths,
7
+ clientEntry,
8
+ createViteConfig,
9
+ serverEntry,
10
+ } from "./utils/viteConfig.js";
11
+
12
+ /**
13
+ * Прод-сборка приложения: клиентский бандл (с manifest) и SSR-бандл.
14
+ * Бэкенд не компилируется — он запускается тем же tsx, что и в dev
15
+ * (`NODE_ENV=production tsx src/index.ts`), а прод-ветка react-плагина
16
+ * отдаёт статику из build/client и рендерит через build/server.
17
+ */
18
+ export async function buildApp({ cwd = process.cwd() } = {}) {
19
+ const root = appFrontendRoot(cwd);
20
+ const paths = buildPaths(cwd);
21
+
22
+ fs.rmSync(paths.root, { recursive: true, force: true });
23
+
24
+ console.log("[build] client bundle…");
25
+ await build(
26
+ createViteConfig({
27
+ root,
28
+ build: {
29
+ outDir: paths.client,
30
+ emptyOutDir: true,
31
+ manifest: true,
32
+ rollupOptions: {
33
+ input: clientEntry,
34
+ },
35
+ },
36
+ }),
37
+ );
38
+
39
+ console.log("[build] ssr bundle…");
40
+ await build(
41
+ createViteConfig({
42
+ root,
43
+ build: {
44
+ outDir: paths.server,
45
+ emptyOutDir: true,
46
+ ssr: serverEntry,
47
+ rollupOptions: {
48
+ output: { entryFileNames: "entry-server.js" },
49
+ },
50
+ },
51
+ }),
52
+ );
53
+
54
+ // статика приложения (favicon и прочее), если есть
55
+ const staticDir = path.join(cwd, "static");
56
+ if (fs.existsSync(staticDir)) {
57
+ fs.cpSync(staticDir, path.join(paths.root, "static"), {
58
+ recursive: true,
59
+ });
60
+ }
61
+
62
+ console.log(`[build] готово → ${paths.root}`);
63
+ return paths;
64
+ }
@@ -0,0 +1,99 @@
1
+ import "vite/modulepreload-polyfill";
2
+ // ВСЕ стили Mantine — только layered-варианты. Смешение layered ядра с
3
+ // не-layered пакетами меняет приоритеты правил, и в прод-сборке (где CSS
4
+ // склеивается в другом порядке) filled-кнопки теряли фон.
5
+ import "@mantine/core/styles.layer.css";
6
+ import "@mantine/notifications/styles.layer.css";
7
+ import "@mantine/dates/styles.layer.css";
8
+ import { ModalsProvider } from "@mantine/modals";
9
+
10
+ import { Notifications } from "@mantine/notifications";
11
+ import { MantineProvider } from "@mantine/core";
12
+
13
+ import { UglyModule, ViteModulesEager } from "../Router/gatherPages.js";
14
+ import { ErrorHandler } from "./ErrorHandler.js";
15
+ import { UglyRouter } from "../Router/Router.js";
16
+ import Store, {
17
+ StoreContext,
18
+ TRPCContext,
19
+ type TRPCClient,
20
+ } from "@quark-fw/store";
21
+ // регистрирует показ ошибок через notifications (side-effect импорт)
22
+ import "./displayError.js";
23
+ import { SeoProvider, type SeoMap } from "../seo/SeoContext.js";
24
+ import { I18nProvider, type EagerLocaleModules } from "../i18n/I18nContext.js";
25
+ import type { I18nConfig } from "../i18n/config.js";
26
+ // @ts-expect-error virtual file
27
+ import { appTheme } from "/theme.ts";
28
+
29
+ interface AppProps {
30
+ viteModules?: ViteModulesEager;
31
+ registerModule: (module: UglyModule) => void;
32
+ colorScheme: "light" | "dark" | undefined;
33
+ store: Store;
34
+ trpcClient: TRPCClient;
35
+ seoObject?: SeoMap;
36
+ locale?: string;
37
+ eagerLocaleModules?: EagerLocaleModules;
38
+ }
39
+
40
+ // i18n опционален: без /i18n.ts в приложении провайдер не рендерится,
41
+ // а useI18n() отдаёт дефолт, в котором $(key) возвращает сам ключ
42
+ const i18nModules: Record<string, { i18nConfig?: I18nConfig }> =
43
+ import.meta.glob("/i18n.ts", { eager: true });
44
+ const i18nConfig = Object.values(i18nModules)[0]?.i18nConfig;
45
+
46
+ export default function App({
47
+ viteModules,
48
+ registerModule,
49
+ colorScheme: defaultColorScheme,
50
+ store,
51
+ trpcClient,
52
+ seoObject,
53
+ locale,
54
+ eagerLocaleModules,
55
+ }: AppProps) {
56
+ const tree = (
57
+ <>
58
+ <Notifications position="top-right" />
59
+
60
+ <ErrorHandler>
61
+ <UglyRouter
62
+ registerModule={registerModule}
63
+ viteModules={viteModules}
64
+ />
65
+ </ErrorHandler>
66
+ </>
67
+ );
68
+ return (
69
+ <ErrorHandler>
70
+ <MantineProvider
71
+ defaultColorScheme={defaultColorScheme || "auto"}
72
+ withGlobalClasses={false}
73
+ theme={appTheme}
74
+ >
75
+ <ModalsProvider>
76
+ <TRPCContext value={trpcClient}>
77
+ <StoreContext value={store}>
78
+ <SeoProvider seoObject={seoObject}>
79
+ {i18nConfig ? (
80
+ <I18nProvider
81
+ locale={
82
+ locale || i18nConfig.defaultLocale
83
+ }
84
+ config={i18nConfig}
85
+ eagerLocaleModules={eagerLocaleModules}
86
+ >
87
+ {tree}
88
+ </I18nProvider>
89
+ ) : (
90
+ tree
91
+ )}
92
+ </SeoProvider>
93
+ </StoreContext>
94
+ </TRPCContext>
95
+ </ModalsProvider>
96
+ </MantineProvider>
97
+ </ErrorHandler>
98
+ );
99
+ }
@@ -0,0 +1,58 @@
1
+ import { Component, type ErrorInfo, type ReactNode } from "react";
2
+
3
+ type ErrorHandlerProps = {
4
+ children?: ReactNode;
5
+ /** подпись, по которой видно, какой участок дерева упал */
6
+ label?: string;
7
+ };
8
+
9
+ type ErrorHandlerState = {
10
+ hasError: boolean;
11
+ error: unknown;
12
+ };
13
+
14
+ class ErrorHandler extends Component<ErrorHandlerProps, ErrorHandlerState> {
15
+ constructor(props: ErrorHandlerProps) {
16
+ super(props);
17
+ this.state = { hasError: false, error: "" };
18
+ }
19
+
20
+ static getDerivedStateFromError(error: unknown) {
21
+ return { hasError: true, error };
22
+ }
23
+
24
+ componentDidCatch(error: unknown, errorInfo: ErrorInfo) {
25
+ console.error(error, errorInfo);
26
+ }
27
+
28
+ /**
29
+ * Сброс ошибки при смене поддерева.
30
+ *
31
+ * Раньше первый аргумент назывался prevState, хотя React передаёт сюда
32
+ * prevProps — из-за этого условие читало поле, которого у пропсов нет, и
33
+ * граница не сбрасывалась никогда. Сбрасываем именно на смену children:
34
+ * сброс на каждый рендер зациклил бы падающего потомка (ошибка → сброс →
35
+ * ошибка).
36
+ */
37
+ componentDidUpdate(prevProps: ErrorHandlerProps) {
38
+ if (this.state.hasError && prevProps.children !== this.props.children) {
39
+ this.setState({ hasError: false, error: "" });
40
+ }
41
+ }
42
+
43
+ render() {
44
+ if (this.state.hasError) {
45
+ return (
46
+ <div>
47
+ {this.props.label ? <code>{this.props.label}</code> : null}
48
+ Error :(
49
+ <pre>{String(this.state.error)}</pre>
50
+ </div>
51
+ );
52
+ }
53
+
54
+ return this.props.children;
55
+ }
56
+ }
57
+
58
+ export { ErrorHandler };