@quark-fw/plugin-react 0.1.7 → 0.1.8
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 +12 -10
- package/src/Router/PageComponentRenderer.tsx +21 -0
- package/src/Router/Router.tsx +5 -178
- package/src/Router/UglyModule.test.ts +48 -0
- package/src/Router/UglyModule.ts +98 -0
- package/src/Router/UglyModuleRenderer.tsx +69 -0
- package/src/Router/UglyRouter.tsx +77 -0
- package/src/Router/createUglyModules.ts +32 -0
- package/src/Router/gatherPages.ts +7 -149
- package/src/Router/types.ts +30 -0
- package/src/build.ts +1 -1
- package/src/frontend/App.tsx +6 -4
- package/src/frontend/entry-client.tsx +4 -2
- package/src/frontend/entry-server.tsx +1 -1
- package/src/i18n/I18nContext.tsx +1 -191
- package/src/i18n/I18nProvider.tsx +168 -0
- package/src/i18n/helpers.ts +29 -0
- package/src/seo/SeoContext.tsx +2 -78
- package/src/seo/SeoProvider.tsx +72 -0
- package/src/ssr/getRenderMiddleware.ts +7 -3
- package/src/utils/createViteConfig.ts +81 -0
- package/src/utils/vite.ts +1 -1
- package/src/utils/viteConfig.ts +2 -76
- package/src/utils/usePromise.ts +0 -24
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quark-fw/plugin-react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"main": "src/index.ts",
|
|
5
5
|
"types": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"dev": "tsx watch src/index.ts",
|
|
9
|
-
"build": "echo raw-src package, no build"
|
|
9
|
+
"build": "echo raw-src package, no build",
|
|
10
|
+
"test": "vitest run"
|
|
10
11
|
},
|
|
11
12
|
"files": [
|
|
12
13
|
"src"
|
|
@@ -21,8 +22,8 @@
|
|
|
21
22
|
"access": "public"
|
|
22
23
|
},
|
|
23
24
|
"dependencies": {
|
|
24
|
-
"@quark-fw/atom": "0.1.
|
|
25
|
-
"@quark-fw/plugin": "0.1.
|
|
25
|
+
"@quark-fw/atom": "0.1.8",
|
|
26
|
+
"@quark-fw/plugin": "0.1.8",
|
|
26
27
|
"@trpc/client": "^11.0.0",
|
|
27
28
|
"@types/react-dom": "^19.2.3",
|
|
28
29
|
"express": "^5.2.1",
|
|
@@ -31,20 +32,21 @@
|
|
|
31
32
|
"vite-tsconfig-paths": "^6.1.1"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
34
|
-
"@vitejs/plugin-react": "^5.1.0",
|
|
35
|
-
"vite": "^7.1.0",
|
|
36
35
|
"@mantine/core": "^8.3.18",
|
|
37
36
|
"@mantine/dates": "^8.3.18",
|
|
38
37
|
"@mantine/modals": "^8.3.18",
|
|
39
38
|
"@mantine/notifications": "^8.3.18",
|
|
40
|
-
"@quark-fw/core": "0.1.
|
|
41
|
-
"@quark-fw/store": "0.1.
|
|
39
|
+
"@quark-fw/core": "0.1.8",
|
|
40
|
+
"@quark-fw/store": "0.1.8",
|
|
41
|
+
"@vitejs/plugin-react": "^5.1.0",
|
|
42
42
|
"react": "^19.2.4",
|
|
43
43
|
"react-dom": "^19.2.4",
|
|
44
|
-
"react-router-dom": "^7.13.2"
|
|
44
|
+
"react-router-dom": "^7.13.2",
|
|
45
|
+
"vite": "^7.1.0"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@types/express": "^5.0.6",
|
|
48
|
-
"@types/react": "^19.2.14"
|
|
49
|
+
"@types/react": "^19.2.14",
|
|
50
|
+
"vitest": "^5.0.0"
|
|
49
51
|
}
|
|
50
52
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { memo, ReactNode, type ComponentType } from "react";
|
|
2
|
+
|
|
3
|
+
function PageComponentRenderer({
|
|
4
|
+
children,
|
|
5
|
+
Component,
|
|
6
|
+
...restProps
|
|
7
|
+
}: {
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
Component: ComponentType<Record<string, unknown>>;
|
|
10
|
+
// остальное — пропсы страницы, их состав задаёт приложение
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}) {
|
|
13
|
+
return <Component {...restProps}>{children}</Component>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const MemoPageComponentRenderer = memo(
|
|
17
|
+
PageComponentRenderer,
|
|
18
|
+
(prevProps, nextProps) =>
|
|
19
|
+
prevProps.pathname === nextProps.pathname &&
|
|
20
|
+
JSON.stringify(prevProps.params) === JSON.stringify(nextProps.params),
|
|
21
|
+
);
|
package/src/Router/Router.tsx
CHANGED
|
@@ -1,178 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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 };
|
|
1
|
+
// Публичная точка входа роутера: реализация разложена по файлам
|
|
2
|
+
// (1 функция = 1 файл), путь @quark-fw/plugin-react/src/Router/Router
|
|
3
|
+
// заморожен — на него завязаны страницы приложений (UglyPageProps).
|
|
4
|
+
export { UglyRouter } from "./UglyRouter.js";
|
|
5
|
+
export type { UglyLayoutProps, UglyPageProps } from "./types.js";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { UglyModule } from "./UglyModule.js";
|
|
3
|
+
|
|
4
|
+
const failingModule = () =>
|
|
5
|
+
new UglyModule({
|
|
6
|
+
modulePath: "/Pages/X/Page.tsx",
|
|
7
|
+
moduleImport: () => Promise.reject(new Error("chunk failed")),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe("UglyModule", () => {
|
|
11
|
+
test("упавший импорт чанка не подвешивает Suspense навсегда", async () => {
|
|
12
|
+
const uglyModule = failingModule();
|
|
13
|
+
|
|
14
|
+
let suspender: unknown;
|
|
15
|
+
try {
|
|
16
|
+
uglyModule.getWrappedPromise();
|
|
17
|
+
} catch (thrown) {
|
|
18
|
+
suspender = thrown;
|
|
19
|
+
}
|
|
20
|
+
expect(suspender).toBeInstanceOf(Promise);
|
|
21
|
+
|
|
22
|
+
// саспендер обязан завершиться — иначе React ждёт вечно
|
|
23
|
+
const settled = await Promise.race([
|
|
24
|
+
(suspender as Promise<unknown>).then(
|
|
25
|
+
() => "settled",
|
|
26
|
+
() => "settled",
|
|
27
|
+
),
|
|
28
|
+
new Promise((resolve) => setTimeout(() => resolve("timeout"), 100)),
|
|
29
|
+
]);
|
|
30
|
+
expect(settled).toBe("settled");
|
|
31
|
+
|
|
32
|
+
// повторное чтение бросает саму ошибку — её покажет ErrorBoundary
|
|
33
|
+
expect(() => uglyModule.getWrappedPromise()).toThrow("chunk failed");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("load() отклоняется вместо вечного ожидания", async () => {
|
|
37
|
+
const uglyModule = failingModule();
|
|
38
|
+
|
|
39
|
+
const result = await Promise.race([
|
|
40
|
+
uglyModule.load().then(
|
|
41
|
+
() => "resolved",
|
|
42
|
+
() => "rejected",
|
|
43
|
+
),
|
|
44
|
+
new Promise((resolve) => setTimeout(() => resolve("timeout"), 100)),
|
|
45
|
+
]);
|
|
46
|
+
expect(result).toBe("rejected");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { usePromise } from "@quark-fw/store/src/usePromise.js";
|
|
2
|
+
import type { TModulePath, TPageModule } from "./types.js";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* usePromise вызывается из класса-загрузчика, а не из компонента: чтение
|
|
6
|
+
* модуля происходит в теле рендера страницы, поэтому порядок вызовов совпадает
|
|
7
|
+
* с порядком рендеров. Ответственность за это на роутере, который читает
|
|
8
|
+
* страницу безусловно.
|
|
9
|
+
*/
|
|
10
|
+
/* eslint-disable react-hooks/rules-of-hooks */
|
|
11
|
+
|
|
12
|
+
type TUrlPath = string;
|
|
13
|
+
type TFilePath = string;
|
|
14
|
+
|
|
15
|
+
const getMeta = (modulePath: TModulePath): [TFilePath, TUrlPath] => {
|
|
16
|
+
const filePath = modulePath.replace(/^\/Pages/, "").replace(/(\.tsx)$/, "");
|
|
17
|
+
let urlPath = (
|
|
18
|
+
filePath.endsWith("/Page")
|
|
19
|
+
? filePath.replace(/Page$/, "")
|
|
20
|
+
: `${filePath}/`
|
|
21
|
+
)
|
|
22
|
+
.replace(/\[\.{3}.+\]/, "*")
|
|
23
|
+
.replaceAll(/\[(.[^[]+)\]/g, ":$1")
|
|
24
|
+
.toLowerCase();
|
|
25
|
+
|
|
26
|
+
urlPath = urlPath === "/main/" ? "/" : urlPath;
|
|
27
|
+
|
|
28
|
+
return [filePath, urlPath];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export class UglyModule {
|
|
32
|
+
modulePath: string;
|
|
33
|
+
|
|
34
|
+
moduleImport: () => Promise<TPageModule>;
|
|
35
|
+
|
|
36
|
+
module?: TPageModule;
|
|
37
|
+
|
|
38
|
+
loadingPromise?: Promise<TPageModule>;
|
|
39
|
+
|
|
40
|
+
wrappedPromise?: () => TPageModule;
|
|
41
|
+
|
|
42
|
+
filePath: string;
|
|
43
|
+
|
|
44
|
+
urlPath: string;
|
|
45
|
+
|
|
46
|
+
constructor({
|
|
47
|
+
moduleImport,
|
|
48
|
+
modulePath,
|
|
49
|
+
module,
|
|
50
|
+
}: {
|
|
51
|
+
moduleImport: () => Promise<TPageModule>;
|
|
52
|
+
modulePath: TModulePath;
|
|
53
|
+
module?: TPageModule;
|
|
54
|
+
}) {
|
|
55
|
+
this.modulePath = modulePath;
|
|
56
|
+
this.moduleImport = moduleImport;
|
|
57
|
+
this.module = module;
|
|
58
|
+
// this.loadingPromise;
|
|
59
|
+
const [filePath, urlPath] = getMeta(modulePath);
|
|
60
|
+
this.filePath = filePath;
|
|
61
|
+
this.urlPath = urlPath;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
getWrappedPromise(force?: TPageModule) {
|
|
65
|
+
if (!this.module && !force) {
|
|
66
|
+
if (!this.wrappedPromise) {
|
|
67
|
+
if (!this.loadingPromise) {
|
|
68
|
+
// rejection НЕ глотаем: usePromise донесёт ошибку до
|
|
69
|
+
// ErrorBoundary — иначе Suspense висел бы вечно
|
|
70
|
+
this.loadingPromise = this.moduleImport();
|
|
71
|
+
}
|
|
72
|
+
this.wrappedPromise = usePromise(this.loadingPromise);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return this.wrappedPromise();
|
|
76
|
+
}
|
|
77
|
+
this.wrappedPromise = usePromise(
|
|
78
|
+
new Promise((resolve) => {
|
|
79
|
+
resolve(this.module! || force);
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
return this.module;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async load() {
|
|
86
|
+
if (!this.module) {
|
|
87
|
+
if (!this.loadingPromise) {
|
|
88
|
+
// ошибка импорта пробрасывается вызывающему — он решает,
|
|
89
|
+
// логировать или показать (раньше промис висел вечно)
|
|
90
|
+
this.loadingPromise = this.moduleImport();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.module = await this.loadingPromise;
|
|
95
|
+
|
|
96
|
+
return this.module;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { memo, ReactNode, Suspense } from "react";
|
|
2
|
+
import { matchPath } from "react-router-dom";
|
|
3
|
+
import { Loader } from "@mantine/core";
|
|
4
|
+
import { ErrorHandler } from "../frontend/ErrorHandler.js";
|
|
5
|
+
import { MemoPageComponentRenderer } from "./PageComponentRenderer.js";
|
|
6
|
+
import { UglyModule } from "./UglyModule.js";
|
|
7
|
+
import type { ViteModulesEager } from "./types.js";
|
|
8
|
+
|
|
9
|
+
function UglyModuleRenderer({
|
|
10
|
+
viteModules,
|
|
11
|
+
renderPath,
|
|
12
|
+
depth,
|
|
13
|
+
pathname,
|
|
14
|
+
children,
|
|
15
|
+
}: {
|
|
16
|
+
renderPath: UglyModule[];
|
|
17
|
+
depth: number;
|
|
18
|
+
pathname: string;
|
|
19
|
+
viteModules?: ViteModulesEager;
|
|
20
|
+
children: ReactNode;
|
|
21
|
+
}) {
|
|
22
|
+
const uglyModule = renderPath[depth]!;
|
|
23
|
+
|
|
24
|
+
if (!uglyModule) return children;
|
|
25
|
+
const PageComponent = uglyModule?.getWrappedPromise()?.default;
|
|
26
|
+
|
|
27
|
+
if (!PageComponent) return children;
|
|
28
|
+
|
|
29
|
+
const routeProps = matchPath(
|
|
30
|
+
{
|
|
31
|
+
path: uglyModule!.urlPath
|
|
32
|
+
.replace("layoutreset/", "")
|
|
33
|
+
.replace("layout/", ""),
|
|
34
|
+
end: false,
|
|
35
|
+
caseSensitive: false,
|
|
36
|
+
},
|
|
37
|
+
pathname,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<ErrorHandler>
|
|
42
|
+
<Suspense fallback={<Loader />}>
|
|
43
|
+
<MemoPageComponentRenderer
|
|
44
|
+
Component={PageComponent}
|
|
45
|
+
pathname={pathname}
|
|
46
|
+
params={routeProps?.params}
|
|
47
|
+
>
|
|
48
|
+
<ErrorHandler>
|
|
49
|
+
<Suspense fallback={<Loader />}>
|
|
50
|
+
<MemoUglyModuleRenderer
|
|
51
|
+
pathname={pathname}
|
|
52
|
+
viteModules={viteModules}
|
|
53
|
+
renderPath={renderPath}
|
|
54
|
+
depth={depth + 1}
|
|
55
|
+
>
|
|
56
|
+
<ErrorHandler>{children}</ErrorHandler>
|
|
57
|
+
</MemoUglyModuleRenderer>
|
|
58
|
+
</Suspense>
|
|
59
|
+
</ErrorHandler>
|
|
60
|
+
</MemoPageComponentRenderer>
|
|
61
|
+
</Suspense>
|
|
62
|
+
</ErrorHandler>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const MemoUglyModuleRenderer = memo(
|
|
67
|
+
UglyModuleRenderer,
|
|
68
|
+
(prevProps, nextProps) => prevProps.pathname === nextProps.pathname,
|
|
69
|
+
);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { startTransition, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { useLocation } from "react-router-dom";
|
|
3
|
+
import ErrorPage from "./ErrorPage.js";
|
|
4
|
+
import { getRenderPath } from "./getRenderPath.js";
|
|
5
|
+
import { ErrorHandler } from "../frontend/ErrorHandler.js";
|
|
6
|
+
import { MemoUglyModuleRenderer } from "./UglyModuleRenderer.js";
|
|
7
|
+
import { UglyModule } from "./UglyModule.js";
|
|
8
|
+
import type { ViteModulesEager } from "./types.js";
|
|
9
|
+
|
|
10
|
+
function PageNotFound() {
|
|
11
|
+
const Component = ErrorPage;
|
|
12
|
+
return <Component code={404} />;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function UglyRouter({
|
|
16
|
+
viteModules,
|
|
17
|
+
registerModule,
|
|
18
|
+
}: {
|
|
19
|
+
viteModules?: ViteModulesEager;
|
|
20
|
+
registerModule: (module: UglyModule) => void;
|
|
21
|
+
}) {
|
|
22
|
+
const { pathname } = useLocation();
|
|
23
|
+
|
|
24
|
+
const [newRenderPath, newRenderPathKey] = useMemo(() => {
|
|
25
|
+
const { path, hasPage: _hasPage } = getRenderPath(pathname);
|
|
26
|
+
path.forEach(registerModule);
|
|
27
|
+
return [
|
|
28
|
+
path,
|
|
29
|
+
`${pathname};${path.map((p) => p.urlPath).join(";")}`,
|
|
30
|
+
_hasPage,
|
|
31
|
+
];
|
|
32
|
+
// registerModule приходит пропсом и пересоздаётся на каждый рендер
|
|
33
|
+
// родителя: с ним путь пересобирался бы вхолостую на каждый рендер
|
|
34
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
35
|
+
}, [pathname]);
|
|
36
|
+
|
|
37
|
+
const [renderPath, setRenderPath] = useState(newRenderPath);
|
|
38
|
+
const [currentPathname, setCurrentPathname] = useState(pathname);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const loadPathModules = async () => {
|
|
42
|
+
if (newRenderPath !== renderPath) {
|
|
43
|
+
await Promise.all(
|
|
44
|
+
newRenderPath.map(async (uglyModule) => uglyModule.load()),
|
|
45
|
+
);
|
|
46
|
+
startTransition(() => {
|
|
47
|
+
setRenderPath(newRenderPath);
|
|
48
|
+
setCurrentPathname(pathname);
|
|
49
|
+
if (pathname !== currentPathname) {
|
|
50
|
+
window.scrollTo({ top: 0 /*, behavior: 'smooth'*/ });
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// упавший чанк: остаёмся на текущей странице, ошибку — в консоль
|
|
57
|
+
loadPathModules().catch(console.error);
|
|
58
|
+
// Эффект намеренно завязан на КЛЮЧ пути, а не на сами значения:
|
|
59
|
+
// newRenderPath — новый массив на каждый рендер, а renderPath и
|
|
60
|
+
// currentPathname этот же эффект и меняет. С ними он перезапускал бы
|
|
61
|
+
// сам себя.
|
|
62
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
63
|
+
}, [newRenderPathKey]);
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<ErrorHandler>
|
|
67
|
+
<MemoUglyModuleRenderer
|
|
68
|
+
viteModules={viteModules}
|
|
69
|
+
renderPath={renderPath}
|
|
70
|
+
depth={0}
|
|
71
|
+
pathname={currentPathname}
|
|
72
|
+
>
|
|
73
|
+
<PageNotFound />
|
|
74
|
+
</MemoUglyModuleRenderer>
|
|
75
|
+
</ErrorHandler>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { UglyModule } from "./UglyModule.js";
|
|
2
|
+
import type {
|
|
3
|
+
TModulePath,
|
|
4
|
+
TPageModule,
|
|
5
|
+
ViteModules,
|
|
6
|
+
ViteModulesEager,
|
|
7
|
+
} from "./types.js";
|
|
8
|
+
|
|
9
|
+
export const createUglyModules = (modules: ViteModules | ViteModulesEager) =>
|
|
10
|
+
Object.keys(modules).reduce(
|
|
11
|
+
(acc, modulePath) => {
|
|
12
|
+
const moduleImport = modules[modulePath];
|
|
13
|
+
if (typeof moduleImport === "object") {
|
|
14
|
+
acc[modulePath] = new UglyModule({
|
|
15
|
+
modulePath,
|
|
16
|
+
module: moduleImport,
|
|
17
|
+
moduleImport: () =>
|
|
18
|
+
new Promise<TPageModule>((resolve) =>
|
|
19
|
+
resolve(moduleImport),
|
|
20
|
+
),
|
|
21
|
+
});
|
|
22
|
+
} else {
|
|
23
|
+
acc[modulePath] = new UglyModule({
|
|
24
|
+
modulePath,
|
|
25
|
+
moduleImport: moduleImport!,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return acc;
|
|
30
|
+
},
|
|
31
|
+
{} as Record<TModulePath, UglyModule>,
|
|
32
|
+
);
|