@alepha/react 0.8.1 → 0.9.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 +0 -121
- package/dist/index.browser.js +63 -50
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +273 -383
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -183
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +48 -183
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +279 -390
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/components/ErrorViewer.tsx +4 -3
- package/src/components/Link.tsx +3 -5
- package/src/descriptors/$page.ts +30 -49
- package/src/hooks/useInject.ts +1 -1
- package/src/hooks/useRouter.ts +2 -2
- package/src/index.browser.ts +13 -8
- package/src/index.ts +12 -130
- package/src/providers/PageDescriptorProvider.ts +34 -26
- package/src/providers/ReactBrowserProvider.ts +1 -1
- package/src/providers/ReactBrowserRenderer.ts +2 -2
- package/src/providers/ReactServerProvider.ts +15 -13
package/src/descriptors/$page.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {
|
|
2
|
-
__descriptor,
|
|
3
2
|
type Async,
|
|
3
|
+
createDescriptor,
|
|
4
|
+
Descriptor,
|
|
4
5
|
KIND,
|
|
5
6
|
NotImplementedError,
|
|
6
|
-
OPTIONS,
|
|
7
7
|
type Static,
|
|
8
8
|
type TSchema,
|
|
9
9
|
} from "@alepha/core";
|
|
@@ -13,8 +13,6 @@ import type { FC, ReactNode } from "react";
|
|
|
13
13
|
import type { ClientOnlyProps } from "../components/ClientOnly.tsx";
|
|
14
14
|
import type { PageReactContext } from "../providers/PageDescriptorProvider.ts";
|
|
15
15
|
|
|
16
|
-
const KEY = "PAGE";
|
|
17
|
-
|
|
18
16
|
/**
|
|
19
17
|
* Main descriptor for defining a React route in the application.
|
|
20
18
|
*/
|
|
@@ -25,45 +23,14 @@ export const $page = <
|
|
|
25
23
|
>(
|
|
26
24
|
options: PageDescriptorOptions<TConfig, TProps, TPropsParent>,
|
|
27
25
|
): PageDescriptor<TConfig, TProps, TPropsParent> => {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
};
|
|
26
|
+
return createDescriptor(
|
|
27
|
+
PageDescriptor<TConfig, TProps, TPropsParent>,
|
|
28
|
+
options,
|
|
29
|
+
);
|
|
52
30
|
};
|
|
53
31
|
|
|
54
|
-
$page[KIND] = KEY;
|
|
55
|
-
|
|
56
32
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
57
33
|
|
|
58
|
-
export interface PageConfigSchema {
|
|
59
|
-
query?: TSchema;
|
|
60
|
-
params?: TSchema;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export type TPropsDefault = any;
|
|
64
|
-
|
|
65
|
-
export type TPropsParentDefault = {};
|
|
66
|
-
|
|
67
34
|
export interface PageDescriptorOptions<
|
|
68
35
|
TConfig extends PageConfigSchema = PageConfigSchema,
|
|
69
36
|
TProps extends object = TPropsDefault,
|
|
@@ -136,11 +103,9 @@ export interface PageDescriptorOptions<
|
|
|
136
103
|
*
|
|
137
104
|
* If you still want to render at this pathname, add a child page with an empty path.
|
|
138
105
|
*/
|
|
139
|
-
children?:
|
|
140
|
-
| Array<{ [OPTIONS]: PageDescriptorOptions }>
|
|
141
|
-
| (() => Array<{ [OPTIONS]: PageDescriptorOptions }>);
|
|
106
|
+
children?: Array<PageDescriptor> | (() => Array<PageDescriptor>);
|
|
142
107
|
|
|
143
|
-
parent?:
|
|
108
|
+
parent?: PageDescriptor<PageConfigSchema, TPropsParent>;
|
|
144
109
|
|
|
145
110
|
can?: () => boolean;
|
|
146
111
|
|
|
@@ -168,23 +133,39 @@ export interface PageDescriptorOptions<
|
|
|
168
133
|
cache?: ServerRouteCache;
|
|
169
134
|
}
|
|
170
135
|
|
|
171
|
-
export
|
|
136
|
+
export class PageDescriptor<
|
|
172
137
|
TConfig extends PageConfigSchema = PageConfigSchema,
|
|
173
138
|
TProps extends object = TPropsDefault,
|
|
174
139
|
TPropsParent extends object = TPropsParentDefault,
|
|
175
|
-
> {
|
|
176
|
-
|
|
177
|
-
|
|
140
|
+
> extends Descriptor<PageDescriptorOptions<TConfig, TProps, TPropsParent>> {
|
|
141
|
+
public get name(): string {
|
|
142
|
+
return this.options.name ?? this.config.propertyKey;
|
|
143
|
+
}
|
|
178
144
|
|
|
179
145
|
/**
|
|
180
146
|
* For testing or build purposes, this will render the page (with or without the HTML layout) and return the HTML and context.
|
|
181
147
|
* Only valid for server-side rendering, it will throw an error if called on the client-side.
|
|
182
148
|
*/
|
|
183
|
-
render
|
|
149
|
+
public async render(
|
|
184
150
|
options?: PageDescriptorRenderOptions,
|
|
185
|
-
)
|
|
151
|
+
): Promise<PageDescriptorRenderResult> {
|
|
152
|
+
throw new NotImplementedError("");
|
|
153
|
+
}
|
|
186
154
|
}
|
|
187
155
|
|
|
156
|
+
$page[KIND] = PageDescriptor;
|
|
157
|
+
|
|
158
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
export interface PageConfigSchema {
|
|
161
|
+
query?: TSchema;
|
|
162
|
+
params?: TSchema;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export type TPropsDefault = any;
|
|
166
|
+
|
|
167
|
+
export type TPropsParentDefault = {};
|
|
168
|
+
|
|
188
169
|
export interface PageDescriptorRenderOptions {
|
|
189
170
|
params?: Record<string, string>;
|
|
190
171
|
query?: Record<string, string>;
|
package/src/hooks/useInject.ts
CHANGED
package/src/hooks/useRouter.ts
CHANGED
|
@@ -13,7 +13,7 @@ export const useRouter = (): RouterHookApi => {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const pages = useMemo(() => {
|
|
16
|
-
return ctx.alepha.
|
|
16
|
+
return ctx.alepha.inject(PageDescriptorProvider).getPages();
|
|
17
17
|
}, []);
|
|
18
18
|
|
|
19
19
|
return useMemo(
|
|
@@ -24,7 +24,7 @@ export const useRouter = (): RouterHookApi => {
|
|
|
24
24
|
ctx.state,
|
|
25
25
|
layer,
|
|
26
26
|
ctx.alepha.isBrowser()
|
|
27
|
-
? ctx.alepha.
|
|
27
|
+
? ctx.alepha.inject(ReactBrowserProvider)
|
|
28
28
|
: undefined,
|
|
29
29
|
),
|
|
30
30
|
[layer],
|
package/src/index.browser.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
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";
|
|
@@ -16,16 +16,21 @@ export * from "./providers/ReactBrowserProvider.ts";
|
|
|
16
16
|
|
|
17
17
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
18
18
|
|
|
19
|
-
export
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
export const AlephaReact = $module({
|
|
20
|
+
name: "alepha.react",
|
|
21
|
+
descriptors: [$page],
|
|
22
|
+
services: [
|
|
23
|
+
PageDescriptorProvider,
|
|
24
|
+
ReactBrowserRenderer,
|
|
25
|
+
BrowserRouterProvider,
|
|
26
|
+
ReactBrowserProvider,
|
|
27
|
+
],
|
|
28
|
+
register: (alepha) =>
|
|
22
29
|
alepha
|
|
23
30
|
.with(AlephaServer)
|
|
24
31
|
.with(AlephaServerLinks)
|
|
25
32
|
.with(PageDescriptorProvider)
|
|
26
33
|
.with(ReactBrowserProvider)
|
|
27
34
|
.with(BrowserRouterProvider)
|
|
28
|
-
.with(ReactBrowserRenderer)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
__bind($page, AlephaReact);
|
|
35
|
+
.with(ReactBrowserRenderer),
|
|
36
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
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";
|
|
@@ -9,7 +9,10 @@ import {
|
|
|
9
9
|
type PageRequest,
|
|
10
10
|
type RouterState,
|
|
11
11
|
} from "./providers/PageDescriptorProvider.ts";
|
|
12
|
-
import
|
|
12
|
+
import {
|
|
13
|
+
ReactBrowserProvider,
|
|
14
|
+
type ReactHydrationState,
|
|
15
|
+
} from "./providers/ReactBrowserProvider.ts";
|
|
13
16
|
import { ReactServerProvider } from "./providers/ReactServerProvider.ts";
|
|
14
17
|
|
|
15
18
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
@@ -72,139 +75,18 @@ declare module "@alepha/core" {
|
|
|
72
75
|
* It delivers seamless server-side rendering, automatic code splitting, and client-side navigation with full
|
|
73
76
|
* type safety and schema validation for route parameters and data.
|
|
74
77
|
*
|
|
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
|
-
* ```
|
|
195
|
-
*
|
|
196
78
|
* @see {@link $page}
|
|
197
79
|
* @module alepha.react
|
|
198
80
|
*/
|
|
199
|
-
export
|
|
200
|
-
|
|
201
|
-
|
|
81
|
+
export const AlephaReact = $module({
|
|
82
|
+
name: "alepha.react",
|
|
83
|
+
descriptors: [$page],
|
|
84
|
+
services: [ReactServerProvider, PageDescriptorProvider, ReactBrowserProvider],
|
|
85
|
+
register: (alepha) =>
|
|
202
86
|
alepha
|
|
203
87
|
.with(AlephaServer)
|
|
204
88
|
.with(AlephaServerCache)
|
|
205
89
|
.with(AlephaServerLinks)
|
|
206
90
|
.with(ReactServerProvider)
|
|
207
|
-
.with(PageDescriptorProvider)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
__bind($page, AlephaReact);
|
|
91
|
+
.with(PageDescriptorProvider),
|
|
92
|
+
});
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
$env,
|
|
3
|
+
$hook,
|
|
4
|
+
$inject,
|
|
5
|
+
$logger,
|
|
6
|
+
Alepha,
|
|
7
|
+
type Static,
|
|
8
|
+
t,
|
|
9
|
+
} from "@alepha/core";
|
|
3
10
|
import type { ApiLinksResponse } from "@alepha/server";
|
|
4
11
|
import { createElement, type ReactNode, StrictMode } from "react";
|
|
5
12
|
import ClientOnly from "../components/ClientOnly.tsx";
|
|
@@ -8,7 +15,11 @@ import NestedView from "../components/NestedView.tsx";
|
|
|
8
15
|
import NotFoundPage from "../components/NotFound.tsx";
|
|
9
16
|
import { RouterContext } from "../contexts/RouterContext.ts";
|
|
10
17
|
import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
|
|
11
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
$page,
|
|
20
|
+
type PageDescriptor,
|
|
21
|
+
type PageDescriptorOptions,
|
|
22
|
+
} from "../descriptors/$page.ts";
|
|
12
23
|
import { RedirectionError } from "../errors/RedirectionError.ts";
|
|
13
24
|
|
|
14
25
|
const envSchema = t.object({
|
|
@@ -21,7 +32,7 @@ declare module "@alepha/core" {
|
|
|
21
32
|
|
|
22
33
|
export class PageDescriptorProvider {
|
|
23
34
|
protected readonly log = $logger();
|
|
24
|
-
protected readonly env = $
|
|
35
|
+
protected readonly env = $env(envSchema);
|
|
25
36
|
protected readonly alepha = $inject(Alepha);
|
|
26
37
|
protected readonly pages: PageRoute[] = [];
|
|
27
38
|
|
|
@@ -289,7 +300,7 @@ export class PageDescriptorProvider {
|
|
|
289
300
|
}
|
|
290
301
|
|
|
291
302
|
public renderError(error: Error): ReactNode {
|
|
292
|
-
return createElement(ErrorViewer, { error });
|
|
303
|
+
return createElement(ErrorViewer, { error, alepha: this.alepha });
|
|
293
304
|
}
|
|
294
305
|
|
|
295
306
|
public renderEmptyView(): ReactNode {
|
|
@@ -356,14 +367,14 @@ export class PageDescriptorProvider {
|
|
|
356
367
|
on: "configure",
|
|
357
368
|
handler: () => {
|
|
358
369
|
let hasNotFoundHandler = false;
|
|
359
|
-
const pages = this.alepha.
|
|
370
|
+
const pages = this.alepha.descriptors($page);
|
|
360
371
|
|
|
361
|
-
const hasParent = (it:
|
|
372
|
+
const hasParent = (it: PageDescriptor) => {
|
|
362
373
|
for (const page of pages) {
|
|
363
|
-
const children = page.
|
|
364
|
-
? Array.isArray(page.
|
|
365
|
-
? page.
|
|
366
|
-
: page.
|
|
374
|
+
const children = page.options.children
|
|
375
|
+
? Array.isArray(page.options.children)
|
|
376
|
+
? page.options.children
|
|
377
|
+
: page.options.children()
|
|
367
378
|
: [];
|
|
368
379
|
if (children.includes(it)) {
|
|
369
380
|
return true;
|
|
@@ -371,21 +382,17 @@ export class PageDescriptorProvider {
|
|
|
371
382
|
}
|
|
372
383
|
};
|
|
373
384
|
|
|
374
|
-
for (const
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
for (const { value } of pages) {
|
|
379
|
-
if (value[OPTIONS].path === "/*") {
|
|
385
|
+
for (const page of pages) {
|
|
386
|
+
if (page.options.path === "/*") {
|
|
380
387
|
hasNotFoundHandler = true;
|
|
381
388
|
}
|
|
382
389
|
|
|
383
390
|
// skip children, we only want root pages
|
|
384
|
-
if (hasParent(
|
|
391
|
+
if (hasParent(page)) {
|
|
385
392
|
continue;
|
|
386
393
|
}
|
|
387
394
|
|
|
388
|
-
this.add(this.map(pages,
|
|
395
|
+
this.add(this.map(pages, page));
|
|
389
396
|
}
|
|
390
397
|
|
|
391
398
|
if (!hasNotFoundHandler && pages.length > 0) {
|
|
@@ -404,17 +411,18 @@ export class PageDescriptorProvider {
|
|
|
404
411
|
});
|
|
405
412
|
|
|
406
413
|
protected map(
|
|
407
|
-
pages: Array<
|
|
408
|
-
target:
|
|
414
|
+
pages: Array<PageDescriptor>,
|
|
415
|
+
target: PageDescriptor,
|
|
409
416
|
): PageRouteEntry {
|
|
410
|
-
const children = target
|
|
411
|
-
? Array.isArray(target
|
|
412
|
-
? target
|
|
413
|
-
: target
|
|
417
|
+
const children = target.options.children
|
|
418
|
+
? Array.isArray(target.options.children)
|
|
419
|
+
? target.options.children
|
|
420
|
+
: target.options.children()
|
|
414
421
|
: [];
|
|
415
422
|
|
|
416
423
|
return {
|
|
417
|
-
...target
|
|
424
|
+
...target.options,
|
|
425
|
+
name: target.name,
|
|
418
426
|
parent: undefined,
|
|
419
427
|
children: children.map((it) => this.map(pages, it)),
|
|
420
428
|
} as PageRoute;
|
|
@@ -174,7 +174,7 @@ export class ReactBrowserProvider {
|
|
|
174
174
|
window.addEventListener("popstate", () => {
|
|
175
175
|
// when you update silently queryparams or hash, skip rendering
|
|
176
176
|
// if you want to force a rendering, use #go()
|
|
177
|
-
if (this.state.pathname ===
|
|
177
|
+
if (this.state.pathname === this.url) {
|
|
178
178
|
return;
|
|
179
179
|
}
|
|
180
180
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $hook, $inject, $logger, type Static, t } from "@alepha/core";
|
|
1
|
+
import { $env, $hook, $inject, $logger, type Static, t } from "@alepha/core";
|
|
2
2
|
import type { ApiLinksResponse } from "@alepha/server";
|
|
3
3
|
import type { Root } from "react-dom/client";
|
|
4
4
|
import { createRoot, hydrateRoot } from "react-dom/client";
|
|
@@ -25,7 +25,7 @@ export interface ReactBrowserRendererOptions {
|
|
|
25
25
|
export class ReactBrowserRenderer {
|
|
26
26
|
protected readonly browserProvider = $inject(ReactBrowserProvider);
|
|
27
27
|
protected readonly browserRouterProvider = $inject(BrowserRouterProvider);
|
|
28
|
-
protected readonly env = $
|
|
28
|
+
protected readonly env = $env(envSchema);
|
|
29
29
|
protected readonly log = $logger();
|
|
30
30
|
|
|
31
31
|
protected root!: Root;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import {
|
|
4
|
+
$env,
|
|
4
5
|
$hook,
|
|
5
6
|
$inject,
|
|
6
7
|
$logger,
|
|
7
8
|
Alepha,
|
|
8
|
-
OPTIONS,
|
|
9
9
|
type Static,
|
|
10
10
|
t,
|
|
11
11
|
} from "@alepha/core";
|
|
@@ -36,12 +36,16 @@ const envSchema = t.object({
|
|
|
36
36
|
REACT_SERVER_PREFIX: t.string({ default: "" }),
|
|
37
37
|
REACT_SSR_ENABLED: t.optional(t.boolean()),
|
|
38
38
|
REACT_ROOT_ID: t.string({ default: "root" }),
|
|
39
|
+
REACT_SERVER_TEMPLATE: t.optional(
|
|
40
|
+
t.string({
|
|
41
|
+
size: "rich",
|
|
42
|
+
}),
|
|
43
|
+
),
|
|
39
44
|
});
|
|
40
45
|
|
|
41
46
|
declare module "@alepha/core" {
|
|
42
47
|
interface Env extends Partial<Static<typeof envSchema>> {}
|
|
43
48
|
interface State {
|
|
44
|
-
"react.server.template"?: string;
|
|
45
49
|
"react.server.ssr"?: boolean;
|
|
46
50
|
}
|
|
47
51
|
}
|
|
@@ -53,7 +57,7 @@ export class ReactServerProvider {
|
|
|
53
57
|
protected readonly serverStaticProvider = $inject(ServerStaticProvider);
|
|
54
58
|
protected readonly serverRouterProvider = $inject(ServerRouterProvider);
|
|
55
59
|
protected readonly serverTimingProvider = $inject(ServerTimingProvider);
|
|
56
|
-
protected readonly env = $
|
|
60
|
+
protected readonly env = $env(envSchema);
|
|
57
61
|
protected readonly ROOT_DIV_REGEX = new RegExp(
|
|
58
62
|
`<div([^>]*)\\s+id=["']${this.env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`,
|
|
59
63
|
"is",
|
|
@@ -62,17 +66,15 @@ export class ReactServerProvider {
|
|
|
62
66
|
public readonly onConfigure = $hook({
|
|
63
67
|
on: "configure",
|
|
64
68
|
handler: async () => {
|
|
65
|
-
const pages = this.alepha.
|
|
69
|
+
const pages = this.alepha.descriptors($page);
|
|
66
70
|
|
|
67
71
|
const ssrEnabled =
|
|
68
72
|
pages.length > 0 && this.env.REACT_SSR_ENABLED !== false;
|
|
69
73
|
|
|
70
74
|
this.alepha.state("react.server.ssr", ssrEnabled);
|
|
71
75
|
|
|
72
|
-
for (const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
instance[key].render = this.createRenderFunction(name);
|
|
76
|
+
for (const page of pages) {
|
|
77
|
+
page.render = this.createRenderFunction(page.name);
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
// development mode
|
|
@@ -105,7 +107,7 @@ export class ReactServerProvider {
|
|
|
105
107
|
|
|
106
108
|
// no SSR enabled, serve index.html for all unmatched routes
|
|
107
109
|
this.log.info("SSR is disabled, use History API fallback");
|
|
108
|
-
|
|
110
|
+
this.serverRouterProvider.createRoute({
|
|
109
111
|
path: "*",
|
|
110
112
|
handler: async ({ url, reply }) => {
|
|
111
113
|
if (url.pathname.includes(".")) {
|
|
@@ -127,7 +129,7 @@ export class ReactServerProvider {
|
|
|
127
129
|
|
|
128
130
|
public get template() {
|
|
129
131
|
return (
|
|
130
|
-
this.alepha.
|
|
132
|
+
this.alepha.env.REACT_SERVER_TEMPLATE ??
|
|
131
133
|
"<!DOCTYPE html><html lang='en'><head></head><body></body></html>"
|
|
132
134
|
);
|
|
133
135
|
}
|
|
@@ -140,7 +142,7 @@ export class ReactServerProvider {
|
|
|
140
142
|
|
|
141
143
|
this.log.debug(`+ ${page.match} -> ${page.name}`);
|
|
142
144
|
|
|
143
|
-
|
|
145
|
+
this.serverRouterProvider.createRoute({
|
|
144
146
|
...page,
|
|
145
147
|
schema: undefined, // schema is handled by the page descriptor provider for now (shared by browser and server)
|
|
146
148
|
method: "GET",
|
|
@@ -166,7 +168,7 @@ export class ReactServerProvider {
|
|
|
166
168
|
}
|
|
167
169
|
|
|
168
170
|
protected async configureStaticServer(root: string) {
|
|
169
|
-
await this.serverStaticProvider.
|
|
171
|
+
await this.serverStaticProvider.createStaticServer({
|
|
170
172
|
root,
|
|
171
173
|
path: this.env.REACT_SERVER_PREFIX,
|
|
172
174
|
});
|
|
@@ -262,7 +264,7 @@ export class ReactServerProvider {
|
|
|
262
264
|
};
|
|
263
265
|
|
|
264
266
|
if (this.alepha.has(ServerLinksProvider)) {
|
|
265
|
-
const srv = this.alepha.
|
|
267
|
+
const srv = this.alepha.inject(ServerLinksProvider);
|
|
266
268
|
const schema = apiLinksResponseSchema as any;
|
|
267
269
|
|
|
268
270
|
context.links = this.alepha.parse(
|