@alepha/react 0.9.4 → 0.9.5
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 +18 -6
- package/dist/index.browser.js +196 -77
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +203 -80
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +240 -195
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +219 -174
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +204 -81
- package/dist/index.js.map +1 -1
- package/package.json +14 -12
- package/src/components/Link.tsx +2 -5
- package/src/components/NestedView.tsx +159 -16
- package/src/descriptors/$page.ts +84 -1
- package/src/hooks/useActive.ts +0 -1
- package/src/hooks/useRouterEvents.ts +27 -19
- package/src/index.browser.ts +3 -0
- package/src/index.ts +6 -1
- package/src/providers/ReactBrowserProvider.ts +19 -14
- package/src/providers/ReactBrowserRendererProvider.ts +22 -0
- package/src/providers/ReactBrowserRouterProvider.ts +8 -3
- package/src/providers/ReactPageProvider.ts +46 -1
- package/src/providers/ReactServerProvider.ts +22 -3
- package/src/services/ReactRouter.ts +5 -8
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alepha/react",
|
|
3
3
|
"description": "Build server-side rendered (SSR) or single-page React applications.",
|
|
4
|
-
"version": "0.9.
|
|
4
|
+
"version": "0.9.5",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=22.0.0"
|
|
@@ -17,21 +17,22 @@
|
|
|
17
17
|
"src"
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@alepha/core": "0.9.
|
|
21
|
-
"@alepha/datetime": "0.9.
|
|
22
|
-
"@alepha/logger": "0.9.
|
|
23
|
-
"@alepha/router": "0.9.
|
|
24
|
-
"@alepha/server": "0.9.
|
|
25
|
-
"@alepha/server-cache": "0.9.
|
|
26
|
-
"@alepha/server-links": "0.9.
|
|
27
|
-
"@alepha/server-static": "0.9.
|
|
20
|
+
"@alepha/core": "0.9.5",
|
|
21
|
+
"@alepha/datetime": "0.9.5",
|
|
22
|
+
"@alepha/logger": "0.9.5",
|
|
23
|
+
"@alepha/router": "0.9.5",
|
|
24
|
+
"@alepha/server": "0.9.5",
|
|
25
|
+
"@alepha/server-cache": "0.9.5",
|
|
26
|
+
"@alepha/server-links": "0.9.5",
|
|
27
|
+
"@alepha/server-static": "0.9.5",
|
|
28
28
|
"react-dom": "^19.1.1"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
-
"@
|
|
32
|
-
"@types/react
|
|
31
|
+
"@biomejs/biome": "^2.2.4",
|
|
32
|
+
"@types/react": "^19.1.13",
|
|
33
|
+
"@types/react-dom": "^19.1.9",
|
|
33
34
|
"react": "^19.1.1",
|
|
34
|
-
"tsdown": "^0.
|
|
35
|
+
"tsdown": "^0.15.1",
|
|
35
36
|
"typescript": "^5.9.2",
|
|
36
37
|
"vitest": "^3.2.4"
|
|
37
38
|
},
|
|
@@ -41,6 +42,7 @@
|
|
|
41
42
|
"scripts": {
|
|
42
43
|
"check": "tsc",
|
|
43
44
|
"test": "vitest run",
|
|
45
|
+
"lint": "biome check --write --unsafe",
|
|
44
46
|
"build": "tsdown -c ../../tsdown.config.ts"
|
|
45
47
|
},
|
|
46
48
|
"homepage": "https://github.com/feunard/alepha",
|
package/src/components/Link.tsx
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
import type React from "react";
|
|
2
1
|
import type { AnchorHTMLAttributes } from "react";
|
|
3
2
|
import { useRouter } from "../hooks/useRouter.ts";
|
|
4
3
|
|
|
5
4
|
export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
|
|
6
|
-
|
|
7
|
-
children?: React.ReactNode;
|
|
5
|
+
href: string;
|
|
8
6
|
}
|
|
9
7
|
|
|
10
8
|
const Link = (props: LinkProps) => {
|
|
11
9
|
const router = useRouter();
|
|
12
|
-
const { to, ...anchorProps } = props;
|
|
13
10
|
|
|
14
11
|
return (
|
|
15
|
-
<a {...router.anchor(
|
|
12
|
+
<a {...props} {...router.anchor(props.href)}>
|
|
16
13
|
{props.children}
|
|
17
14
|
</a>
|
|
18
15
|
);
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import type
|
|
2
|
-
import { useContext, useState } from "react";
|
|
1
|
+
import { memo, type ReactNode, use, useRef, useState } from "react";
|
|
3
2
|
import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
|
|
3
|
+
import type { PageAnimation } from "../descriptors/$page.ts";
|
|
4
4
|
import { Redirection } from "../errors/Redirection.ts";
|
|
5
|
-
import { useAlepha } from "../hooks/useAlepha.ts";
|
|
6
5
|
import { useRouterEvents } from "../hooks/useRouterEvents.ts";
|
|
6
|
+
import { useRouterState } from "../hooks/useRouterState.ts";
|
|
7
|
+
import type { ReactRouterState } from "../providers/ReactPageProvider.ts";
|
|
7
8
|
import ErrorBoundary from "./ErrorBoundary.tsx";
|
|
8
9
|
|
|
9
10
|
export interface NestedViewProps {
|
|
10
11
|
children?: ReactNode;
|
|
12
|
+
errorBoundary?: false | ((error: Error) => ReactNode);
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -17,7 +19,7 @@ export interface NestedViewProps {
|
|
|
17
19
|
*
|
|
18
20
|
* @example
|
|
19
21
|
* ```tsx
|
|
20
|
-
* import { NestedView } from "
|
|
22
|
+
* import { NestedView } from "alepha/react";
|
|
21
23
|
*
|
|
22
24
|
* class App {
|
|
23
25
|
* parent = $page({
|
|
@@ -32,30 +34,115 @@ export interface NestedViewProps {
|
|
|
32
34
|
* ```
|
|
33
35
|
*/
|
|
34
36
|
const NestedView = (props: NestedViewProps) => {
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const alepha = useAlepha();
|
|
38
|
-
const state = alepha.state("react.router.state");
|
|
39
|
-
if (!state) {
|
|
40
|
-
throw new Error("<NestedView/> must be used inside a RouterLayerContext.");
|
|
41
|
-
}
|
|
37
|
+
const index = use(RouterLayerContext)?.index ?? 0;
|
|
38
|
+
const state = useRouterState();
|
|
42
39
|
|
|
43
40
|
const [view, setView] = useState<ReactNode | undefined>(
|
|
44
41
|
state.layers[index]?.element,
|
|
45
42
|
);
|
|
46
43
|
|
|
44
|
+
const [animation, setAnimation] = useState("");
|
|
45
|
+
const animationExitDuration = useRef<number>(0);
|
|
46
|
+
const animationExitNow = useRef<number>(0);
|
|
47
|
+
|
|
47
48
|
useRouterEvents(
|
|
48
49
|
{
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
onBegin: async ({ previous, state }) => {
|
|
51
|
+
// --------- Animations Begin ---------
|
|
52
|
+
const layer = previous.layers[index];
|
|
53
|
+
if (`${state.url.pathname}/`.startsWith(`${layer?.path}/`)) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const animationExit = parseAnimation(
|
|
58
|
+
layer.route?.animation,
|
|
59
|
+
state,
|
|
60
|
+
"exit",
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (animationExit) {
|
|
64
|
+
const duration = animationExit.duration || 200;
|
|
65
|
+
animationExitNow.current = Date.now();
|
|
66
|
+
animationExitDuration.current = duration;
|
|
67
|
+
setAnimation(animationExit.animation);
|
|
68
|
+
} else {
|
|
69
|
+
animationExitNow.current = 0;
|
|
70
|
+
animationExitDuration.current = 0;
|
|
71
|
+
setAnimation("");
|
|
72
|
+
}
|
|
73
|
+
// --------- Animations End ---------
|
|
74
|
+
},
|
|
75
|
+
onEnd: async ({ state }) => {
|
|
76
|
+
const layer = state.layers[index];
|
|
77
|
+
|
|
78
|
+
// --------- Animations Begin ---------
|
|
79
|
+
if (animationExitNow.current) {
|
|
80
|
+
const duration = animationExitDuration.current;
|
|
81
|
+
const diff = Date.now() - animationExitNow.current;
|
|
82
|
+
if (diff < duration) {
|
|
83
|
+
await new Promise((resolve) =>
|
|
84
|
+
setTimeout(resolve, duration - diff),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// --------- Animations End ---------
|
|
89
|
+
|
|
90
|
+
if (!layer?.cache) {
|
|
91
|
+
setView(layer?.element);
|
|
92
|
+
|
|
93
|
+
// --------- Animations Begin ---------
|
|
94
|
+
const animationEnter = parseAnimation(
|
|
95
|
+
layer?.route?.animation,
|
|
96
|
+
state,
|
|
97
|
+
"enter",
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (animationEnter) {
|
|
101
|
+
setAnimation(animationEnter.animation);
|
|
102
|
+
} else {
|
|
103
|
+
setAnimation("");
|
|
104
|
+
}
|
|
105
|
+
// --------- Animations End ---------
|
|
52
106
|
}
|
|
53
107
|
},
|
|
54
108
|
},
|
|
55
109
|
[],
|
|
56
110
|
);
|
|
57
111
|
|
|
58
|
-
|
|
112
|
+
let element = view ?? props.children ?? null;
|
|
113
|
+
|
|
114
|
+
// --------- Animations Begin ---------
|
|
115
|
+
if (animation) {
|
|
116
|
+
element = (
|
|
117
|
+
<div
|
|
118
|
+
style={{
|
|
119
|
+
display: "flex",
|
|
120
|
+
flex: 1,
|
|
121
|
+
height: "100%",
|
|
122
|
+
width: "100%",
|
|
123
|
+
position: "relative",
|
|
124
|
+
overflow: "hidden",
|
|
125
|
+
}}
|
|
126
|
+
>
|
|
127
|
+
<div
|
|
128
|
+
style={{ height: "100%", width: "100%", display: "flex", animation }}
|
|
129
|
+
>
|
|
130
|
+
{element}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
// --------- Animations End ---------
|
|
136
|
+
|
|
137
|
+
if (props.errorBoundary === false) {
|
|
138
|
+
return <>{element}</>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (props.errorBoundary) {
|
|
142
|
+
return (
|
|
143
|
+
<ErrorBoundary fallback={props.errorBoundary}>{element}</ErrorBoundary>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
59
146
|
|
|
60
147
|
return (
|
|
61
148
|
<ErrorBoundary
|
|
@@ -72,4 +159,60 @@ const NestedView = (props: NestedViewProps) => {
|
|
|
72
159
|
);
|
|
73
160
|
};
|
|
74
161
|
|
|
75
|
-
export default NestedView;
|
|
162
|
+
export default memo(NestedView);
|
|
163
|
+
|
|
164
|
+
function parseAnimation(
|
|
165
|
+
animationLike: PageAnimation | undefined,
|
|
166
|
+
state: ReactRouterState,
|
|
167
|
+
type: "enter" | "exit" = "enter",
|
|
168
|
+
):
|
|
169
|
+
| {
|
|
170
|
+
duration: number;
|
|
171
|
+
animation: string;
|
|
172
|
+
}
|
|
173
|
+
| undefined {
|
|
174
|
+
if (!animationLike) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const DEFAULT_DURATION = 300;
|
|
179
|
+
|
|
180
|
+
const animation =
|
|
181
|
+
typeof animationLike === "function" ? animationLike(state) : animationLike;
|
|
182
|
+
|
|
183
|
+
if (typeof animation === "string") {
|
|
184
|
+
if (type === "exit") {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
duration: DEFAULT_DURATION,
|
|
189
|
+
animation: `${DEFAULT_DURATION}ms ${animation}`,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (typeof animation === "object") {
|
|
194
|
+
const anim = animation[type];
|
|
195
|
+
const duration =
|
|
196
|
+
typeof anim === "object"
|
|
197
|
+
? (anim.duration ?? DEFAULT_DURATION)
|
|
198
|
+
: DEFAULT_DURATION;
|
|
199
|
+
const name = typeof anim === "object" ? anim.name : anim;
|
|
200
|
+
|
|
201
|
+
if (type === "exit") {
|
|
202
|
+
const timing = typeof anim === "object" ? (anim.timing ?? "") : "";
|
|
203
|
+
return {
|
|
204
|
+
duration,
|
|
205
|
+
animation: `${duration}ms ${timing} ${name}`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const timing = typeof anim === "object" ? (anim.timing ?? "") : "";
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
duration,
|
|
213
|
+
animation: `${duration}ms ${timing} ${name}`,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
package/src/descriptors/$page.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
AlephaError,
|
|
2
3
|
type Async,
|
|
3
4
|
createDescriptor,
|
|
4
5
|
Descriptor,
|
|
@@ -179,6 +180,50 @@ export interface PageDescriptorOptions<
|
|
|
179
180
|
* Called when user leaves the page. (browser only)
|
|
180
181
|
*/
|
|
181
182
|
onLeave?: () => void;
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @experimental
|
|
186
|
+
*
|
|
187
|
+
* Add a css animation when the page is loaded or unloaded.
|
|
188
|
+
* It uses CSS animations, so you need to define the keyframes in your CSS.
|
|
189
|
+
*
|
|
190
|
+
* @example Simple animation name
|
|
191
|
+
* ```ts
|
|
192
|
+
* animation: "fadeIn"
|
|
193
|
+
* ```
|
|
194
|
+
*
|
|
195
|
+
* CSS example:
|
|
196
|
+
* ```css
|
|
197
|
+
* @keyframes fadeIn {
|
|
198
|
+
* from { opacity: 0; }
|
|
199
|
+
* to { opacity: 1; }
|
|
200
|
+
* }
|
|
201
|
+
* ```
|
|
202
|
+
*
|
|
203
|
+
* @example Detailed animation
|
|
204
|
+
* ```ts
|
|
205
|
+
* animation: {
|
|
206
|
+
* enter: { name: "fadeIn", duration: 300 },
|
|
207
|
+
* exit: { name: "fadeOut", duration: 200, timing: "ease-in-out" },
|
|
208
|
+
* }
|
|
209
|
+
* ```
|
|
210
|
+
*
|
|
211
|
+
* @example Only exit animation
|
|
212
|
+
* ```ts
|
|
213
|
+
* animation: {
|
|
214
|
+
* exit: "fadeOut"
|
|
215
|
+
* }
|
|
216
|
+
* ```
|
|
217
|
+
*
|
|
218
|
+
* @example With custom timing function
|
|
219
|
+
* ```ts
|
|
220
|
+
* animation: {
|
|
221
|
+
* enter: { name: "fadeIn", duration: 300, timing: "cubic-bezier(0.4, 0, 0.2, 1)" },
|
|
222
|
+
* exit: { name: "fadeOut", duration: 200, timing: "ease-in-out" },
|
|
223
|
+
* }
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
animation?: PageAnimation;
|
|
182
227
|
}
|
|
183
228
|
|
|
184
229
|
export type ErrorHandler = (
|
|
@@ -211,7 +256,18 @@ export class PageDescriptor<
|
|
|
211
256
|
public async render(
|
|
212
257
|
options?: PageDescriptorRenderOptions,
|
|
213
258
|
): Promise<PageDescriptorRenderResult> {
|
|
214
|
-
throw new
|
|
259
|
+
throw new AlephaError(
|
|
260
|
+
"render() method is not implemented in this environment",
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
public async fetch(options?: PageDescriptorRenderOptions): Promise<{
|
|
265
|
+
html: string;
|
|
266
|
+
response: Response;
|
|
267
|
+
}> {
|
|
268
|
+
throw new AlephaError(
|
|
269
|
+
"fetch() method is not implemented in this environment",
|
|
270
|
+
);
|
|
215
271
|
}
|
|
216
272
|
|
|
217
273
|
public match(url: string): boolean {
|
|
@@ -241,6 +297,13 @@ export type TPropsParentDefault = {};
|
|
|
241
297
|
export interface PageDescriptorRenderOptions {
|
|
242
298
|
params?: Record<string, string>;
|
|
243
299
|
query?: Record<string, string>;
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* If true, the HTML layout will be included in the response.
|
|
303
|
+
* If false, only the page content will be returned.
|
|
304
|
+
*
|
|
305
|
+
* @default true
|
|
306
|
+
*/
|
|
244
307
|
html?: boolean;
|
|
245
308
|
hydration?: boolean;
|
|
246
309
|
}
|
|
@@ -248,6 +311,7 @@ export interface PageDescriptorRenderOptions {
|
|
|
248
311
|
export interface PageDescriptorRenderResult {
|
|
249
312
|
html: string;
|
|
250
313
|
state: ReactRouterState;
|
|
314
|
+
redirect?: string;
|
|
251
315
|
}
|
|
252
316
|
|
|
253
317
|
export interface PageRequestConfig<
|
|
@@ -268,3 +332,22 @@ export type PageResolve<
|
|
|
268
332
|
> = PageRequestConfig<TConfig> &
|
|
269
333
|
TPropsParent &
|
|
270
334
|
Omit<ReactRouterState, "layers" | "onError">;
|
|
335
|
+
|
|
336
|
+
export type PageAnimation =
|
|
337
|
+
| PageAnimationObject
|
|
338
|
+
| ((state: ReactRouterState) => PageAnimationObject | undefined);
|
|
339
|
+
|
|
340
|
+
type PageAnimationObject =
|
|
341
|
+
| CssAnimationName
|
|
342
|
+
| {
|
|
343
|
+
enter?: CssAnimation | CssAnimationName;
|
|
344
|
+
exit?: CssAnimation | CssAnimationName;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
type CssAnimationName = string;
|
|
348
|
+
|
|
349
|
+
type CssAnimation = {
|
|
350
|
+
name: string;
|
|
351
|
+
duration?: number;
|
|
352
|
+
timing?: string;
|
|
353
|
+
};
|
package/src/hooks/useActive.ts
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
|
+
import type { Hooks } from "@alepha/core";
|
|
1
2
|
import { useEffect } from "react";
|
|
2
|
-
import type { ReactRouterState } from "../providers/ReactPageProvider.ts";
|
|
3
3
|
import { useAlepha } from "./useAlepha.ts";
|
|
4
4
|
|
|
5
|
+
type Hook<T extends keyof Hooks> =
|
|
6
|
+
| ((ev: Hooks[T]) => void)
|
|
7
|
+
| {
|
|
8
|
+
priority?: "first" | "last";
|
|
9
|
+
callback: (ev: Hooks[T]) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
5
12
|
/**
|
|
6
13
|
* Subscribe to various router events.
|
|
7
14
|
*/
|
|
8
15
|
export const useRouterEvents = (
|
|
9
16
|
opts: {
|
|
10
|
-
onBegin?:
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
onBegin?: Hook<"react:transition:begin">;
|
|
18
|
+
onError?: Hook<"react:transition:error">;
|
|
19
|
+
onEnd?: Hook<"react:transition:end">;
|
|
20
|
+
onSuccess?: Hook<"react:transition:success">;
|
|
13
21
|
} = {},
|
|
14
22
|
deps: any[] = [],
|
|
15
23
|
) => {
|
|
@@ -20,33 +28,33 @@ export const useRouterEvents = (
|
|
|
20
28
|
return;
|
|
21
29
|
}
|
|
22
30
|
|
|
31
|
+
const cb = <T extends keyof Hooks>(callback: Hook<T>) => {
|
|
32
|
+
if (typeof callback === "function") {
|
|
33
|
+
return { callback };
|
|
34
|
+
}
|
|
35
|
+
return callback;
|
|
36
|
+
};
|
|
37
|
+
|
|
23
38
|
const subs: Function[] = [];
|
|
24
39
|
const onBegin = opts.onBegin;
|
|
25
40
|
const onEnd = opts.onEnd;
|
|
26
41
|
const onError = opts.onError;
|
|
42
|
+
const onSuccess = opts.onSuccess;
|
|
27
43
|
|
|
28
44
|
if (onBegin) {
|
|
29
|
-
subs.push(
|
|
30
|
-
alepha.on("react:transition:begin", {
|
|
31
|
-
callback: onBegin,
|
|
32
|
-
}),
|
|
33
|
-
);
|
|
45
|
+
subs.push(alepha.on("react:transition:begin", cb(onBegin)));
|
|
34
46
|
}
|
|
35
47
|
|
|
36
48
|
if (onEnd) {
|
|
37
|
-
subs.push(
|
|
38
|
-
alepha.on("react:transition:end", {
|
|
39
|
-
callback: onEnd,
|
|
40
|
-
}),
|
|
41
|
-
);
|
|
49
|
+
subs.push(alepha.on("react:transition:end", cb(onEnd)));
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
if (onError) {
|
|
45
|
-
subs.push(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
);
|
|
53
|
+
subs.push(alepha.on("react:transition:error", cb(onError)));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (onSuccess) {
|
|
57
|
+
subs.push(alepha.on("react:transition:success", cb(onSuccess)));
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
return () => {
|
package/src/index.browser.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { AlephaServer } from "@alepha/server";
|
|
|
3
3
|
import { AlephaServerLinks } from "@alepha/server-links";
|
|
4
4
|
import { $page } from "./descriptors/$page.ts";
|
|
5
5
|
import { ReactBrowserProvider } from "./providers/ReactBrowserProvider.ts";
|
|
6
|
+
import { ReactBrowserRendererProvider } from "./providers/ReactBrowserRendererProvider.ts";
|
|
6
7
|
import { ReactBrowserRouterProvider } from "./providers/ReactBrowserRouterProvider.ts";
|
|
7
8
|
import { ReactPageProvider } from "./providers/ReactPageProvider.ts";
|
|
8
9
|
import { ReactRouter } from "./services/ReactRouter.ts";
|
|
@@ -24,6 +25,7 @@ export const AlephaReact = $module({
|
|
|
24
25
|
ReactBrowserRouterProvider,
|
|
25
26
|
ReactBrowserProvider,
|
|
26
27
|
ReactRouter,
|
|
28
|
+
ReactBrowserRendererProvider,
|
|
27
29
|
],
|
|
28
30
|
register: (alepha) =>
|
|
29
31
|
alepha
|
|
@@ -32,5 +34,6 @@ export const AlephaReact = $module({
|
|
|
32
34
|
.with(ReactPageProvider)
|
|
33
35
|
.with(ReactBrowserProvider)
|
|
34
36
|
.with(ReactBrowserRouterProvider)
|
|
37
|
+
.with(ReactBrowserRendererProvider)
|
|
35
38
|
.with(ReactRouter),
|
|
36
39
|
});
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { $module } from "@alepha/core";
|
|
|
2
2
|
import { AlephaServer, type ServerRequest } from "@alepha/server";
|
|
3
3
|
import { AlephaServerCache } from "@alepha/server-cache";
|
|
4
4
|
import { AlephaServerLinks } from "@alepha/server-links";
|
|
5
|
-
import {
|
|
5
|
+
import type { ReactNode } from "react";
|
|
6
|
+
import { $page, type PageAnimation } from "./descriptors/$page.ts";
|
|
6
7
|
import type { ReactHydrationState } from "./providers/ReactBrowserProvider.ts";
|
|
7
8
|
import {
|
|
8
9
|
ReactPageProvider,
|
|
@@ -37,11 +38,15 @@ declare module "@alepha/core" {
|
|
|
37
38
|
};
|
|
38
39
|
// -----------------------------------------------------------------------------------------------------------------
|
|
39
40
|
"react:browser:render": {
|
|
41
|
+
root: HTMLDivElement;
|
|
42
|
+
element: ReactNode;
|
|
40
43
|
state: ReactRouterState;
|
|
41
44
|
hydration?: ReactHydrationState;
|
|
42
45
|
};
|
|
43
46
|
"react:transition:begin": {
|
|
47
|
+
previous: ReactRouterState;
|
|
44
48
|
state: ReactRouterState;
|
|
49
|
+
animation?: PageAnimation;
|
|
45
50
|
};
|
|
46
51
|
"react:transition:success": {
|
|
47
52
|
state: ReactRouterState;
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
import { DateTimeProvider } from "@alepha/datetime";
|
|
11
11
|
import { $logger } from "@alepha/logger";
|
|
12
12
|
import { LinkProvider } from "@alepha/server-links";
|
|
13
|
-
import { createRoot, hydrateRoot, type Root } from "react-dom/client";
|
|
14
13
|
import { ReactBrowserRouterProvider } from "./ReactBrowserRouterProvider.ts";
|
|
15
14
|
import type {
|
|
16
15
|
PreviousLayerData,
|
|
@@ -37,7 +36,6 @@ export class ReactBrowserProvider {
|
|
|
37
36
|
protected readonly alepha = $inject(Alepha);
|
|
38
37
|
protected readonly router = $inject(ReactBrowserRouterProvider);
|
|
39
38
|
protected readonly dateTimeProvider = $inject(DateTimeProvider);
|
|
40
|
-
protected root?: Root;
|
|
41
39
|
|
|
42
40
|
public options: ReactBrowserRendererOptions = {
|
|
43
41
|
scrollRestoration: "top",
|
|
@@ -150,6 +148,7 @@ export class ReactBrowserProvider {
|
|
|
150
148
|
await this.render({
|
|
151
149
|
url,
|
|
152
150
|
previous: options.force ? [] : this.state.layers,
|
|
151
|
+
meta: options.meta,
|
|
153
152
|
});
|
|
154
153
|
|
|
155
154
|
// when redirecting in browser
|
|
@@ -161,9 +160,7 @@ export class ReactBrowserProvider {
|
|
|
161
160
|
this.pushState(url, options.replace);
|
|
162
161
|
}
|
|
163
162
|
|
|
164
|
-
protected async render(
|
|
165
|
-
options: { url?: string; previous?: PreviousLayerData[] } = {},
|
|
166
|
-
): Promise<void> {
|
|
163
|
+
protected async render(options: RouterRenderOptions = {}): Promise<void> {
|
|
167
164
|
const previous = options.previous ?? this.state.layers;
|
|
168
165
|
const url = options.url ?? this.url;
|
|
169
166
|
const start = this.dateTimeProvider.now();
|
|
@@ -180,6 +177,7 @@ export class ReactBrowserProvider {
|
|
|
180
177
|
const redirect = await this.router.transition(
|
|
181
178
|
new URL(`http://localhost${url}`),
|
|
182
179
|
previous,
|
|
180
|
+
options.meta,
|
|
183
181
|
);
|
|
184
182
|
|
|
185
183
|
if (redirect) {
|
|
@@ -215,7 +213,8 @@ export class ReactBrowserProvider {
|
|
|
215
213
|
handler: () => {
|
|
216
214
|
if (
|
|
217
215
|
this.options.scrollRestoration === "top" &&
|
|
218
|
-
typeof window !== "undefined"
|
|
216
|
+
typeof window !== "undefined" &&
|
|
217
|
+
!this.alepha.isTest()
|
|
219
218
|
) {
|
|
220
219
|
this.log.trace("Restoring scroll position to top");
|
|
221
220
|
window.scrollTo(0, 0);
|
|
@@ -241,14 +240,13 @@ export class ReactBrowserProvider {
|
|
|
241
240
|
await this.render({ previous });
|
|
242
241
|
|
|
243
242
|
const element = this.router.root(this.state);
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
this.
|
|
250
|
-
|
|
251
|
-
}
|
|
243
|
+
|
|
244
|
+
await this.alepha.emit("react:browser:render", {
|
|
245
|
+
element,
|
|
246
|
+
root: this.getRootElement(),
|
|
247
|
+
hydration,
|
|
248
|
+
state: this.state,
|
|
249
|
+
});
|
|
252
250
|
|
|
253
251
|
window.addEventListener("popstate", () => {
|
|
254
252
|
// when you update silently queryParams or hash, skip rendering
|
|
@@ -274,6 +272,7 @@ export interface RouterGoOptions {
|
|
|
274
272
|
match?: TransitionOptions;
|
|
275
273
|
params?: Record<string, string>;
|
|
276
274
|
query?: Record<string, string>;
|
|
275
|
+
meta?: Record<string, any>;
|
|
277
276
|
|
|
278
277
|
/**
|
|
279
278
|
* Recreate the whole page, ignoring the current state.
|
|
@@ -286,3 +285,9 @@ export type ReactHydrationState = {
|
|
|
286
285
|
} & {
|
|
287
286
|
[key: string]: any;
|
|
288
287
|
};
|
|
288
|
+
|
|
289
|
+
export interface RouterRenderOptions {
|
|
290
|
+
url?: string;
|
|
291
|
+
previous?: PreviousLayerData[];
|
|
292
|
+
meta?: Record<string, any>;
|
|
293
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { $hook } from "@alepha/core";
|
|
2
|
+
import { $logger } from "@alepha/logger";
|
|
3
|
+
import { createRoot, hydrateRoot, type Root } from "react-dom/client";
|
|
4
|
+
|
|
5
|
+
export class ReactBrowserRendererProvider {
|
|
6
|
+
protected readonly log = $logger();
|
|
7
|
+
protected root?: Root;
|
|
8
|
+
|
|
9
|
+
protected readonly onBrowserRender = $hook({
|
|
10
|
+
on: "react:browser:render",
|
|
11
|
+
handler: async ({ hydration, root, element }) => {
|
|
12
|
+
if (hydration?.layers) {
|
|
13
|
+
this.root = hydrateRoot(root, element);
|
|
14
|
+
this.log.info("Hydrated root element");
|
|
15
|
+
} else {
|
|
16
|
+
this.root ??= createRoot(root);
|
|
17
|
+
this.root.render(element);
|
|
18
|
+
this.log.info("Created root element");
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -43,6 +43,7 @@ export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
|
|
|
43
43
|
public async transition(
|
|
44
44
|
url: URL,
|
|
45
45
|
previous: PreviousLayerData[] = [],
|
|
46
|
+
meta = {},
|
|
46
47
|
): Promise<string | void> {
|
|
47
48
|
const { pathname, search } = url;
|
|
48
49
|
|
|
@@ -52,11 +53,15 @@ export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
|
|
|
52
53
|
params: {},
|
|
53
54
|
layers: [],
|
|
54
55
|
onError: () => null,
|
|
56
|
+
meta,
|
|
55
57
|
};
|
|
56
58
|
|
|
57
59
|
const state = entry as ReactRouterState;
|
|
58
60
|
|
|
59
|
-
await this.alepha.emit("react:transition:begin", {
|
|
61
|
+
await this.alepha.emit("react:transition:begin", {
|
|
62
|
+
previous: this.alepha.state("react.router.state")!,
|
|
63
|
+
state,
|
|
64
|
+
});
|
|
60
65
|
|
|
61
66
|
try {
|
|
62
67
|
const { route, params } = this.match(pathname);
|
|
@@ -119,11 +124,11 @@ export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
|
|
|
119
124
|
}
|
|
120
125
|
}
|
|
121
126
|
|
|
127
|
+
this.alepha.state("react.router.state", state);
|
|
128
|
+
|
|
122
129
|
await this.alepha.emit("react:transition:end", {
|
|
123
130
|
state,
|
|
124
131
|
});
|
|
125
|
-
|
|
126
|
-
this.alepha.state("react.router.state", state);
|
|
127
132
|
}
|
|
128
133
|
|
|
129
134
|
public root(state: ReactRouterState): ReactNode {
|