@alepha/react 0.9.2 → 0.9.4
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 +46 -0
- package/dist/index.browser.js +378 -325
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +570 -458
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +305 -213
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +304 -212
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +567 -460
- package/dist/index.js.map +1 -1
- package/package.json +16 -13
- package/src/components/ErrorViewer.tsx +1 -1
- package/src/components/Link.tsx +4 -24
- package/src/components/NestedView.tsx +20 -9
- package/src/components/NotFound.tsx +5 -2
- package/src/descriptors/$page.ts +86 -12
- package/src/errors/Redirection.ts +13 -0
- package/src/hooks/useActive.ts +28 -30
- package/src/hooks/useAlepha.ts +16 -2
- package/src/hooks/useClient.ts +7 -2
- package/src/hooks/useInject.ts +4 -1
- package/src/hooks/useQueryParams.ts +9 -6
- package/src/hooks/useRouter.ts +18 -30
- package/src/hooks/useRouterEvents.ts +7 -4
- package/src/hooks/useRouterState.ts +8 -20
- package/src/hooks/useSchema.ts +10 -15
- package/src/hooks/useStore.ts +9 -8
- package/src/index.browser.ts +11 -11
- package/src/index.shared.ts +4 -5
- package/src/index.ts +21 -30
- package/src/providers/ReactBrowserProvider.ts +155 -65
- package/src/providers/ReactBrowserRouterProvider.ts +132 -0
- package/src/providers/{PageDescriptorProvider.ts → ReactPageProvider.ts} +164 -112
- package/src/providers/ReactServerProvider.ts +100 -68
- package/src/{hooks/RouterHookApi.ts → services/ReactRouter.ts} +75 -61
- package/src/contexts/RouterContext.ts +0 -14
- package/src/errors/RedirectionError.ts +0 -10
- package/src/providers/BrowserRouterProvider.ts +0 -146
- package/src/providers/ReactBrowserRenderer.ts +0 -93
package/src/index.browser.ts
CHANGED
|
@@ -2,17 +2,17 @@ import { $module } from "@alepha/core";
|
|
|
2
2
|
import { AlephaServer } from "@alepha/server";
|
|
3
3
|
import { AlephaServerLinks } from "@alepha/server-links";
|
|
4
4
|
import { $page } from "./descriptors/$page.ts";
|
|
5
|
-
import { BrowserRouterProvider } from "./providers/BrowserRouterProvider.ts";
|
|
6
|
-
import { PageDescriptorProvider } from "./providers/PageDescriptorProvider.ts";
|
|
7
5
|
import { ReactBrowserProvider } from "./providers/ReactBrowserProvider.ts";
|
|
8
|
-
import {
|
|
6
|
+
import { ReactBrowserRouterProvider } from "./providers/ReactBrowserRouterProvider.ts";
|
|
7
|
+
import { ReactPageProvider } from "./providers/ReactPageProvider.ts";
|
|
8
|
+
import { ReactRouter } from "./services/ReactRouter.ts";
|
|
9
9
|
|
|
10
10
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
11
11
|
|
|
12
12
|
export * from "./index.shared.ts";
|
|
13
|
-
export * from "./providers/BrowserRouterProvider.ts";
|
|
14
|
-
export * from "./providers/PageDescriptorProvider.ts";
|
|
15
13
|
export * from "./providers/ReactBrowserProvider.ts";
|
|
14
|
+
export * from "./providers/ReactBrowserRouterProvider.ts";
|
|
15
|
+
export * from "./providers/ReactPageProvider.ts";
|
|
16
16
|
|
|
17
17
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
18
18
|
|
|
@@ -20,17 +20,17 @@ export const AlephaReact = $module({
|
|
|
20
20
|
name: "alepha.react",
|
|
21
21
|
descriptors: [$page],
|
|
22
22
|
services: [
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
BrowserRouterProvider,
|
|
23
|
+
ReactPageProvider,
|
|
24
|
+
ReactBrowserRouterProvider,
|
|
26
25
|
ReactBrowserProvider,
|
|
26
|
+
ReactRouter,
|
|
27
27
|
],
|
|
28
28
|
register: (alepha) =>
|
|
29
29
|
alepha
|
|
30
30
|
.with(AlephaServer)
|
|
31
31
|
.with(AlephaServerLinks)
|
|
32
|
-
.with(
|
|
32
|
+
.with(ReactPageProvider)
|
|
33
33
|
.with(ReactBrowserProvider)
|
|
34
|
-
.with(
|
|
35
|
-
.with(
|
|
34
|
+
.with(ReactBrowserRouterProvider)
|
|
35
|
+
.with(ReactRouter),
|
|
36
36
|
});
|
package/src/index.shared.ts
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
export { default as ClientOnly } from "./components/ClientOnly.tsx";
|
|
2
2
|
export { default as ErrorBoundary } from "./components/ErrorBoundary.tsx";
|
|
3
|
-
export
|
|
4
|
-
export { default as Link } from "./components/Link.tsx";
|
|
3
|
+
export { default as ErrorViewer } from "./components/ErrorViewer.tsx";
|
|
4
|
+
export { default as Link, type LinkProps } from "./components/Link.tsx";
|
|
5
5
|
export { default as NestedView } from "./components/NestedView.tsx";
|
|
6
6
|
export { default as NotFound } from "./components/NotFound.tsx";
|
|
7
7
|
export * from "./contexts/AlephaContext.ts";
|
|
8
|
-
export * from "./contexts/RouterContext.ts";
|
|
9
8
|
export * from "./contexts/RouterLayerContext.ts";
|
|
10
9
|
export * from "./descriptors/$page.ts";
|
|
11
|
-
export * from "./errors/
|
|
12
|
-
export * from "./hooks/RouterHookApi.ts";
|
|
10
|
+
export * from "./errors/Redirection.ts";
|
|
13
11
|
export * from "./hooks/useActive.ts";
|
|
14
12
|
export * from "./hooks/useAlepha.ts";
|
|
15
13
|
export * from "./hooks/useClient.ts";
|
|
@@ -20,3 +18,4 @@ export * from "./hooks/useRouterEvents.ts";
|
|
|
20
18
|
export * from "./hooks/useRouterState.ts";
|
|
21
19
|
export * from "./hooks/useSchema.ts";
|
|
22
20
|
export * from "./hooks/useStore.ts";
|
|
21
|
+
export * from "./services/ReactRouter.ts";
|
package/src/index.ts
CHANGED
|
@@ -3,65 +3,55 @@ import { AlephaServer, type ServerRequest } from "@alepha/server";
|
|
|
3
3
|
import { AlephaServerCache } from "@alepha/server-cache";
|
|
4
4
|
import { AlephaServerLinks } from "@alepha/server-links";
|
|
5
5
|
import { $page } from "./descriptors/$page.ts";
|
|
6
|
+
import type { ReactHydrationState } from "./providers/ReactBrowserProvider.ts";
|
|
6
7
|
import {
|
|
7
|
-
|
|
8
|
-
type
|
|
9
|
-
|
|
10
|
-
type RouterState,
|
|
11
|
-
} from "./providers/PageDescriptorProvider.ts";
|
|
12
|
-
import {
|
|
13
|
-
ReactBrowserProvider,
|
|
14
|
-
type ReactHydrationState,
|
|
15
|
-
} from "./providers/ReactBrowserProvider.ts";
|
|
8
|
+
ReactPageProvider,
|
|
9
|
+
type ReactRouterState,
|
|
10
|
+
} from "./providers/ReactPageProvider.ts";
|
|
16
11
|
import { ReactServerProvider } from "./providers/ReactServerProvider.ts";
|
|
12
|
+
import { ReactRouter } from "./services/ReactRouter.ts";
|
|
17
13
|
|
|
18
14
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
19
15
|
|
|
20
16
|
export * from "./index.shared.ts";
|
|
21
|
-
export * from "./providers/PageDescriptorProvider.ts";
|
|
22
17
|
export * from "./providers/ReactBrowserProvider.ts";
|
|
18
|
+
export * from "./providers/ReactPageProvider.ts";
|
|
23
19
|
export * from "./providers/ReactServerProvider.ts";
|
|
24
20
|
|
|
25
21
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
26
22
|
|
|
27
23
|
declare module "@alepha/core" {
|
|
24
|
+
interface State {
|
|
25
|
+
"react.router.state"?: ReactRouterState;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
28
|
interface Hooks {
|
|
29
|
-
"react:router:createLayers": {
|
|
30
|
-
request: ServerRequest;
|
|
31
|
-
context: PageRequest;
|
|
32
|
-
layers: PageRequest[];
|
|
33
|
-
};
|
|
34
29
|
"react:server:render:begin": {
|
|
35
30
|
request?: ServerRequest;
|
|
36
|
-
|
|
31
|
+
state: ReactRouterState;
|
|
37
32
|
};
|
|
38
33
|
"react:server:render:end": {
|
|
39
34
|
request?: ServerRequest;
|
|
40
|
-
|
|
41
|
-
state: RouterState;
|
|
35
|
+
state: ReactRouterState;
|
|
42
36
|
html: string;
|
|
43
37
|
};
|
|
38
|
+
// -----------------------------------------------------------------------------------------------------------------
|
|
44
39
|
"react:browser:render": {
|
|
45
|
-
state:
|
|
46
|
-
context: PageReactContext;
|
|
40
|
+
state: ReactRouterState;
|
|
47
41
|
hydration?: ReactHydrationState;
|
|
48
42
|
};
|
|
49
43
|
"react:transition:begin": {
|
|
50
|
-
state:
|
|
51
|
-
context: PageReactContext;
|
|
44
|
+
state: ReactRouterState;
|
|
52
45
|
};
|
|
53
46
|
"react:transition:success": {
|
|
54
|
-
state:
|
|
55
|
-
context: PageReactContext;
|
|
47
|
+
state: ReactRouterState;
|
|
56
48
|
};
|
|
57
49
|
"react:transition:error": {
|
|
50
|
+
state: ReactRouterState;
|
|
58
51
|
error: Error;
|
|
59
|
-
state: RouterState;
|
|
60
|
-
context: PageReactContext;
|
|
61
52
|
};
|
|
62
53
|
"react:transition:end": {
|
|
63
|
-
state:
|
|
64
|
-
context: PageReactContext;
|
|
54
|
+
state: ReactRouterState;
|
|
65
55
|
};
|
|
66
56
|
}
|
|
67
57
|
}
|
|
@@ -81,12 +71,13 @@ declare module "@alepha/core" {
|
|
|
81
71
|
export const AlephaReact = $module({
|
|
82
72
|
name: "alepha.react",
|
|
83
73
|
descriptors: [$page],
|
|
84
|
-
services: [ReactServerProvider,
|
|
74
|
+
services: [ReactServerProvider, ReactPageProvider, ReactRouter],
|
|
85
75
|
register: (alepha) =>
|
|
86
76
|
alepha
|
|
87
77
|
.with(AlephaServer)
|
|
88
78
|
.with(AlephaServerCache)
|
|
89
79
|
.with(AlephaServerLinks)
|
|
90
80
|
.with(ReactServerProvider)
|
|
91
|
-
.with(
|
|
81
|
+
.with(ReactPageProvider)
|
|
82
|
+
.with(ReactRouter),
|
|
92
83
|
});
|
|
@@ -1,74 +1,124 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
$env,
|
|
3
|
+
$hook,
|
|
4
|
+
$inject,
|
|
5
|
+
Alepha,
|
|
6
|
+
type State,
|
|
7
|
+
type Static,
|
|
8
|
+
t,
|
|
9
|
+
} from "@alepha/core";
|
|
10
|
+
import { DateTimeProvider } from "@alepha/datetime";
|
|
11
|
+
import { $logger } from "@alepha/logger";
|
|
3
12
|
import { LinkProvider } from "@alepha/server-links";
|
|
4
|
-
import type
|
|
5
|
-
import {
|
|
13
|
+
import { createRoot, hydrateRoot, type Root } from "react-dom/client";
|
|
14
|
+
import { ReactBrowserRouterProvider } from "./ReactBrowserRouterProvider.ts";
|
|
6
15
|
import type {
|
|
7
16
|
PreviousLayerData,
|
|
8
|
-
|
|
9
|
-
RouterState,
|
|
17
|
+
ReactRouterState,
|
|
10
18
|
TransitionOptions,
|
|
11
|
-
} from "./
|
|
19
|
+
} from "./ReactPageProvider.ts";
|
|
20
|
+
|
|
21
|
+
const envSchema = t.object({
|
|
22
|
+
REACT_ROOT_ID: t.string({ default: "root" }),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
declare module "@alepha/core" {
|
|
26
|
+
interface Env extends Partial<Static<typeof envSchema>> {}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ReactBrowserRendererOptions {
|
|
30
|
+
scrollRestoration?: "top" | "manual";
|
|
31
|
+
}
|
|
12
32
|
|
|
13
33
|
export class ReactBrowserProvider {
|
|
34
|
+
protected readonly env = $env(envSchema);
|
|
14
35
|
protected readonly log = $logger();
|
|
15
36
|
protected readonly client = $inject(LinkProvider);
|
|
16
37
|
protected readonly alepha = $inject(Alepha);
|
|
17
|
-
protected readonly router = $inject(
|
|
18
|
-
protected
|
|
38
|
+
protected readonly router = $inject(ReactBrowserRouterProvider);
|
|
39
|
+
protected readonly dateTimeProvider = $inject(DateTimeProvider);
|
|
40
|
+
protected root?: Root;
|
|
41
|
+
|
|
42
|
+
public options: ReactBrowserRendererOptions = {
|
|
43
|
+
scrollRestoration: "top",
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
protected getRootElement() {
|
|
47
|
+
const root = this.document.getElementById(this.env.REACT_ROOT_ID);
|
|
48
|
+
if (root) {
|
|
49
|
+
return root;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const div = this.document.createElement("div");
|
|
53
|
+
div.id = this.env.REACT_ROOT_ID;
|
|
54
|
+
|
|
55
|
+
this.document.body.prepend(div);
|
|
56
|
+
|
|
57
|
+
return div;
|
|
58
|
+
}
|
|
19
59
|
|
|
20
60
|
public transitioning?: {
|
|
21
61
|
to: string;
|
|
62
|
+
from?: string;
|
|
22
63
|
};
|
|
23
64
|
|
|
24
|
-
public state:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
search: "",
|
|
28
|
-
};
|
|
65
|
+
public get state(): ReactRouterState {
|
|
66
|
+
return this.alepha.state("react.router.state")!;
|
|
67
|
+
}
|
|
29
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Accessor for Document DOM API.
|
|
71
|
+
*/
|
|
30
72
|
public get document() {
|
|
31
73
|
return window.document;
|
|
32
74
|
}
|
|
33
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Accessor for History DOM API.
|
|
78
|
+
*/
|
|
34
79
|
public get history() {
|
|
35
80
|
return window.history;
|
|
36
81
|
}
|
|
37
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Accessor for Location DOM API.
|
|
85
|
+
*/
|
|
38
86
|
public get location() {
|
|
39
87
|
return window.location;
|
|
40
88
|
}
|
|
41
89
|
|
|
42
|
-
public get
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
url = url.replace(import.meta.env?.BASE_URL, "");
|
|
47
|
-
if (!url.startsWith("/")) {
|
|
48
|
-
url = `/${url}`;
|
|
49
|
-
}
|
|
90
|
+
public get base() {
|
|
91
|
+
const base = import.meta.env?.BASE_URL;
|
|
92
|
+
if (!base || base === "/") {
|
|
93
|
+
return "";
|
|
50
94
|
}
|
|
51
95
|
|
|
52
|
-
return
|
|
96
|
+
return base;
|
|
53
97
|
}
|
|
54
98
|
|
|
55
|
-
public
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
path = (import.meta.env?.BASE_URL + path).replaceAll("//", "/");
|
|
99
|
+
public get url(): string {
|
|
100
|
+
const url = this.location.pathname + this.location.search;
|
|
101
|
+
if (this.base) {
|
|
102
|
+
return url.replace(this.base, "");
|
|
60
103
|
}
|
|
104
|
+
return url;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public pushState(path: string, replace?: boolean) {
|
|
108
|
+
const url = this.base + path;
|
|
61
109
|
|
|
62
110
|
if (replace) {
|
|
63
|
-
this.history.replaceState({}, "",
|
|
111
|
+
this.history.replaceState({}, "", url);
|
|
64
112
|
} else {
|
|
65
|
-
this.history.pushState({}, "",
|
|
113
|
+
this.history.pushState({}, "", url);
|
|
66
114
|
}
|
|
67
115
|
}
|
|
68
116
|
|
|
69
117
|
public async invalidate(props?: Record<string, any>) {
|
|
70
118
|
const previous: PreviousLayerData[] = [];
|
|
71
119
|
|
|
120
|
+
this.log.trace("Invalidating layers");
|
|
121
|
+
|
|
72
122
|
if (props) {
|
|
73
123
|
const [key] = Object.keys(props);
|
|
74
124
|
const value = props[key];
|
|
@@ -92,48 +142,57 @@ export class ReactBrowserProvider {
|
|
|
92
142
|
}
|
|
93
143
|
|
|
94
144
|
public async go(url: string, options: RouterGoOptions = {}): Promise<void> {
|
|
95
|
-
|
|
145
|
+
this.log.trace(`Going to ${url}`, {
|
|
96
146
|
url,
|
|
147
|
+
options,
|
|
97
148
|
});
|
|
98
149
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
150
|
+
await this.render({
|
|
151
|
+
url,
|
|
152
|
+
previous: options.force ? [] : this.state.layers,
|
|
153
|
+
});
|
|
105
154
|
|
|
106
|
-
|
|
107
|
-
|
|
155
|
+
// when redirecting in browser
|
|
156
|
+
if (this.state.url.pathname + this.state.url.search !== url) {
|
|
157
|
+
this.pushState(this.state.url.pathname + this.state.url.search);
|
|
108
158
|
return;
|
|
109
159
|
}
|
|
110
160
|
|
|
111
|
-
this.pushState(url);
|
|
161
|
+
this.pushState(url, options.replace);
|
|
112
162
|
}
|
|
113
163
|
|
|
114
164
|
protected async render(
|
|
115
165
|
options: { url?: string; previous?: PreviousLayerData[] } = {},
|
|
116
|
-
): Promise<
|
|
166
|
+
): Promise<void> {
|
|
117
167
|
const previous = options.previous ?? this.state.layers;
|
|
118
168
|
const url = options.url ?? this.url;
|
|
169
|
+
const start = this.dateTimeProvider.now();
|
|
119
170
|
|
|
120
|
-
this.transitioning = {
|
|
171
|
+
this.transitioning = {
|
|
172
|
+
to: url,
|
|
173
|
+
from: this.state?.url.pathname,
|
|
174
|
+
};
|
|
121
175
|
|
|
122
|
-
|
|
176
|
+
this.log.debug("Transitioning...", {
|
|
177
|
+
to: url,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const redirect = await this.router.transition(
|
|
123
181
|
new URL(`http://localhost${url}`),
|
|
124
|
-
|
|
125
|
-
previous,
|
|
126
|
-
state: this.state,
|
|
127
|
-
},
|
|
182
|
+
previous,
|
|
128
183
|
);
|
|
129
184
|
|
|
130
|
-
if (
|
|
131
|
-
|
|
185
|
+
if (redirect) {
|
|
186
|
+
this.log.info("Redirecting to", {
|
|
187
|
+
redirect,
|
|
188
|
+
});
|
|
189
|
+
return await this.render({ url: redirect });
|
|
132
190
|
}
|
|
133
191
|
|
|
134
|
-
|
|
192
|
+
const ms = this.dateTimeProvider.now().diff(start);
|
|
193
|
+
this.log.info(`Transition OK [${ms}ms]`, this.transitioning);
|
|
135
194
|
|
|
136
|
-
|
|
195
|
+
this.transitioning = undefined;
|
|
137
196
|
}
|
|
138
197
|
|
|
139
198
|
/**
|
|
@@ -151,33 +210,57 @@ export class ReactBrowserProvider {
|
|
|
151
210
|
|
|
152
211
|
// -------------------------------------------------------------------------------------------------------------------
|
|
153
212
|
|
|
213
|
+
protected readonly onTransitionEnd = $hook({
|
|
214
|
+
on: "react:transition:end",
|
|
215
|
+
handler: () => {
|
|
216
|
+
if (
|
|
217
|
+
this.options.scrollRestoration === "top" &&
|
|
218
|
+
typeof window !== "undefined"
|
|
219
|
+
) {
|
|
220
|
+
this.log.trace("Restoring scroll position to top");
|
|
221
|
+
window.scrollTo(0, 0);
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
154
226
|
public readonly ready = $hook({
|
|
155
227
|
on: "ready",
|
|
156
228
|
handler: async () => {
|
|
157
229
|
const hydration = this.getHydrationState();
|
|
158
230
|
const previous = hydration?.layers ?? [];
|
|
159
231
|
|
|
160
|
-
if (hydration
|
|
161
|
-
|
|
162
|
-
|
|
232
|
+
if (hydration) {
|
|
233
|
+
// low budget, but works for now
|
|
234
|
+
for (const [key, value] of Object.entries(hydration)) {
|
|
235
|
+
if (key !== "layers") {
|
|
236
|
+
this.alepha.state(key as keyof State, value);
|
|
237
|
+
}
|
|
163
238
|
}
|
|
164
239
|
}
|
|
165
240
|
|
|
166
|
-
|
|
241
|
+
await this.render({ previous });
|
|
167
242
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
243
|
+
const element = this.router.root(this.state);
|
|
244
|
+
if (hydration?.layers) {
|
|
245
|
+
this.root = hydrateRoot(this.getRootElement(), element);
|
|
246
|
+
this.log.info("Hydrated root element");
|
|
247
|
+
} else {
|
|
248
|
+
this.root ??= createRoot(this.getRootElement());
|
|
249
|
+
this.root.render(element);
|
|
250
|
+
this.log.info("Created root element");
|
|
251
|
+
}
|
|
173
252
|
|
|
174
253
|
window.addEventListener("popstate", () => {
|
|
175
|
-
// when you update silently
|
|
254
|
+
// when you update silently queryParams or hash, skip rendering
|
|
176
255
|
// if you want to force a rendering, use #go()
|
|
177
|
-
if (this.state.pathname === this.
|
|
256
|
+
if (this.base + this.state.url.pathname === this.location.pathname) {
|
|
178
257
|
return;
|
|
179
258
|
}
|
|
180
259
|
|
|
260
|
+
this.log.debug("Popstate event triggered - rendering new state", {
|
|
261
|
+
url: this.location.pathname + this.location.search,
|
|
262
|
+
});
|
|
263
|
+
|
|
181
264
|
this.render();
|
|
182
265
|
});
|
|
183
266
|
},
|
|
@@ -190,9 +273,16 @@ export interface RouterGoOptions {
|
|
|
190
273
|
replace?: boolean;
|
|
191
274
|
match?: TransitionOptions;
|
|
192
275
|
params?: Record<string, string>;
|
|
276
|
+
query?: Record<string, string>;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Recreate the whole page, ignoring the current state.
|
|
280
|
+
*/
|
|
281
|
+
force?: boolean;
|
|
193
282
|
}
|
|
194
283
|
|
|
195
|
-
export
|
|
284
|
+
export type ReactHydrationState = {
|
|
196
285
|
layers?: Array<PreviousLayerData>;
|
|
197
|
-
|
|
198
|
-
|
|
286
|
+
} & {
|
|
287
|
+
[key: string]: any;
|
|
288
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { $hook, $inject, Alepha } from "@alepha/core";
|
|
2
|
+
import { $logger } from "@alepha/logger";
|
|
3
|
+
import { type Route, RouterProvider } from "@alepha/router";
|
|
4
|
+
import { createElement, type ReactNode } from "react";
|
|
5
|
+
import NotFoundPage from "../components/NotFound.tsx";
|
|
6
|
+
import {
|
|
7
|
+
isPageRoute,
|
|
8
|
+
type PageRoute,
|
|
9
|
+
type PageRouteEntry,
|
|
10
|
+
type PreviousLayerData,
|
|
11
|
+
ReactPageProvider,
|
|
12
|
+
type ReactRouterState,
|
|
13
|
+
} from "./ReactPageProvider.ts";
|
|
14
|
+
|
|
15
|
+
export interface BrowserRoute extends Route {
|
|
16
|
+
page: PageRoute;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
|
|
20
|
+
protected readonly log = $logger();
|
|
21
|
+
protected readonly alepha = $inject(Alepha);
|
|
22
|
+
protected readonly pageApi = $inject(ReactPageProvider);
|
|
23
|
+
|
|
24
|
+
public add(entry: PageRouteEntry) {
|
|
25
|
+
this.pageApi.add(entry);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
protected readonly configure = $hook({
|
|
29
|
+
on: "configure",
|
|
30
|
+
handler: async () => {
|
|
31
|
+
for (const page of this.pageApi.getPages()) {
|
|
32
|
+
// mount only if a view is provided
|
|
33
|
+
if (page.component || page.lazy) {
|
|
34
|
+
this.push({
|
|
35
|
+
path: page.match,
|
|
36
|
+
page,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
public async transition(
|
|
44
|
+
url: URL,
|
|
45
|
+
previous: PreviousLayerData[] = [],
|
|
46
|
+
): Promise<string | void> {
|
|
47
|
+
const { pathname, search } = url;
|
|
48
|
+
|
|
49
|
+
const entry: Partial<ReactRouterState> = {
|
|
50
|
+
url,
|
|
51
|
+
query: {},
|
|
52
|
+
params: {},
|
|
53
|
+
layers: [],
|
|
54
|
+
onError: () => null,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const state = entry as ReactRouterState;
|
|
58
|
+
|
|
59
|
+
await this.alepha.emit("react:transition:begin", { state });
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const { route, params } = this.match(pathname);
|
|
63
|
+
|
|
64
|
+
const query: Record<string, string> = {};
|
|
65
|
+
if (search) {
|
|
66
|
+
for (const [key, value] of new URLSearchParams(search).entries()) {
|
|
67
|
+
query[key] = String(value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
state.query = query;
|
|
72
|
+
state.params = params ?? {};
|
|
73
|
+
|
|
74
|
+
if (isPageRoute(route)) {
|
|
75
|
+
const { redirect } = await this.pageApi.createLayers(
|
|
76
|
+
route.page,
|
|
77
|
+
state,
|
|
78
|
+
previous,
|
|
79
|
+
);
|
|
80
|
+
if (redirect) {
|
|
81
|
+
return redirect;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (state.layers.length === 0) {
|
|
86
|
+
state.layers.push({
|
|
87
|
+
name: "not-found",
|
|
88
|
+
element: createElement(NotFoundPage),
|
|
89
|
+
index: 0,
|
|
90
|
+
path: "/",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await this.alepha.emit("react:transition:success", { state });
|
|
95
|
+
} catch (e) {
|
|
96
|
+
this.log.error("Transition has failed", e);
|
|
97
|
+
state.layers = [
|
|
98
|
+
{
|
|
99
|
+
name: "error",
|
|
100
|
+
element: this.pageApi.renderError(e as Error),
|
|
101
|
+
index: 0,
|
|
102
|
+
path: "/",
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
await this.alepha.emit("react:transition:error", {
|
|
107
|
+
error: e as Error,
|
|
108
|
+
state,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// [feature]: local hook for leaving a page
|
|
113
|
+
if (previous) {
|
|
114
|
+
for (let i = 0; i < previous.length; i++) {
|
|
115
|
+
const layer = previous[i];
|
|
116
|
+
if (state.layers[i]?.name !== layer.name) {
|
|
117
|
+
this.pageApi.page(layer.name)?.onLeave?.();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await this.alepha.emit("react:transition:end", {
|
|
123
|
+
state,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
this.alepha.state("react.router.state", state);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
public root(state: ReactRouterState): ReactNode {
|
|
130
|
+
return this.pageApi.root(state);
|
|
131
|
+
}
|
|
132
|
+
}
|