@alepha/react 0.7.0 → 0.7.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/README.md +1 -1
- package/dist/index.browser.cjs +59 -22
- package/dist/index.browser.js +42 -6
- package/dist/index.cjs +152 -85
- package/dist/index.d.ts +317 -206
- package/dist/index.js +130 -64
- package/dist/{useActive-DjpZBEuB.cjs → useRouterState-C2uo0jXu.cjs} +319 -140
- package/dist/{useActive-BX41CqY8.js → useRouterState-D5__ZcUV.js} +321 -143
- package/package.json +11 -10
- package/src/components/ClientOnly.tsx +35 -0
- package/src/components/ErrorBoundary.tsx +1 -1
- package/src/components/ErrorViewer.tsx +161 -0
- package/src/components/Link.tsx +9 -3
- package/src/components/NestedView.tsx +18 -3
- package/src/components/NotFound.tsx +30 -0
- package/src/descriptors/$page.ts +141 -30
- package/src/errors/RedirectionError.ts +4 -1
- package/src/hooks/RouterHookApi.ts +42 -24
- package/src/hooks/useAlepha.ts +12 -0
- package/src/hooks/useClient.ts +8 -6
- package/src/hooks/useInject.ts +2 -2
- package/src/hooks/useQueryParams.ts +1 -1
- package/src/hooks/useRouter.ts +6 -0
- package/src/index.browser.ts +4 -2
- package/src/index.shared.ts +11 -5
- package/src/index.ts +3 -4
- package/src/providers/BrowserRouterProvider.ts +4 -3
- package/src/providers/PageDescriptorProvider.ts +91 -20
- package/src/providers/ReactBrowserProvider.ts +6 -59
- package/src/providers/ReactBrowserRenderer.ts +72 -0
- package/src/providers/ReactServerProvider.ts +200 -81
- package/dist/index.browser.cjs.map +0 -1
- package/dist/index.browser.js.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/useActive-BX41CqY8.js.map +0 -1
- package/dist/useActive-DjpZBEuB.cjs.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PageDescriptor } from "../descriptors/$page.ts";
|
|
2
2
|
import type {
|
|
3
3
|
AnchorProps,
|
|
4
|
+
PageRoute,
|
|
4
5
|
RouterState,
|
|
5
6
|
} from "../providers/PageDescriptorProvider.ts";
|
|
6
7
|
import type {
|
|
@@ -10,6 +11,7 @@ import type {
|
|
|
10
11
|
|
|
11
12
|
export class RouterHookApi {
|
|
12
13
|
constructor(
|
|
14
|
+
private readonly pages: PageRoute[],
|
|
13
15
|
private readonly state: RouterState,
|
|
14
16
|
private readonly layer: {
|
|
15
17
|
path: string;
|
|
@@ -17,23 +19,14 @@ export class RouterHookApi {
|
|
|
17
19
|
private readonly browser?: ReactBrowserProvider,
|
|
18
20
|
) {}
|
|
19
21
|
|
|
20
|
-
/**
|
|
21
|
-
*
|
|
22
|
-
*/
|
|
23
22
|
public get current(): RouterState {
|
|
24
23
|
return this.state;
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
/**
|
|
28
|
-
*
|
|
29
|
-
*/
|
|
30
26
|
public get pathname(): string {
|
|
31
27
|
return this.state.pathname;
|
|
32
28
|
}
|
|
33
29
|
|
|
34
|
-
/**
|
|
35
|
-
*
|
|
36
|
-
*/
|
|
37
30
|
public get query(): Record<string, string> {
|
|
38
31
|
const query: Record<string, string> = {};
|
|
39
32
|
|
|
@@ -46,24 +39,14 @@ export class RouterHookApi {
|
|
|
46
39
|
return query;
|
|
47
40
|
}
|
|
48
41
|
|
|
49
|
-
/**
|
|
50
|
-
*
|
|
51
|
-
*/
|
|
52
42
|
public async back() {
|
|
53
43
|
this.browser?.history.back();
|
|
54
44
|
}
|
|
55
45
|
|
|
56
|
-
/**
|
|
57
|
-
*
|
|
58
|
-
*/
|
|
59
46
|
public async forward() {
|
|
60
47
|
this.browser?.history.forward();
|
|
61
48
|
}
|
|
62
49
|
|
|
63
|
-
/**
|
|
64
|
-
*
|
|
65
|
-
* @param props
|
|
66
|
-
*/
|
|
67
50
|
public async invalidate(props?: Record<string, any>) {
|
|
68
51
|
await this.browser?.invalidate(props);
|
|
69
52
|
}
|
|
@@ -74,11 +57,21 @@ export class RouterHookApi {
|
|
|
74
57
|
* @param pathname
|
|
75
58
|
* @param layer
|
|
76
59
|
*/
|
|
77
|
-
public createHref(
|
|
60
|
+
public createHref(
|
|
61
|
+
pathname: HrefLike,
|
|
62
|
+
layer: { path: string } = this.layer,
|
|
63
|
+
options: { params?: Record<string, any> } = {},
|
|
64
|
+
) {
|
|
78
65
|
if (typeof pathname === "object") {
|
|
79
66
|
pathname = pathname.options.path ?? "";
|
|
80
67
|
}
|
|
81
68
|
|
|
69
|
+
if (options.params) {
|
|
70
|
+
for (const [key, value] of Object.entries(options.params)) {
|
|
71
|
+
pathname = pathname.replace(`:${key}`, String(value));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
82
75
|
return pathname.startsWith("/")
|
|
83
76
|
? pathname
|
|
84
77
|
: `${layer.path}/${pathname}`.replace(/\/\/+/g, "/");
|
|
@@ -90,18 +83,43 @@ export class RouterHookApi {
|
|
|
90
83
|
options?: RouterGoOptions,
|
|
91
84
|
): Promise<void>;
|
|
92
85
|
public async go(path: string, options?: RouterGoOptions): Promise<void> {
|
|
93
|
-
|
|
86
|
+
for (const page of this.pages) {
|
|
87
|
+
if (page.name === path) {
|
|
88
|
+
path = page.path ?? "";
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await this.browser?.go(this.createHref(path, this.layer, options), options);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
public anchor(
|
|
97
|
-
|
|
96
|
+
public anchor(
|
|
97
|
+
path: string,
|
|
98
|
+
options?: { params?: Record<string, any> },
|
|
99
|
+
): AnchorProps;
|
|
100
|
+
public anchor<T extends object>(
|
|
101
|
+
path: keyof VirtualRouter<T>,
|
|
102
|
+
options?: { params?: Record<string, any> },
|
|
103
|
+
): AnchorProps;
|
|
104
|
+
public anchor(
|
|
105
|
+
path: string,
|
|
106
|
+
options: { params?: Record<string, any> } = {},
|
|
107
|
+
): AnchorProps {
|
|
108
|
+
for (const page of this.pages) {
|
|
109
|
+
if (page.name === path) {
|
|
110
|
+
path = page.path ?? "";
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const href = this.createHref(path, this.layer, options);
|
|
98
116
|
return {
|
|
99
117
|
href,
|
|
100
118
|
onClick: (ev: any) => {
|
|
101
119
|
ev.stopPropagation();
|
|
102
120
|
ev.preventDefault();
|
|
103
121
|
|
|
104
|
-
this.go(path).catch(console.error);
|
|
122
|
+
this.go(path, options).catch(console.error);
|
|
105
123
|
},
|
|
106
124
|
};
|
|
107
125
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Alepha } from "@alepha/core";
|
|
2
|
+
import { useContext } from "react";
|
|
3
|
+
import { RouterContext } from "../contexts/RouterContext.ts";
|
|
4
|
+
|
|
5
|
+
export const useAlepha = (): Alepha => {
|
|
6
|
+
const routerContext = useContext(RouterContext);
|
|
7
|
+
if (!routerContext) {
|
|
8
|
+
throw new Error("useAlepha must be used within a RouterProvider");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return routerContext.alepha;
|
|
12
|
+
};
|
package/src/hooks/useClient.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type ClientScope,
|
|
3
|
+
HttpClient,
|
|
4
|
+
type HttpVirtualClient,
|
|
5
|
+
} from "@alepha/server";
|
|
2
6
|
import { useInject } from "./useInject.ts";
|
|
3
7
|
|
|
4
|
-
export const useClient =
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export const useApi = <T extends object>() => {
|
|
8
|
+
export const useClient = <T extends object>(
|
|
9
|
+
_scope?: ClientScope,
|
|
10
|
+
): HttpVirtualClient<T> => {
|
|
9
11
|
return useInject(HttpClient).of<T>();
|
|
10
12
|
};
|
package/src/hooks/useInject.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Service } from "@alepha/core";
|
|
2
2
|
import { useContext, useMemo } from "react";
|
|
3
3
|
import { RouterContext } from "../contexts/RouterContext.ts";
|
|
4
4
|
|
|
5
|
-
export const useInject = <T extends object>(clazz:
|
|
5
|
+
export const useInject = <T extends object>(clazz: Service<T>): T => {
|
|
6
6
|
const ctx = useContext(RouterContext);
|
|
7
7
|
if (!ctx) {
|
|
8
8
|
throw new Error("useRouter must be used within a <RouterProvider>");
|
|
@@ -50,7 +50,7 @@ const encode = (alepha: Alepha, schema: TObject, data: any) => {
|
|
|
50
50
|
const decode = (alepha: Alepha, schema: TObject, data: any) => {
|
|
51
51
|
try {
|
|
52
52
|
return alepha.parse(schema, JSON.parse(atob(decodeURIComponent(data))));
|
|
53
|
-
} catch (
|
|
53
|
+
} catch (_error) {
|
|
54
54
|
return {};
|
|
55
55
|
}
|
|
56
56
|
};
|
package/src/hooks/useRouter.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useContext, useMemo } from "react";
|
|
2
2
|
import { RouterContext } from "../contexts/RouterContext.ts";
|
|
3
3
|
import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
|
|
4
|
+
import { PageDescriptorProvider } from "../providers/PageDescriptorProvider.ts";
|
|
4
5
|
import { ReactBrowserProvider } from "../providers/ReactBrowserProvider.ts";
|
|
5
6
|
import { RouterHookApi } from "./RouterHookApi.ts";
|
|
6
7
|
|
|
@@ -11,9 +12,14 @@ export const useRouter = (): RouterHookApi => {
|
|
|
11
12
|
throw new Error("useRouter must be used within a RouterProvider");
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
const pages = useMemo(() => {
|
|
16
|
+
return ctx.alepha.get(PageDescriptorProvider).getPages();
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
14
19
|
return useMemo(
|
|
15
20
|
() =>
|
|
16
21
|
new RouterHookApi(
|
|
22
|
+
pages,
|
|
17
23
|
ctx.state,
|
|
18
24
|
layer,
|
|
19
25
|
ctx.alepha.isBrowser()
|
package/src/index.browser.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { $inject, Alepha
|
|
1
|
+
import { __bind, $inject, Alepha } from "@alepha/core";
|
|
2
2
|
import { $page } from "./descriptors/$page.ts";
|
|
3
3
|
import { BrowserRouterProvider } from "./providers/BrowserRouterProvider.ts";
|
|
4
4
|
import { PageDescriptorProvider } from "./providers/PageDescriptorProvider.ts";
|
|
5
5
|
import { ReactBrowserProvider } from "./providers/ReactBrowserProvider.ts";
|
|
6
|
+
import { ReactBrowserRenderer } from "./providers/ReactBrowserRenderer.ts";
|
|
6
7
|
|
|
7
8
|
export * from "./index.shared";
|
|
8
9
|
export * from "./providers/ReactBrowserProvider.ts";
|
|
@@ -14,7 +15,8 @@ export class ReactModule {
|
|
|
14
15
|
this.alepha //
|
|
15
16
|
.with(PageDescriptorProvider)
|
|
16
17
|
.with(ReactBrowserProvider)
|
|
17
|
-
.with(BrowserRouterProvider)
|
|
18
|
+
.with(BrowserRouterProvider)
|
|
19
|
+
.with(ReactBrowserRenderer);
|
|
18
20
|
}
|
|
19
21
|
}
|
|
20
22
|
|
package/src/index.shared.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
export { default as
|
|
2
|
-
export { default as Link } from "./components/Link.tsx";
|
|
1
|
+
export { default as ClientOnly } from "./components/ClientOnly.tsx";
|
|
3
2
|
export { default as ErrorBoundary } from "./components/ErrorBoundary.tsx";
|
|
3
|
+
export * from "./components/ErrorViewer.tsx";
|
|
4
|
+
export { default as Link } from "./components/Link.tsx";
|
|
5
|
+
export { default as NestedView } from "./components/NestedView.tsx";
|
|
4
6
|
|
|
5
7
|
export * from "./contexts/RouterContext.ts";
|
|
6
8
|
export * from "./contexts/RouterLayerContext.ts";
|
|
9
|
+
|
|
7
10
|
export * from "./descriptors/$page.ts";
|
|
11
|
+
|
|
12
|
+
export * from "./errors/RedirectionError.ts";
|
|
13
|
+
|
|
8
14
|
export * from "./hooks/RouterHookApi.ts";
|
|
9
|
-
export * from "./hooks/
|
|
15
|
+
export * from "./hooks/useActive.ts";
|
|
16
|
+
export * from "./hooks/useAlepha.ts";
|
|
10
17
|
export * from "./hooks/useClient.ts";
|
|
18
|
+
export * from "./hooks/useInject.ts";
|
|
11
19
|
export * from "./hooks/useQueryParams.ts";
|
|
12
20
|
export * from "./hooks/useRouter.ts";
|
|
13
21
|
export * from "./hooks/useRouterEvents.ts";
|
|
14
22
|
export * from "./hooks/useRouterState.ts";
|
|
15
|
-
export * from "./hooks/useActive.ts";
|
|
16
|
-
export * from "./errors/RedirectionError.ts";
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $inject, Alepha
|
|
1
|
+
import { __bind, $inject, Alepha } from "@alepha/core";
|
|
2
2
|
import {
|
|
3
3
|
ServerLinksProvider,
|
|
4
4
|
ServerModule,
|
|
@@ -13,13 +13,13 @@ import {
|
|
|
13
13
|
} from "./providers/PageDescriptorProvider.ts";
|
|
14
14
|
import type { ReactHydrationState } from "./providers/ReactBrowserProvider.ts";
|
|
15
15
|
import { ReactServerProvider } from "./providers/ReactServerProvider.ts";
|
|
16
|
-
export { default as NestedView } from "./components/NestedView.tsx";
|
|
17
16
|
|
|
17
|
+
export { default as NestedView } from "./components/NestedView.tsx";
|
|
18
|
+
export * from "./errors/RedirectionError.ts";
|
|
18
19
|
export * from "./index.shared.ts";
|
|
19
20
|
export * from "./providers/PageDescriptorProvider.ts";
|
|
20
21
|
export * from "./providers/ReactBrowserProvider.ts";
|
|
21
22
|
export * from "./providers/ReactServerProvider.ts";
|
|
22
|
-
export * from "./errors/RedirectionError.ts";
|
|
23
23
|
|
|
24
24
|
declare module "@alepha/core" {
|
|
25
25
|
interface Hooks {
|
|
@@ -32,7 +32,6 @@ declare module "@alepha/core" {
|
|
|
32
32
|
request: ServerRequest;
|
|
33
33
|
pageRequest: PageRequest;
|
|
34
34
|
};
|
|
35
|
-
|
|
36
35
|
"react:transition:begin": {
|
|
37
36
|
state: RouterState;
|
|
38
37
|
context: PageReactContext;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { $hook, $inject, $logger, Alepha } from "@alepha/core";
|
|
2
2
|
import { type Route, RouterProvider } from "@alepha/router";
|
|
3
|
-
import type
|
|
3
|
+
import { createElement, type ReactNode } from "react";
|
|
4
|
+
import NotFoundPage from "../components/NotFound.tsx";
|
|
4
5
|
import {
|
|
6
|
+
isPageRoute,
|
|
5
7
|
PageDescriptorProvider,
|
|
6
8
|
type PageReactContext,
|
|
7
9
|
type PageRequest,
|
|
@@ -10,7 +12,6 @@ import {
|
|
|
10
12
|
type RouterRenderResult,
|
|
11
13
|
type RouterState,
|
|
12
14
|
type TransitionOptions,
|
|
13
|
-
isPageRoute,
|
|
14
15
|
} from "./PageDescriptorProvider.ts";
|
|
15
16
|
|
|
16
17
|
export interface BrowserRoute extends Route {
|
|
@@ -98,7 +99,7 @@ export class BrowserRouterProvider extends RouterProvider<BrowserRoute> {
|
|
|
98
99
|
if (state.layers.length === 0) {
|
|
99
100
|
state.layers.push({
|
|
100
101
|
name: "not-found",
|
|
101
|
-
element:
|
|
102
|
+
element: createElement(NotFoundPage),
|
|
102
103
|
index: 0,
|
|
103
104
|
path: "/",
|
|
104
105
|
});
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Static } from "@alepha/core";
|
|
2
|
+
import { $hook, $inject, $logger, Alepha, OPTIONS, t } from "@alepha/core";
|
|
2
3
|
import type { ApiLinksResponse } from "@alepha/server";
|
|
3
|
-
import { type ReactNode,
|
|
4
|
+
import { createElement, type ReactNode, StrictMode } from "react";
|
|
5
|
+
import ClientOnly from "../components/ClientOnly.tsx";
|
|
6
|
+
import ErrorViewer from "../components/ErrorViewer.tsx";
|
|
4
7
|
import NestedView from "../components/NestedView.tsx";
|
|
8
|
+
import NotFoundPage from "../components/NotFound.tsx";
|
|
5
9
|
import { RouterContext } from "../contexts/RouterContext.ts";
|
|
6
10
|
import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
|
|
7
11
|
import {
|
|
@@ -11,8 +15,17 @@ import {
|
|
|
11
15
|
} from "../descriptors/$page.ts";
|
|
12
16
|
import { RedirectionError } from "../errors/RedirectionError.ts";
|
|
13
17
|
|
|
18
|
+
const envSchema = t.object({
|
|
19
|
+
REACT_STRICT_MODE: t.boolean({ default: true }),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
declare module "@alepha/core" {
|
|
23
|
+
export interface Env extends Partial<Static<typeof envSchema>> {}
|
|
24
|
+
}
|
|
25
|
+
|
|
14
26
|
export class PageDescriptorProvider {
|
|
15
27
|
protected readonly log = $logger();
|
|
28
|
+
protected readonly env = $inject(envSchema);
|
|
16
29
|
protected readonly alepha = $inject(Alepha);
|
|
17
30
|
protected readonly pages: PageRoute[] = [];
|
|
18
31
|
|
|
@@ -30,8 +43,32 @@ export class PageDescriptorProvider {
|
|
|
30
43
|
throw new Error(`Page ${name} not found`);
|
|
31
44
|
}
|
|
32
45
|
|
|
46
|
+
public url(
|
|
47
|
+
name: string,
|
|
48
|
+
options: { params?: Record<string, string>; base?: string } = {},
|
|
49
|
+
): URL {
|
|
50
|
+
const page = this.page(name);
|
|
51
|
+
if (!page) {
|
|
52
|
+
throw new Error(`Page ${name} not found`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let url = page.path ?? "";
|
|
56
|
+
let parent = page.parent;
|
|
57
|
+
while (parent) {
|
|
58
|
+
url = `${parent.path ?? ""}/${url}`;
|
|
59
|
+
parent = parent.parent;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
url = this.compile(url, options.params ?? {});
|
|
63
|
+
|
|
64
|
+
return new URL(
|
|
65
|
+
url.replace(/\/\/+/g, "/") || "/",
|
|
66
|
+
options.base ?? `http://localhost`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
33
70
|
public root(state: RouterState, context: PageReactContext): ReactNode {
|
|
34
|
-
|
|
71
|
+
const root = createElement(
|
|
35
72
|
RouterContext.Provider,
|
|
36
73
|
{
|
|
37
74
|
value: {
|
|
@@ -42,6 +79,12 @@ export class PageDescriptorProvider {
|
|
|
42
79
|
},
|
|
43
80
|
createElement(NestedView, {}, state.layers[0]?.element),
|
|
44
81
|
);
|
|
82
|
+
|
|
83
|
+
if (this.env.REACT_STRICT_MODE) {
|
|
84
|
+
return createElement(StrictMode, {}, root);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return root;
|
|
45
88
|
}
|
|
46
89
|
|
|
47
90
|
public async createLayers(
|
|
@@ -52,7 +95,7 @@ export class PageDescriptorProvider {
|
|
|
52
95
|
const layers: Layer[] = []; // result layers
|
|
53
96
|
let context: Record<string, any> = {}; // all props
|
|
54
97
|
const stack: Array<RouterStackItem> = [{ route }]; // stack of routes
|
|
55
|
-
|
|
98
|
+
request.onError = (error) => this.renderError(error); // error handler
|
|
56
99
|
|
|
57
100
|
let parent = route.parent;
|
|
58
101
|
while (parent) {
|
|
@@ -182,12 +225,15 @@ export class PageDescriptorProvider {
|
|
|
182
225
|
const path = acc.replace(/\/+/, "/");
|
|
183
226
|
const localErrorHandler = this.getErrorHandler(it.route);
|
|
184
227
|
if (localErrorHandler) {
|
|
185
|
-
onError = localErrorHandler;
|
|
228
|
+
request.onError = localErrorHandler;
|
|
186
229
|
}
|
|
187
230
|
|
|
188
231
|
// handler has thrown an error, render an error view
|
|
189
232
|
if (it.error) {
|
|
190
|
-
|
|
233
|
+
let element: ReactNode = await request.onError(it.error);
|
|
234
|
+
if (element === null) {
|
|
235
|
+
element = this.renderError(it.error);
|
|
236
|
+
}
|
|
191
237
|
|
|
192
238
|
layers.push({
|
|
193
239
|
props,
|
|
@@ -195,7 +241,7 @@ export class PageDescriptorProvider {
|
|
|
195
241
|
name: it.route.name,
|
|
196
242
|
part: it.route.path,
|
|
197
243
|
config: it.config,
|
|
198
|
-
element: this.renderView(i + 1, path, element),
|
|
244
|
+
element: this.renderView(i + 1, path, element, it.route),
|
|
199
245
|
index: i + 1,
|
|
200
246
|
path,
|
|
201
247
|
});
|
|
@@ -204,7 +250,7 @@ export class PageDescriptorProvider {
|
|
|
204
250
|
|
|
205
251
|
// normal use case
|
|
206
252
|
|
|
207
|
-
const
|
|
253
|
+
const element = await this.createElement(it.route, {
|
|
208
254
|
...props,
|
|
209
255
|
...context,
|
|
210
256
|
});
|
|
@@ -214,7 +260,7 @@ export class PageDescriptorProvider {
|
|
|
214
260
|
props,
|
|
215
261
|
part: it.route.path,
|
|
216
262
|
config: it.config,
|
|
217
|
-
element: this.renderView(i + 1, path,
|
|
263
|
+
element: this.renderView(i + 1, path, element, it.route),
|
|
218
264
|
index: i + 1,
|
|
219
265
|
path,
|
|
220
266
|
});
|
|
@@ -293,8 +339,8 @@ export class PageDescriptorProvider {
|
|
|
293
339
|
}
|
|
294
340
|
}
|
|
295
341
|
|
|
296
|
-
public renderError(
|
|
297
|
-
return createElement(
|
|
342
|
+
public renderError(error: Error): ReactNode {
|
|
343
|
+
return createElement(ErrorViewer, { error });
|
|
298
344
|
}
|
|
299
345
|
|
|
300
346
|
public renderEmptyView(): ReactNode {
|
|
@@ -332,8 +378,19 @@ export class PageDescriptorProvider {
|
|
|
332
378
|
protected renderView(
|
|
333
379
|
index: number,
|
|
334
380
|
path: string,
|
|
335
|
-
view: ReactNode
|
|
381
|
+
view: ReactNode | undefined,
|
|
382
|
+
page: PageRoute,
|
|
336
383
|
): ReactNode {
|
|
384
|
+
view ??= this.renderEmptyView();
|
|
385
|
+
|
|
386
|
+
const element = page.client
|
|
387
|
+
? createElement(
|
|
388
|
+
ClientOnly,
|
|
389
|
+
typeof page.client === "object" ? page.client : {},
|
|
390
|
+
view,
|
|
391
|
+
)
|
|
392
|
+
: view;
|
|
393
|
+
|
|
337
394
|
return createElement(
|
|
338
395
|
RouterLayerContext.Provider,
|
|
339
396
|
{
|
|
@@ -342,24 +399,44 @@ export class PageDescriptorProvider {
|
|
|
342
399
|
path,
|
|
343
400
|
},
|
|
344
401
|
},
|
|
345
|
-
|
|
402
|
+
element,
|
|
346
403
|
);
|
|
347
404
|
}
|
|
348
405
|
|
|
349
406
|
protected readonly configure = $hook({
|
|
350
407
|
name: "configure",
|
|
351
408
|
handler: () => {
|
|
409
|
+
let hasNotFoundHandler = false;
|
|
352
410
|
const pages = this.alepha.getDescriptorValues($page);
|
|
353
411
|
for (const { value, key } of pages) {
|
|
354
412
|
value[OPTIONS].name ??= key;
|
|
413
|
+
}
|
|
355
414
|
|
|
415
|
+
for (const { value } of pages) {
|
|
356
416
|
// skip children, we only want root pages
|
|
357
417
|
if (value[OPTIONS].parent) {
|
|
358
418
|
continue;
|
|
359
419
|
}
|
|
360
420
|
|
|
421
|
+
if (value[OPTIONS].path === "/*") {
|
|
422
|
+
hasNotFoundHandler = true;
|
|
423
|
+
}
|
|
424
|
+
|
|
361
425
|
this.add(this.map(pages, value));
|
|
362
426
|
}
|
|
427
|
+
|
|
428
|
+
if (!hasNotFoundHandler && pages.length > 0) {
|
|
429
|
+
// add a default 404 page if not already defined
|
|
430
|
+
this.add({
|
|
431
|
+
path: "/*",
|
|
432
|
+
name: "notFound",
|
|
433
|
+
cache: true,
|
|
434
|
+
component: NotFoundPage,
|
|
435
|
+
afterHandler: ({ reply }) => {
|
|
436
|
+
reply.status = 404;
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
}
|
|
363
440
|
},
|
|
364
441
|
});
|
|
365
442
|
|
|
@@ -369,12 +446,6 @@ export class PageDescriptorProvider {
|
|
|
369
446
|
): PageRouteEntry {
|
|
370
447
|
const children = target[OPTIONS].children ?? [];
|
|
371
448
|
|
|
372
|
-
for (const it of pages) {
|
|
373
|
-
if (it.value[OPTIONS].parent === target) {
|
|
374
|
-
children.push(it.value);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
449
|
return {
|
|
379
450
|
...target[OPTIONS],
|
|
380
451
|
parent: undefined,
|
|
@@ -465,7 +536,7 @@ export interface Layer {
|
|
|
465
536
|
path: string;
|
|
466
537
|
}
|
|
467
538
|
|
|
468
|
-
export type PreviousLayerData = Omit<Layer, "element">;
|
|
539
|
+
export type PreviousLayerData = Omit<Layer, "element" | "index" | "path">;
|
|
469
540
|
|
|
470
541
|
export interface AnchorProps {
|
|
471
542
|
href: string;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { $hook, $inject, $logger, Alepha
|
|
2
|
-
import {
|
|
1
|
+
import { $hook, $inject, $logger, Alepha } from "@alepha/core";
|
|
2
|
+
import { type ApiLinksResponse, HttpClient } from "@alepha/server";
|
|
3
3
|
import type { Root } from "react-dom/client";
|
|
4
|
-
import { createRoot, hydrateRoot } from "react-dom/client";
|
|
5
4
|
import { BrowserHeadProvider } from "./BrowserHeadProvider.ts";
|
|
6
5
|
import { BrowserRouterProvider } from "./BrowserRouterProvider.ts";
|
|
7
6
|
import type {
|
|
@@ -11,21 +10,12 @@ import type {
|
|
|
11
10
|
TransitionOptions,
|
|
12
11
|
} from "./PageDescriptorProvider.ts";
|
|
13
12
|
|
|
14
|
-
const envSchema = t.object({
|
|
15
|
-
REACT_ROOT_ID: t.string({ default: "root" }),
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
declare module "@alepha/core" {
|
|
19
|
-
interface Env extends Partial<Static<typeof envSchema>> {}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
13
|
export class ReactBrowserProvider {
|
|
23
14
|
protected readonly log = $logger();
|
|
24
15
|
protected readonly client = $inject(HttpClient);
|
|
25
16
|
protected readonly alepha = $inject(Alepha);
|
|
26
17
|
protected readonly router = $inject(BrowserRouterProvider);
|
|
27
18
|
protected readonly headProvider = $inject(BrowserHeadProvider);
|
|
28
|
-
protected readonly env = $inject(envSchema);
|
|
29
19
|
protected root!: Root;
|
|
30
20
|
|
|
31
21
|
public transitioning?: {
|
|
@@ -75,11 +65,6 @@ export class ReactBrowserProvider {
|
|
|
75
65
|
await this.render({ previous });
|
|
76
66
|
}
|
|
77
67
|
|
|
78
|
-
/**
|
|
79
|
-
*
|
|
80
|
-
* @param url
|
|
81
|
-
* @param options
|
|
82
|
-
*/
|
|
83
68
|
public async go(url: string, options: RouterGoOptions = {}): Promise<void> {
|
|
84
69
|
const result = await this.render({
|
|
85
70
|
url,
|
|
@@ -101,10 +86,7 @@ export class ReactBrowserProvider {
|
|
|
101
86
|
}
|
|
102
87
|
|
|
103
88
|
protected async render(
|
|
104
|
-
options: {
|
|
105
|
-
url?: string;
|
|
106
|
-
previous?: PreviousLayerData[];
|
|
107
|
-
} = {},
|
|
89
|
+
options: { url?: string; previous?: PreviousLayerData[] } = {},
|
|
108
90
|
): Promise<RouterRenderResult> {
|
|
109
91
|
const previous = options.previous ?? this.state.layers;
|
|
110
92
|
const url = options.url ?? this.url;
|
|
@@ -130,8 +112,6 @@ export class ReactBrowserProvider {
|
|
|
130
112
|
|
|
131
113
|
/**
|
|
132
114
|
* Get embedded layers from the server.
|
|
133
|
-
*
|
|
134
|
-
* @protected
|
|
135
115
|
*/
|
|
136
116
|
protected getHydrationState(): ReactHydrationState | undefined {
|
|
137
117
|
try {
|
|
@@ -143,30 +123,8 @@ export class ReactBrowserProvider {
|
|
|
143
123
|
}
|
|
144
124
|
}
|
|
145
125
|
|
|
146
|
-
/**
|
|
147
|
-
*
|
|
148
|
-
* @protected
|
|
149
|
-
*/
|
|
150
|
-
protected getRootElement() {
|
|
151
|
-
const root = this.document.getElementById(this.env.REACT_ROOT_ID);
|
|
152
|
-
if (root) {
|
|
153
|
-
return root;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const div = this.document.createElement("div");
|
|
157
|
-
div.id = this.env.REACT_ROOT_ID;
|
|
158
|
-
|
|
159
|
-
this.document.body.prepend(div);
|
|
160
|
-
|
|
161
|
-
return div;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
126
|
// -------------------------------------------------------------------------------------------------------------------
|
|
165
127
|
|
|
166
|
-
/**
|
|
167
|
-
*
|
|
168
|
-
* @protected
|
|
169
|
-
*/
|
|
170
128
|
public readonly ready = $hook({
|
|
171
129
|
name: "ready",
|
|
172
130
|
handler: async () => {
|
|
@@ -174,7 +132,7 @@ export class ReactBrowserProvider {
|
|
|
174
132
|
const previous = hydration?.layers ?? [];
|
|
175
133
|
|
|
176
134
|
if (hydration?.links) {
|
|
177
|
-
for (const link of hydration.links) {
|
|
135
|
+
for (const link of hydration.links.links) {
|
|
178
136
|
this.client.pushLink(link);
|
|
179
137
|
}
|
|
180
138
|
}
|
|
@@ -190,17 +148,6 @@ export class ReactBrowserProvider {
|
|
|
190
148
|
hydration,
|
|
191
149
|
});
|
|
192
150
|
|
|
193
|
-
const element = this.router.root(this.state, context);
|
|
194
|
-
|
|
195
|
-
if (previous.length > 0) {
|
|
196
|
-
this.root = hydrateRoot(this.getRootElement(), element);
|
|
197
|
-
this.log.info("Hydrated root element");
|
|
198
|
-
} else {
|
|
199
|
-
this.root ??= createRoot(this.getRootElement());
|
|
200
|
-
this.root.render(element);
|
|
201
|
-
this.log.info("Created root element");
|
|
202
|
-
}
|
|
203
|
-
|
|
204
151
|
window.addEventListener("popstate", () => {
|
|
205
152
|
this.render();
|
|
206
153
|
});
|
|
@@ -224,6 +171,6 @@ export interface RouterGoOptions {
|
|
|
224
171
|
}
|
|
225
172
|
|
|
226
173
|
export interface ReactHydrationState {
|
|
227
|
-
layers?: PreviousLayerData
|
|
228
|
-
links?:
|
|
174
|
+
layers?: Array<PreviousLayerData>;
|
|
175
|
+
links?: ApiLinksResponse;
|
|
229
176
|
}
|