@alepha/react 0.7.7 → 0.8.1
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 +153 -1
- package/dist/index.browser.js +61 -26
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +175 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +198 -41
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +197 -40
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +180 -31
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/src/components/NestedView.tsx +3 -1
- package/src/descriptors/$page.ts +50 -42
- package/src/hooks/RouterHookApi.ts +17 -0
- package/src/hooks/useRouter.ts +1 -0
- package/src/index.browser.ts +4 -0
- package/src/index.shared.ts +1 -0
- package/src/index.ts +127 -3
- package/src/providers/BrowserRouterProvider.ts +1 -1
- package/src/providers/PageDescriptorProvider.ts +39 -23
- package/src/providers/ReactBrowserProvider.ts +38 -5
- package/src/providers/ReactBrowserRenderer.ts +21 -1
- package/src/providers/ReactServerProvider.ts +5 -5
- package/dist/index.browser.d.ts +0 -523
package/src/descriptors/$page.ts
CHANGED
|
@@ -15,6 +15,46 @@ import type { PageReactContext } from "../providers/PageDescriptorProvider.ts";
|
|
|
15
15
|
|
|
16
16
|
const KEY = "PAGE";
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Main descriptor for defining a React route in the application.
|
|
20
|
+
*/
|
|
21
|
+
export const $page = <
|
|
22
|
+
TConfig extends PageConfigSchema = PageConfigSchema,
|
|
23
|
+
TProps extends object = TPropsDefault,
|
|
24
|
+
TPropsParent extends object = TPropsParentDefault,
|
|
25
|
+
>(
|
|
26
|
+
options: PageDescriptorOptions<TConfig, TProps, TPropsParent>,
|
|
27
|
+
): PageDescriptor<TConfig, TProps, TPropsParent> => {
|
|
28
|
+
__descriptor(KEY);
|
|
29
|
+
|
|
30
|
+
// if (options.children) {
|
|
31
|
+
// for (const child of options.children) {
|
|
32
|
+
// child[OPTIONS].parent = {
|
|
33
|
+
// [OPTIONS]: options as PageDescriptorOptions<any, any, any>,
|
|
34
|
+
// };
|
|
35
|
+
// }
|
|
36
|
+
// }
|
|
37
|
+
|
|
38
|
+
// if (options.parent) {
|
|
39
|
+
// options.parent[OPTIONS].children ??= [];
|
|
40
|
+
// options.parent[OPTIONS].children.push({
|
|
41
|
+
// [OPTIONS]: options as PageDescriptorOptions<any, any, any>,
|
|
42
|
+
// });
|
|
43
|
+
// }
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
[KIND]: KEY,
|
|
47
|
+
[OPTIONS]: options,
|
|
48
|
+
render: () => {
|
|
49
|
+
throw new NotImplementedError(KEY);
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
$page[KIND] = KEY;
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
57
|
+
|
|
18
58
|
export interface PageConfigSchema {
|
|
19
59
|
query?: TSchema;
|
|
20
60
|
params?: TSchema;
|
|
@@ -96,7 +136,9 @@ export interface PageDescriptorOptions<
|
|
|
96
136
|
*
|
|
97
137
|
* If you still want to render at this pathname, add a child page with an empty path.
|
|
98
138
|
*/
|
|
99
|
-
children?:
|
|
139
|
+
children?:
|
|
140
|
+
| Array<{ [OPTIONS]: PageDescriptorOptions }>
|
|
141
|
+
| (() => Array<{ [OPTIONS]: PageDescriptorOptions }>);
|
|
100
142
|
|
|
101
143
|
parent?: { [OPTIONS]: PageDescriptorOptions<PageConfigSchema, TPropsParent> };
|
|
102
144
|
|
|
@@ -104,7 +146,13 @@ export interface PageDescriptorOptions<
|
|
|
104
146
|
|
|
105
147
|
errorHandler?: (error: Error) => ReactNode;
|
|
106
148
|
|
|
107
|
-
|
|
149
|
+
/**
|
|
150
|
+
* If true, the page will be rendered on the build time.
|
|
151
|
+
* Works only with viteAlepha plugin.
|
|
152
|
+
*
|
|
153
|
+
* Replace boolean by an object to define static entries. (e.g. list of params/query)
|
|
154
|
+
*/
|
|
155
|
+
static?:
|
|
108
156
|
| boolean
|
|
109
157
|
| {
|
|
110
158
|
entries?: Array<Partial<PageRequestConfig<TConfig>>>;
|
|
@@ -137,46 +185,6 @@ export interface PageDescriptor<
|
|
|
137
185
|
) => Promise<PageDescriptorRenderResult>;
|
|
138
186
|
}
|
|
139
187
|
|
|
140
|
-
/**
|
|
141
|
-
* Main descriptor for defining a React route in the application.
|
|
142
|
-
*/
|
|
143
|
-
export const $page = <
|
|
144
|
-
TConfig extends PageConfigSchema = PageConfigSchema,
|
|
145
|
-
TProps extends object = TPropsDefault,
|
|
146
|
-
TPropsParent extends object = TPropsParentDefault,
|
|
147
|
-
>(
|
|
148
|
-
options: PageDescriptorOptions<TConfig, TProps, TPropsParent>,
|
|
149
|
-
): PageDescriptor<TConfig, TProps, TPropsParent> => {
|
|
150
|
-
__descriptor(KEY);
|
|
151
|
-
|
|
152
|
-
if (options.children) {
|
|
153
|
-
for (const child of options.children) {
|
|
154
|
-
child[OPTIONS].parent = {
|
|
155
|
-
[OPTIONS]: options as PageDescriptorOptions<any, any, any>,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
if (options.parent) {
|
|
161
|
-
options.parent[OPTIONS].children ??= [];
|
|
162
|
-
options.parent[OPTIONS].children.push({
|
|
163
|
-
[OPTIONS]: options as PageDescriptorOptions<any, any, any>,
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return {
|
|
168
|
-
[KIND]: KEY,
|
|
169
|
-
[OPTIONS]: options,
|
|
170
|
-
render: () => {
|
|
171
|
-
throw new NotImplementedError(KEY);
|
|
172
|
-
},
|
|
173
|
-
};
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
$page[KIND] = KEY;
|
|
177
|
-
|
|
178
|
-
// ---------------------------------------------------------------------------------------------------------------------
|
|
179
|
-
|
|
180
188
|
export interface PageDescriptorRenderOptions {
|
|
181
189
|
params?: Record<string, string>;
|
|
182
190
|
query?: Record<string, string>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PageDescriptor } from "../descriptors/$page.ts";
|
|
2
2
|
import type {
|
|
3
3
|
AnchorProps,
|
|
4
|
+
PageReactContext,
|
|
4
5
|
PageRoute,
|
|
5
6
|
RouterState,
|
|
6
7
|
} from "../providers/PageDescriptorProvider.ts";
|
|
@@ -12,6 +13,7 @@ import type {
|
|
|
12
13
|
export class RouterHookApi {
|
|
13
14
|
constructor(
|
|
14
15
|
private readonly pages: PageRoute[],
|
|
16
|
+
private readonly context: PageReactContext,
|
|
15
17
|
private readonly state: RouterState,
|
|
16
18
|
private readonly layer: {
|
|
17
19
|
path: string;
|
|
@@ -19,6 +21,21 @@ export class RouterHookApi {
|
|
|
19
21
|
private readonly browser?: ReactBrowserProvider,
|
|
20
22
|
) {}
|
|
21
23
|
|
|
24
|
+
public getURL(): URL {
|
|
25
|
+
if (!this.browser) {
|
|
26
|
+
return this.context.url;
|
|
27
|
+
}
|
|
28
|
+
return new URL(this.location.href);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public get location(): Location {
|
|
32
|
+
if (!this.browser) {
|
|
33
|
+
throw new Error("Browser is required");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return this.browser.location;
|
|
37
|
+
}
|
|
38
|
+
|
|
22
39
|
public get current(): RouterState {
|
|
23
40
|
return this.state;
|
|
24
41
|
}
|
package/src/hooks/useRouter.ts
CHANGED
package/src/index.browser.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { __bind, type Alepha, type Module } from "@alepha/core";
|
|
2
|
+
import { AlephaServer } from "@alepha/server";
|
|
3
|
+
import { AlephaServerLinks } from "@alepha/server-links";
|
|
2
4
|
import { $page } from "./descriptors/$page.ts";
|
|
3
5
|
import { BrowserRouterProvider } from "./providers/BrowserRouterProvider.ts";
|
|
4
6
|
import { PageDescriptorProvider } from "./providers/PageDescriptorProvider.ts";
|
|
@@ -18,6 +20,8 @@ export class AlephaReact implements Module {
|
|
|
18
20
|
public readonly name = "alepha.react";
|
|
19
21
|
public readonly $services = (alepha: Alepha) =>
|
|
20
22
|
alepha
|
|
23
|
+
.with(AlephaServer)
|
|
24
|
+
.with(AlephaServerLinks)
|
|
21
25
|
.with(PageDescriptorProvider)
|
|
22
26
|
.with(ReactBrowserProvider)
|
|
23
27
|
.with(BrowserRouterProvider)
|
package/src/index.shared.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { default as ErrorBoundary } from "./components/ErrorBoundary.tsx";
|
|
|
3
3
|
export * from "./components/ErrorViewer.tsx";
|
|
4
4
|
export { default as Link } from "./components/Link.tsx";
|
|
5
5
|
export { default as NestedView } from "./components/NestedView.tsx";
|
|
6
|
+
export { default as NotFound } from "./components/NotFound.tsx";
|
|
6
7
|
export * from "./contexts/RouterContext.ts";
|
|
7
8
|
export * from "./contexts/RouterLayerContext.ts";
|
|
8
9
|
export * from "./descriptors/$page.ts";
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { __bind, type Alepha, type Module } from "@alepha/core";
|
|
2
2
|
import { AlephaServer, type ServerRequest } from "@alepha/server";
|
|
3
3
|
import { AlephaServerCache } from "@alepha/server-cache";
|
|
4
|
+
import { AlephaServerLinks } from "@alepha/server-links";
|
|
4
5
|
import { $page } from "./descriptors/$page.ts";
|
|
5
6
|
import {
|
|
6
7
|
PageDescriptorProvider,
|
|
@@ -65,10 +66,132 @@ declare module "@alepha/core" {
|
|
|
65
66
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
66
67
|
|
|
67
68
|
/**
|
|
68
|
-
*
|
|
69
|
+
* Provides full-stack React development with declarative routing, server-side rendering, and client-side hydration.
|
|
69
70
|
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
71
|
+
* The React module enables building modern React applications using the `$page` descriptor on class properties.
|
|
72
|
+
* It delivers seamless server-side rendering, automatic code splitting, and client-side navigation with full
|
|
73
|
+
* type safety and schema validation for route parameters and data.
|
|
74
|
+
*
|
|
75
|
+
* **Key Features:**
|
|
76
|
+
* - Declarative page definition with `$page` descriptor
|
|
77
|
+
* - Server-side rendering (SSR) with automatic hydration
|
|
78
|
+
* - Type-safe routing with parameter validation
|
|
79
|
+
* - Schema-based data resolution and validation
|
|
80
|
+
* - SEO-friendly meta tag management
|
|
81
|
+
* - Automatic code splitting and lazy loading
|
|
82
|
+
* - Client-side navigation with browser history
|
|
83
|
+
*
|
|
84
|
+
* **Basic Usage:**
|
|
85
|
+
* ```ts
|
|
86
|
+
* import { Alepha, run, t } from "alepha";
|
|
87
|
+
* import { AlephaReact, $page } from "alepha/react";
|
|
88
|
+
*
|
|
89
|
+
* class AppRoutes {
|
|
90
|
+
* // Home page
|
|
91
|
+
* home = $page({
|
|
92
|
+
* path: "/",
|
|
93
|
+
* component: () => (
|
|
94
|
+
* <div>
|
|
95
|
+
* <h1>Welcome to Alepha</h1>
|
|
96
|
+
* <p>Build amazing React applications!</p>
|
|
97
|
+
* </div>
|
|
98
|
+
* ),
|
|
99
|
+
* });
|
|
100
|
+
*
|
|
101
|
+
* // About page with meta tags
|
|
102
|
+
* about = $page({
|
|
103
|
+
* path: "/about",
|
|
104
|
+
* head: {
|
|
105
|
+
* title: "About Us",
|
|
106
|
+
* description: "Learn more about our mission",
|
|
107
|
+
* },
|
|
108
|
+
* component: () => (
|
|
109
|
+
* <div>
|
|
110
|
+
* <h1>About Us</h1>
|
|
111
|
+
* <p>Learn more about our mission.</p>
|
|
112
|
+
* </div>
|
|
113
|
+
* ),
|
|
114
|
+
* });
|
|
115
|
+
* }
|
|
116
|
+
*
|
|
117
|
+
* const alepha = Alepha.create()
|
|
118
|
+
* .with(AlephaReact)
|
|
119
|
+
* .with(AppRoutes);
|
|
120
|
+
*
|
|
121
|
+
* run(alepha);
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* **Dynamic Routes with Parameters:**
|
|
125
|
+
* ```tsx
|
|
126
|
+
* class UserRoutes {
|
|
127
|
+
* userProfile = $page({
|
|
128
|
+
* path: "/users/:id",
|
|
129
|
+
* schema: {
|
|
130
|
+
* params: t.object({
|
|
131
|
+
* id: t.string(),
|
|
132
|
+
* }),
|
|
133
|
+
* },
|
|
134
|
+
* resolve: async ({ params }) => {
|
|
135
|
+
* // Fetch user data server-side
|
|
136
|
+
* const user = await getUserById(params.id);
|
|
137
|
+
* return { user };
|
|
138
|
+
* },
|
|
139
|
+
* head: ({ user }) => ({
|
|
140
|
+
* title: `${user.name} - Profile`,
|
|
141
|
+
* description: `View ${user.name}'s profile`,
|
|
142
|
+
* }),
|
|
143
|
+
* component: ({ user }) => (
|
|
144
|
+
* <div>
|
|
145
|
+
* <h1>{user.name}</h1>
|
|
146
|
+
* <p>Email: {user.email}</p>
|
|
147
|
+
* </div>
|
|
148
|
+
* ),
|
|
149
|
+
* });
|
|
150
|
+
*
|
|
151
|
+
* userSettings = $page({
|
|
152
|
+
* path: "/users/:id/settings",
|
|
153
|
+
* schema: {
|
|
154
|
+
* params: t.object({
|
|
155
|
+
* id: t.string(),
|
|
156
|
+
* }),
|
|
157
|
+
* },
|
|
158
|
+
* component: ({ params }) => (
|
|
159
|
+
* <UserSettings userId={params.id} />
|
|
160
|
+
* ),
|
|
161
|
+
* });
|
|
162
|
+
* }
|
|
163
|
+
* ```
|
|
164
|
+
*
|
|
165
|
+
* **Static Generation:**
|
|
166
|
+
* ```tsx
|
|
167
|
+
* class BlogRoutes {
|
|
168
|
+
* blogPost = $page({
|
|
169
|
+
* path: "/blog/:slug",
|
|
170
|
+
* schema: {
|
|
171
|
+
* params: t.object({
|
|
172
|
+
* slug: t.string(),
|
|
173
|
+
* }),
|
|
174
|
+
* },
|
|
175
|
+
* static: {
|
|
176
|
+
* entries: [
|
|
177
|
+
* { params: { slug: "getting-started" } },
|
|
178
|
+
* { params: { slug: "advanced-features" } },
|
|
179
|
+
* { params: { slug: "deployment" } },
|
|
180
|
+
* ],
|
|
181
|
+
* },
|
|
182
|
+
* resolve: ({ params }) => {
|
|
183
|
+
* const post = getBlogPost(params.slug);
|
|
184
|
+
* return { post };
|
|
185
|
+
* },
|
|
186
|
+
* component: ({ post }) => (
|
|
187
|
+
* <article>
|
|
188
|
+
* <h1>{post.title}</h1>
|
|
189
|
+
* <div>{post.content}</div>
|
|
190
|
+
* </article>
|
|
191
|
+
* ),
|
|
192
|
+
* });
|
|
193
|
+
* }
|
|
194
|
+
* ```
|
|
72
195
|
*
|
|
73
196
|
* @see {@link $page}
|
|
74
197
|
* @module alepha.react
|
|
@@ -79,6 +202,7 @@ export class AlephaReact implements Module {
|
|
|
79
202
|
alepha
|
|
80
203
|
.with(AlephaServer)
|
|
81
204
|
.with(AlephaServerCache)
|
|
205
|
+
.with(AlephaServerLinks)
|
|
82
206
|
.with(ReactServerProvider)
|
|
83
207
|
.with(PageDescriptorProvider);
|
|
84
208
|
}
|
|
@@ -28,7 +28,7 @@ export class BrowserRouterProvider extends RouterProvider<BrowserRoute> {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
protected readonly configure = $hook({
|
|
31
|
-
|
|
31
|
+
on: "configure",
|
|
32
32
|
handler: async () => {
|
|
33
33
|
for (const page of this.pageDescriptorProvider.getPages()) {
|
|
34
34
|
// mount only if a view is provided
|
|
@@ -109,7 +109,7 @@ export class PageDescriptorProvider {
|
|
|
109
109
|
try {
|
|
110
110
|
config.query = route.schema?.query
|
|
111
111
|
? this.alepha.parse(route.schema.query, request.query)
|
|
112
|
-
:
|
|
112
|
+
: {};
|
|
113
113
|
} catch (e) {
|
|
114
114
|
it.error = e as Error;
|
|
115
115
|
break;
|
|
@@ -118,7 +118,7 @@ export class PageDescriptorProvider {
|
|
|
118
118
|
try {
|
|
119
119
|
config.params = route.schema?.params
|
|
120
120
|
? this.alepha.parse(route.schema.params, request.params)
|
|
121
|
-
:
|
|
121
|
+
: {};
|
|
122
122
|
} catch (e) {
|
|
123
123
|
it.error = e as Error;
|
|
124
124
|
break;
|
|
@@ -129,11 +129,6 @@ export class PageDescriptorProvider {
|
|
|
129
129
|
...config,
|
|
130
130
|
};
|
|
131
131
|
|
|
132
|
-
// no resolve, render a basic view by default
|
|
133
|
-
if (!route.resolve) {
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
132
|
// check if previous layer is the same, reuse if possible
|
|
138
133
|
const previous = request.previous;
|
|
139
134
|
if (previous?.[i] && !forceRefresh && previous[i].name === route.name) {
|
|
@@ -153,16 +148,23 @@ export class PageDescriptorProvider {
|
|
|
153
148
|
// part is the same, reuse previous layer
|
|
154
149
|
it.props = previous[i].props;
|
|
155
150
|
it.error = previous[i].error;
|
|
151
|
+
it.cache = true;
|
|
156
152
|
context = {
|
|
157
153
|
...context,
|
|
158
154
|
...it.props,
|
|
159
155
|
};
|
|
160
156
|
continue;
|
|
161
157
|
}
|
|
158
|
+
|
|
162
159
|
// part is different, force refresh of next layers
|
|
163
160
|
forceRefresh = true;
|
|
164
161
|
}
|
|
165
162
|
|
|
163
|
+
// no resolve, render a basic view by default
|
|
164
|
+
if (!route.resolve) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
166
168
|
try {
|
|
167
169
|
const props =
|
|
168
170
|
(await route.resolve?.({
|
|
@@ -209,13 +211,6 @@ export class PageDescriptorProvider {
|
|
|
209
211
|
params[key] = String(params[key]);
|
|
210
212
|
}
|
|
211
213
|
|
|
212
|
-
// if (it.route.head && !it.error) {
|
|
213
|
-
// this.fillHead(it.route, request, {
|
|
214
|
-
// ...props,
|
|
215
|
-
// ...context,
|
|
216
|
-
// });
|
|
217
|
-
// }
|
|
218
|
-
|
|
219
214
|
acc += "/";
|
|
220
215
|
acc += it.route.path ? this.compile(it.route.path, params) : "";
|
|
221
216
|
const path = acc.replace(/\/+/, "/");
|
|
@@ -240,7 +235,7 @@ export class PageDescriptorProvider {
|
|
|
240
235
|
element: this.renderView(i + 1, path, element, it.route),
|
|
241
236
|
index: i + 1,
|
|
242
237
|
path,
|
|
243
|
-
route,
|
|
238
|
+
route: it.route,
|
|
244
239
|
});
|
|
245
240
|
break;
|
|
246
241
|
}
|
|
@@ -260,7 +255,8 @@ export class PageDescriptorProvider {
|
|
|
260
255
|
element: this.renderView(i + 1, path, element, it.route),
|
|
261
256
|
index: i + 1,
|
|
262
257
|
path,
|
|
263
|
-
route,
|
|
258
|
+
route: it.route,
|
|
259
|
+
cache: it.cache,
|
|
264
260
|
});
|
|
265
261
|
}
|
|
266
262
|
|
|
@@ -357,24 +353,38 @@ export class PageDescriptorProvider {
|
|
|
357
353
|
}
|
|
358
354
|
|
|
359
355
|
protected readonly configure = $hook({
|
|
360
|
-
|
|
356
|
+
on: "configure",
|
|
361
357
|
handler: () => {
|
|
362
358
|
let hasNotFoundHandler = false;
|
|
363
359
|
const pages = this.alepha.getDescriptorValues($page);
|
|
360
|
+
|
|
361
|
+
const hasParent = (it: { [OPTIONS]: PageDescriptorOptions }) => {
|
|
362
|
+
for (const page of pages) {
|
|
363
|
+
const children = page.value[OPTIONS].children
|
|
364
|
+
? Array.isArray(page.value[OPTIONS].children)
|
|
365
|
+
? page.value[OPTIONS].children
|
|
366
|
+
: page.value[OPTIONS].children()
|
|
367
|
+
: [];
|
|
368
|
+
if (children.includes(it)) {
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
|
|
364
374
|
for (const { value, key } of pages) {
|
|
365
375
|
value[OPTIONS].name ??= key;
|
|
366
376
|
}
|
|
367
377
|
|
|
368
378
|
for (const { value } of pages) {
|
|
369
|
-
// skip children, we only want root pages
|
|
370
|
-
if (value[OPTIONS].parent) {
|
|
371
|
-
continue;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
379
|
if (value[OPTIONS].path === "/*") {
|
|
375
380
|
hasNotFoundHandler = true;
|
|
376
381
|
}
|
|
377
382
|
|
|
383
|
+
// skip children, we only want root pages
|
|
384
|
+
if (hasParent(value)) {
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
|
|
378
388
|
this.add(this.map(pages, value));
|
|
379
389
|
}
|
|
380
390
|
|
|
@@ -397,7 +407,11 @@ export class PageDescriptorProvider {
|
|
|
397
407
|
pages: Array<{ value: { [OPTIONS]: PageDescriptorOptions } }>,
|
|
398
408
|
target: { [OPTIONS]: PageDescriptorOptions },
|
|
399
409
|
): PageRouteEntry {
|
|
400
|
-
const children = target[OPTIONS].children
|
|
410
|
+
const children = target[OPTIONS].children
|
|
411
|
+
? Array.isArray(target[OPTIONS].children)
|
|
412
|
+
? target[OPTIONS].children
|
|
413
|
+
: target[OPTIONS].children()
|
|
414
|
+
: [];
|
|
401
415
|
|
|
402
416
|
return {
|
|
403
417
|
...target[OPTIONS],
|
|
@@ -488,6 +502,7 @@ export interface Layer {
|
|
|
488
502
|
index: number;
|
|
489
503
|
path: string;
|
|
490
504
|
route?: PageRoute;
|
|
505
|
+
cache?: boolean;
|
|
491
506
|
}
|
|
492
507
|
|
|
493
508
|
export type PreviousLayerData = Omit<Layer, "element" | "index" | "path">;
|
|
@@ -514,6 +529,7 @@ export interface RouterStackItem {
|
|
|
514
529
|
config?: Record<string, any>;
|
|
515
530
|
props?: Record<string, any>;
|
|
516
531
|
error?: Error;
|
|
532
|
+
cache?: boolean;
|
|
517
533
|
}
|
|
518
534
|
|
|
519
535
|
export interface RouterRenderResult {
|
|
@@ -35,8 +35,35 @@ export class ReactBrowserProvider {
|
|
|
35
35
|
return window.history;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
public get location() {
|
|
39
|
+
return window.location;
|
|
40
|
+
}
|
|
41
|
+
|
|
38
42
|
public get url(): string {
|
|
39
|
-
|
|
43
|
+
let url = this.location.pathname + this.location.search;
|
|
44
|
+
|
|
45
|
+
if (import.meta?.env?.BASE_URL) {
|
|
46
|
+
url = url.replace(import.meta.env?.BASE_URL, "");
|
|
47
|
+
if (!url.startsWith("/")) {
|
|
48
|
+
url = `/${url}`;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return url;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public pushState(url: string, replace?: boolean) {
|
|
56
|
+
let path = url;
|
|
57
|
+
|
|
58
|
+
if (import.meta?.env?.BASE_URL) {
|
|
59
|
+
path = (import.meta.env?.BASE_URL + path).replaceAll("//", "/");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (replace) {
|
|
63
|
+
this.history.replaceState({}, "", path);
|
|
64
|
+
} else {
|
|
65
|
+
this.history.pushState({}, "", path);
|
|
66
|
+
}
|
|
40
67
|
}
|
|
41
68
|
|
|
42
69
|
public async invalidate(props?: Record<string, any>) {
|
|
@@ -72,16 +99,16 @@ export class ReactBrowserProvider {
|
|
|
72
99
|
// when redirecting in browser
|
|
73
100
|
if (result.context.url.pathname !== url) {
|
|
74
101
|
// TODO: check if losing search params is acceptable?
|
|
75
|
-
this.
|
|
102
|
+
this.pushState(result.context.url.pathname);
|
|
76
103
|
return;
|
|
77
104
|
}
|
|
78
105
|
|
|
79
106
|
if (options.replace) {
|
|
80
|
-
this.
|
|
107
|
+
this.pushState(url);
|
|
81
108
|
return;
|
|
82
109
|
}
|
|
83
110
|
|
|
84
|
-
this.
|
|
111
|
+
this.pushState(url);
|
|
85
112
|
}
|
|
86
113
|
|
|
87
114
|
protected async render(
|
|
@@ -125,7 +152,7 @@ export class ReactBrowserProvider {
|
|
|
125
152
|
// -------------------------------------------------------------------------------------------------------------------
|
|
126
153
|
|
|
127
154
|
public readonly ready = $hook({
|
|
128
|
-
|
|
155
|
+
on: "ready",
|
|
129
156
|
handler: async () => {
|
|
130
157
|
const hydration = this.getHydrationState();
|
|
131
158
|
const previous = hydration?.layers ?? [];
|
|
@@ -145,6 +172,12 @@ export class ReactBrowserProvider {
|
|
|
145
172
|
});
|
|
146
173
|
|
|
147
174
|
window.addEventListener("popstate", () => {
|
|
175
|
+
// when you update silently queryparams or hash, skip rendering
|
|
176
|
+
// if you want to force a rendering, use #go()
|
|
177
|
+
if (this.state.pathname === location.pathname) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
148
181
|
this.render();
|
|
149
182
|
});
|
|
150
183
|
},
|
|
@@ -17,6 +17,10 @@ declare module "@alepha/core" {
|
|
|
17
17
|
interface Env extends Partial<Static<typeof envSchema>> {}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export interface ReactBrowserRendererOptions {
|
|
21
|
+
scrollRestoration?: "top" | "manual";
|
|
22
|
+
}
|
|
23
|
+
|
|
20
24
|
// TODO: move to ReactBrowserProvider when it will be removed from server-side imports
|
|
21
25
|
export class ReactBrowserRenderer {
|
|
22
26
|
protected readonly browserProvider = $inject(ReactBrowserProvider);
|
|
@@ -26,6 +30,10 @@ export class ReactBrowserRenderer {
|
|
|
26
30
|
|
|
27
31
|
protected root!: Root;
|
|
28
32
|
|
|
33
|
+
public options: ReactBrowserRendererOptions = {
|
|
34
|
+
scrollRestoration: "top",
|
|
35
|
+
};
|
|
36
|
+
|
|
29
37
|
protected getRootElement() {
|
|
30
38
|
const root = this.browserProvider.document.getElementById(
|
|
31
39
|
this.env.REACT_ROOT_ID,
|
|
@@ -43,7 +51,7 @@ export class ReactBrowserRenderer {
|
|
|
43
51
|
}
|
|
44
52
|
|
|
45
53
|
public readonly ready = $hook({
|
|
46
|
-
|
|
54
|
+
on: "react:browser:render",
|
|
47
55
|
handler: async ({ state, context, hydration }) => {
|
|
48
56
|
const element = this.browserRouterProvider.root(state, context);
|
|
49
57
|
|
|
@@ -57,6 +65,18 @@ export class ReactBrowserRenderer {
|
|
|
57
65
|
}
|
|
58
66
|
},
|
|
59
67
|
});
|
|
68
|
+
|
|
69
|
+
protected readonly onTransitionEnd = $hook({
|
|
70
|
+
on: "react:transition:end",
|
|
71
|
+
handler: () => {
|
|
72
|
+
if (
|
|
73
|
+
this.options.scrollRestoration === "top" &&
|
|
74
|
+
typeof window !== "undefined"
|
|
75
|
+
) {
|
|
76
|
+
window.scrollTo(0, 0);
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
});
|
|
60
80
|
}
|
|
61
81
|
|
|
62
82
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
@@ -41,8 +41,8 @@ const envSchema = t.object({
|
|
|
41
41
|
declare module "@alepha/core" {
|
|
42
42
|
interface Env extends Partial<Static<typeof envSchema>> {}
|
|
43
43
|
interface State {
|
|
44
|
-
"
|
|
45
|
-
"
|
|
44
|
+
"react.server.template"?: string;
|
|
45
|
+
"react.server.ssr"?: boolean;
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
@@ -60,14 +60,14 @@ export class ReactServerProvider {
|
|
|
60
60
|
);
|
|
61
61
|
|
|
62
62
|
public readonly onConfigure = $hook({
|
|
63
|
-
|
|
63
|
+
on: "configure",
|
|
64
64
|
handler: async () => {
|
|
65
65
|
const pages = this.alepha.getDescriptorValues($page);
|
|
66
66
|
|
|
67
67
|
const ssrEnabled =
|
|
68
68
|
pages.length > 0 && this.env.REACT_SSR_ENABLED !== false;
|
|
69
69
|
|
|
70
|
-
this.alepha.state("
|
|
70
|
+
this.alepha.state("react.server.ssr", ssrEnabled);
|
|
71
71
|
|
|
72
72
|
for (const { key, instance, value } of pages) {
|
|
73
73
|
const name = value[OPTIONS].name ?? key;
|
|
@@ -127,7 +127,7 @@ export class ReactServerProvider {
|
|
|
127
127
|
|
|
128
128
|
public get template() {
|
|
129
129
|
return (
|
|
130
|
-
this.alepha.state("
|
|
130
|
+
this.alepha.state("react.server.template") ??
|
|
131
131
|
"<!DOCTYPE html><html lang='en'><head></head><body></body></html>"
|
|
132
132
|
);
|
|
133
133
|
}
|