@alepha/react 0.7.6 → 0.8.0
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/dist/index.browser.js +25 -73
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +66 -143
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +76 -102
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +82 -108
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +65 -142
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
- package/src/descriptors/$page.ts +26 -63
- package/src/hooks/useClient.ts +3 -3
- package/src/index.browser.ts +4 -0
- package/src/index.ts +18 -4
- package/src/providers/BrowserRouterProvider.ts +4 -5
- package/src/providers/PageDescriptorProvider.ts +25 -61
- package/src/providers/ReactBrowserProvider.ts +4 -15
- package/src/providers/ReactBrowserRenderer.ts +1 -1
- package/src/providers/ReactServerProvider.ts +67 -59
- package/dist/index.browser.d.ts +0 -578
- package/src/providers/BrowserHeadProvider.ts +0 -43
- package/src/providers/ServerHeadProvider.ts +0 -91
package/src/descriptors/$page.ts
CHANGED
|
@@ -96,17 +96,23 @@ export interface PageDescriptorOptions<
|
|
|
96
96
|
*
|
|
97
97
|
* If you still want to render at this pathname, add a child page with an empty path.
|
|
98
98
|
*/
|
|
99
|
-
children?:
|
|
99
|
+
children?:
|
|
100
|
+
| Array<{ [OPTIONS]: PageDescriptorOptions }>
|
|
101
|
+
| (() => Array<{ [OPTIONS]: PageDescriptorOptions }>);
|
|
100
102
|
|
|
101
103
|
parent?: { [OPTIONS]: PageDescriptorOptions<PageConfigSchema, TPropsParent> };
|
|
102
104
|
|
|
103
105
|
can?: () => boolean;
|
|
104
106
|
|
|
105
|
-
head?: Head | ((props: TProps, previous?: Head) => Head);
|
|
106
|
-
|
|
107
107
|
errorHandler?: (error: Error) => ReactNode;
|
|
108
108
|
|
|
109
|
-
|
|
109
|
+
/**
|
|
110
|
+
* If true, the page will be rendered on the build time.
|
|
111
|
+
* Works only with viteAlepha plugin.
|
|
112
|
+
*
|
|
113
|
+
* Replace boolean by an object to define static entries. (e.g. list of params/query)
|
|
114
|
+
*/
|
|
115
|
+
static?:
|
|
110
116
|
| boolean
|
|
111
117
|
| {
|
|
112
118
|
entries?: Array<Partial<PageRequestConfig<TConfig>>>;
|
|
@@ -151,20 +157,20 @@ export const $page = <
|
|
|
151
157
|
): PageDescriptor<TConfig, TProps, TPropsParent> => {
|
|
152
158
|
__descriptor(KEY);
|
|
153
159
|
|
|
154
|
-
if (options.children) {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (options.parent) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
160
|
+
// if (options.children) {
|
|
161
|
+
// for (const child of options.children) {
|
|
162
|
+
// child[OPTIONS].parent = {
|
|
163
|
+
// [OPTIONS]: options as PageDescriptorOptions<any, any, any>,
|
|
164
|
+
// };
|
|
165
|
+
// }
|
|
166
|
+
// }
|
|
167
|
+
|
|
168
|
+
// if (options.parent) {
|
|
169
|
+
// options.parent[OPTIONS].children ??= [];
|
|
170
|
+
// options.parent[OPTIONS].children.push({
|
|
171
|
+
// [OPTIONS]: options as PageDescriptorOptions<any, any, any>,
|
|
172
|
+
// });
|
|
173
|
+
// }
|
|
168
174
|
|
|
169
175
|
return {
|
|
170
176
|
[KIND]: KEY,
|
|
@@ -182,7 +188,8 @@ $page[KIND] = KEY;
|
|
|
182
188
|
export interface PageDescriptorRenderOptions {
|
|
183
189
|
params?: Record<string, string>;
|
|
184
190
|
query?: Record<string, string>;
|
|
185
|
-
|
|
191
|
+
html?: boolean;
|
|
192
|
+
hydration?: boolean;
|
|
186
193
|
}
|
|
187
194
|
|
|
188
195
|
export interface PageDescriptorRenderResult {
|
|
@@ -190,50 +197,6 @@ export interface PageDescriptorRenderResult {
|
|
|
190
197
|
context: PageReactContext;
|
|
191
198
|
}
|
|
192
199
|
|
|
193
|
-
export interface Head {
|
|
194
|
-
title?: string;
|
|
195
|
-
description?: string;
|
|
196
|
-
titleSeparator?: string;
|
|
197
|
-
htmlAttributes?: Record<string, string>;
|
|
198
|
-
bodyAttributes?: Record<string, string>;
|
|
199
|
-
meta?: Array<{ name: string; content: string }>;
|
|
200
|
-
|
|
201
|
-
// TODO
|
|
202
|
-
keywords?: string[];
|
|
203
|
-
author?: string;
|
|
204
|
-
robots?: string;
|
|
205
|
-
themeColor?: string;
|
|
206
|
-
viewport?:
|
|
207
|
-
| string
|
|
208
|
-
| {
|
|
209
|
-
width?: string;
|
|
210
|
-
height?: string;
|
|
211
|
-
initialScale?: string;
|
|
212
|
-
maximumScale?: string;
|
|
213
|
-
userScalable?: "no" | "yes" | "0" | "1";
|
|
214
|
-
interactiveWidget?:
|
|
215
|
-
| "resizes-visual"
|
|
216
|
-
| "resizes-content"
|
|
217
|
-
| "overlays-content";
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
og?: {
|
|
221
|
-
title?: string;
|
|
222
|
-
description?: string;
|
|
223
|
-
image?: string;
|
|
224
|
-
url?: string;
|
|
225
|
-
type?: string;
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
twitter?: {
|
|
229
|
-
card?: string;
|
|
230
|
-
title?: string;
|
|
231
|
-
description?: string;
|
|
232
|
-
image?: string;
|
|
233
|
-
site?: string;
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
|
|
237
200
|
export interface PageRequestConfig<
|
|
238
201
|
TConfig extends PageConfigSchema = PageConfigSchema,
|
|
239
202
|
> {
|
package/src/hooks/useClient.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type ClientScope,
|
|
3
|
-
HttpClient,
|
|
4
3
|
type HttpVirtualClient,
|
|
5
|
-
|
|
4
|
+
LinkProvider,
|
|
5
|
+
} from "@alepha/server-links";
|
|
6
6
|
import { useInject } from "./useInject.ts";
|
|
7
7
|
|
|
8
8
|
export const useClient = <T extends object>(
|
|
9
9
|
_scope?: ClientScope,
|
|
10
10
|
): HttpVirtualClient<T> => {
|
|
11
|
-
return useInject(
|
|
11
|
+
return useInject(LinkProvider).client<T>();
|
|
12
12
|
};
|
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.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,
|
|
@@ -22,21 +23,33 @@ export * from "./providers/ReactServerProvider.ts";
|
|
|
22
23
|
|
|
23
24
|
declare module "@alepha/core" {
|
|
24
25
|
interface Hooks {
|
|
26
|
+
"react:router:createLayers": {
|
|
27
|
+
request: ServerRequest;
|
|
28
|
+
context: PageRequest;
|
|
29
|
+
layers: PageRequest[];
|
|
30
|
+
};
|
|
31
|
+
"react:server:render:begin": {
|
|
32
|
+
request?: ServerRequest;
|
|
33
|
+
context: PageRequest;
|
|
34
|
+
};
|
|
35
|
+
"react:server:render:end": {
|
|
36
|
+
request?: ServerRequest;
|
|
37
|
+
context: PageRequest;
|
|
38
|
+
state: RouterState;
|
|
39
|
+
html: string;
|
|
40
|
+
};
|
|
25
41
|
"react:browser:render": {
|
|
26
42
|
state: RouterState;
|
|
27
43
|
context: PageReactContext;
|
|
28
44
|
hydration?: ReactHydrationState;
|
|
29
45
|
};
|
|
30
|
-
"react:server:render": {
|
|
31
|
-
request: ServerRequest;
|
|
32
|
-
pageRequest: PageRequest;
|
|
33
|
-
};
|
|
34
46
|
"react:transition:begin": {
|
|
35
47
|
state: RouterState;
|
|
36
48
|
context: PageReactContext;
|
|
37
49
|
};
|
|
38
50
|
"react:transition:success": {
|
|
39
51
|
state: RouterState;
|
|
52
|
+
context: PageReactContext;
|
|
40
53
|
};
|
|
41
54
|
"react:transition:error": {
|
|
42
55
|
error: Error;
|
|
@@ -67,6 +80,7 @@ export class AlephaReact implements Module {
|
|
|
67
80
|
alepha
|
|
68
81
|
.with(AlephaServer)
|
|
69
82
|
.with(AlephaServerCache)
|
|
83
|
+
.with(AlephaServerLinks)
|
|
70
84
|
.with(ReactServerProvider)
|
|
71
85
|
.with(PageDescriptorProvider);
|
|
72
86
|
}
|
|
@@ -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
|
|
@@ -53,14 +53,13 @@ export class BrowserRouterProvider extends RouterProvider<BrowserRoute> {
|
|
|
53
53
|
layers: [],
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
const context
|
|
56
|
+
const context = {
|
|
57
57
|
url,
|
|
58
58
|
query: {},
|
|
59
59
|
params: {},
|
|
60
|
-
head: {},
|
|
61
60
|
onError: () => null,
|
|
62
61
|
...(options.context ?? {}),
|
|
63
|
-
};
|
|
62
|
+
} as PageRequest;
|
|
64
63
|
|
|
65
64
|
await this.alepha.emit("react:transition:begin", { state, context });
|
|
66
65
|
|
|
@@ -105,7 +104,7 @@ export class BrowserRouterProvider extends RouterProvider<BrowserRoute> {
|
|
|
105
104
|
});
|
|
106
105
|
}
|
|
107
106
|
|
|
108
|
-
await this.alepha.emit("react:transition:success", { state });
|
|
107
|
+
await this.alepha.emit("react:transition:success", { state, context });
|
|
109
108
|
} catch (e) {
|
|
110
109
|
this.log.error(e);
|
|
111
110
|
state.layers = [
|
|
@@ -8,11 +8,7 @@ import NestedView from "../components/NestedView.tsx";
|
|
|
8
8
|
import NotFoundPage from "../components/NotFound.tsx";
|
|
9
9
|
import { RouterContext } from "../contexts/RouterContext.ts";
|
|
10
10
|
import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
|
|
11
|
-
import {
|
|
12
|
-
$page,
|
|
13
|
-
type Head,
|
|
14
|
-
type PageDescriptorOptions,
|
|
15
|
-
} from "../descriptors/$page.ts";
|
|
11
|
+
import { $page, type PageDescriptorOptions } from "../descriptors/$page.ts";
|
|
16
12
|
import { RedirectionError } from "../errors/RedirectionError.ts";
|
|
17
13
|
|
|
18
14
|
const envSchema = t.object({
|
|
@@ -213,13 +209,6 @@ export class PageDescriptorProvider {
|
|
|
213
209
|
params[key] = String(params[key]);
|
|
214
210
|
}
|
|
215
211
|
|
|
216
|
-
if (it.route.head && !it.error) {
|
|
217
|
-
this.fillHead(it.route, request, {
|
|
218
|
-
...props,
|
|
219
|
-
...context,
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
|
|
223
212
|
acc += "/";
|
|
224
213
|
acc += it.route.path ? this.compile(it.route.path, params) : "";
|
|
225
214
|
const path = acc.replace(/\/+/, "/");
|
|
@@ -244,6 +233,7 @@ export class PageDescriptorProvider {
|
|
|
244
233
|
element: this.renderView(i + 1, path, element, it.route),
|
|
245
234
|
index: i + 1,
|
|
246
235
|
path,
|
|
236
|
+
route,
|
|
247
237
|
});
|
|
248
238
|
break;
|
|
249
239
|
}
|
|
@@ -263,6 +253,7 @@ export class PageDescriptorProvider {
|
|
|
263
253
|
element: this.renderView(i + 1, path, element, it.route),
|
|
264
254
|
index: i + 1,
|
|
265
255
|
path,
|
|
256
|
+
route,
|
|
266
257
|
});
|
|
267
258
|
}
|
|
268
259
|
|
|
@@ -294,51 +285,6 @@ export class PageDescriptorProvider {
|
|
|
294
285
|
return undefined;
|
|
295
286
|
}
|
|
296
287
|
|
|
297
|
-
protected fillHead(
|
|
298
|
-
page: PageRoute,
|
|
299
|
-
ctx: PageRequest,
|
|
300
|
-
props: Record<string, any>,
|
|
301
|
-
): void {
|
|
302
|
-
if (!page.head) {
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
ctx.head ??= {};
|
|
307
|
-
|
|
308
|
-
const head =
|
|
309
|
-
typeof page.head === "function" ? page.head(props, ctx.head) : page.head;
|
|
310
|
-
|
|
311
|
-
if (head.title) {
|
|
312
|
-
ctx.head ??= {};
|
|
313
|
-
|
|
314
|
-
if (ctx.head.titleSeparator) {
|
|
315
|
-
ctx.head.title = `${head.title}${ctx.head.titleSeparator}${ctx.head.title}`;
|
|
316
|
-
} else {
|
|
317
|
-
ctx.head.title = head.title;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
ctx.head.titleSeparator = head.titleSeparator;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (head.htmlAttributes) {
|
|
324
|
-
ctx.head.htmlAttributes = {
|
|
325
|
-
...ctx.head.htmlAttributes,
|
|
326
|
-
...head.htmlAttributes,
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
if (head.bodyAttributes) {
|
|
331
|
-
ctx.head.bodyAttributes = {
|
|
332
|
-
...ctx.head.bodyAttributes,
|
|
333
|
-
...head.bodyAttributes,
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (head.meta) {
|
|
338
|
-
ctx.head.meta = [...(ctx.head.meta ?? []), ...(head.meta ?? [])];
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
288
|
public renderError(error: Error): ReactNode {
|
|
343
289
|
return createElement(ErrorViewer, { error });
|
|
344
290
|
}
|
|
@@ -404,17 +350,31 @@ export class PageDescriptorProvider {
|
|
|
404
350
|
}
|
|
405
351
|
|
|
406
352
|
protected readonly configure = $hook({
|
|
407
|
-
|
|
353
|
+
on: "configure",
|
|
408
354
|
handler: () => {
|
|
409
355
|
let hasNotFoundHandler = false;
|
|
410
356
|
const pages = this.alepha.getDescriptorValues($page);
|
|
357
|
+
|
|
358
|
+
const hasParent = (it: { [OPTIONS]: PageDescriptorOptions }) => {
|
|
359
|
+
for (const page of pages) {
|
|
360
|
+
const children = page.value[OPTIONS].children
|
|
361
|
+
? Array.isArray(page.value[OPTIONS].children)
|
|
362
|
+
? page.value[OPTIONS].children
|
|
363
|
+
: page.value[OPTIONS].children()
|
|
364
|
+
: [];
|
|
365
|
+
if (children.includes(it)) {
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
411
371
|
for (const { value, key } of pages) {
|
|
412
372
|
value[OPTIONS].name ??= key;
|
|
413
373
|
}
|
|
414
374
|
|
|
415
375
|
for (const { value } of pages) {
|
|
416
376
|
// skip children, we only want root pages
|
|
417
|
-
if (value
|
|
377
|
+
if (hasParent(value)) {
|
|
418
378
|
continue;
|
|
419
379
|
}
|
|
420
380
|
|
|
@@ -444,7 +404,11 @@ export class PageDescriptorProvider {
|
|
|
444
404
|
pages: Array<{ value: { [OPTIONS]: PageDescriptorOptions } }>,
|
|
445
405
|
target: { [OPTIONS]: PageDescriptorOptions },
|
|
446
406
|
): PageRouteEntry {
|
|
447
|
-
const children = target[OPTIONS].children
|
|
407
|
+
const children = target[OPTIONS].children
|
|
408
|
+
? Array.isArray(target[OPTIONS].children)
|
|
409
|
+
? target[OPTIONS].children
|
|
410
|
+
: target[OPTIONS].children()
|
|
411
|
+
: [];
|
|
448
412
|
|
|
449
413
|
return {
|
|
450
414
|
...target[OPTIONS],
|
|
@@ -534,6 +498,7 @@ export interface Layer {
|
|
|
534
498
|
element: ReactNode;
|
|
535
499
|
index: number;
|
|
536
500
|
path: string;
|
|
501
|
+
route?: PageRoute;
|
|
537
502
|
}
|
|
538
503
|
|
|
539
504
|
export type PreviousLayerData = Omit<Layer, "element" | "index" | "path">;
|
|
@@ -586,7 +551,6 @@ export interface CreateLayersResult extends RouterState {
|
|
|
586
551
|
*/
|
|
587
552
|
export interface PageReactContext {
|
|
588
553
|
url: URL;
|
|
589
|
-
head: Head;
|
|
590
554
|
onError: (error: Error) => ReactNode;
|
|
591
555
|
links?: ApiLinksResponse;
|
|
592
556
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { $hook, $inject, $logger, Alepha } from "@alepha/core";
|
|
2
|
-
import {
|
|
2
|
+
import type { ApiLinksResponse } from "@alepha/server";
|
|
3
|
+
import { LinkProvider } from "@alepha/server-links";
|
|
3
4
|
import type { Root } from "react-dom/client";
|
|
4
|
-
import { BrowserHeadProvider } from "./BrowserHeadProvider.ts";
|
|
5
5
|
import { BrowserRouterProvider } from "./BrowserRouterProvider.ts";
|
|
6
6
|
import type {
|
|
7
7
|
PreviousLayerData,
|
|
@@ -12,10 +12,9 @@ import type {
|
|
|
12
12
|
|
|
13
13
|
export class ReactBrowserProvider {
|
|
14
14
|
protected readonly log = $logger();
|
|
15
|
-
protected readonly client = $inject(
|
|
15
|
+
protected readonly client = $inject(LinkProvider);
|
|
16
16
|
protected readonly alepha = $inject(Alepha);
|
|
17
17
|
protected readonly router = $inject(BrowserRouterProvider);
|
|
18
|
-
protected readonly headProvider = $inject(BrowserHeadProvider);
|
|
19
18
|
protected root!: Root;
|
|
20
19
|
|
|
21
20
|
public transitioning?: {
|
|
@@ -126,7 +125,7 @@ export class ReactBrowserProvider {
|
|
|
126
125
|
// -------------------------------------------------------------------------------------------------------------------
|
|
127
126
|
|
|
128
127
|
public readonly ready = $hook({
|
|
129
|
-
|
|
128
|
+
on: "ready",
|
|
130
129
|
handler: async () => {
|
|
131
130
|
const hydration = this.getHydrationState();
|
|
132
131
|
const previous = hydration?.layers ?? [];
|
|
@@ -138,9 +137,6 @@ export class ReactBrowserProvider {
|
|
|
138
137
|
}
|
|
139
138
|
|
|
140
139
|
const { context } = await this.render({ previous });
|
|
141
|
-
if (context.head) {
|
|
142
|
-
this.headProvider.renderHead(this.document, context.head);
|
|
143
|
-
}
|
|
144
140
|
|
|
145
141
|
await this.alepha.emit("react:browser:render", {
|
|
146
142
|
state: this.state,
|
|
@@ -153,13 +149,6 @@ export class ReactBrowserProvider {
|
|
|
153
149
|
});
|
|
154
150
|
},
|
|
155
151
|
});
|
|
156
|
-
|
|
157
|
-
public readonly onTransitionEnd = $hook({
|
|
158
|
-
name: "react:transition:end",
|
|
159
|
-
handler: async ({ context }) => {
|
|
160
|
-
this.headProvider.renderHead(this.document, context.head);
|
|
161
|
-
},
|
|
162
|
-
});
|
|
163
152
|
}
|
|
164
153
|
|
|
165
154
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
@@ -43,7 +43,7 @@ export class ReactBrowserRenderer {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
public readonly ready = $hook({
|
|
46
|
-
|
|
46
|
+
on: "react:browser:render",
|
|
47
47
|
handler: async ({ state, context, hydration }) => {
|
|
48
48
|
const element = this.browserRouterProvider.root(state, context);
|
|
49
49
|
|
|
@@ -12,13 +12,16 @@ import {
|
|
|
12
12
|
import {
|
|
13
13
|
apiLinksResponseSchema,
|
|
14
14
|
type ServerHandler,
|
|
15
|
-
ServerLinksProvider,
|
|
16
15
|
ServerRouterProvider,
|
|
17
16
|
ServerTimingProvider,
|
|
18
17
|
} from "@alepha/server";
|
|
18
|
+
import { ServerLinksProvider } from "@alepha/server-links";
|
|
19
19
|
import { ServerStaticProvider } from "@alepha/server-static";
|
|
20
20
|
import { renderToString } from "react-dom/server";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
$page,
|
|
23
|
+
type PageDescriptorRenderOptions,
|
|
24
|
+
} from "../descriptors/$page.ts";
|
|
22
25
|
import {
|
|
23
26
|
PageDescriptorProvider,
|
|
24
27
|
type PageReactContext,
|
|
@@ -27,7 +30,6 @@ import {
|
|
|
27
30
|
type RouterState,
|
|
28
31
|
} from "./PageDescriptorProvider.ts";
|
|
29
32
|
import type { ReactHydrationState } from "./ReactBrowserProvider.ts";
|
|
30
|
-
import { ServerHeadProvider } from "./ServerHeadProvider.ts";
|
|
31
33
|
|
|
32
34
|
const envSchema = t.object({
|
|
33
35
|
REACT_SERVER_DIST: t.string({ default: "public" }),
|
|
@@ -50,7 +52,6 @@ export class ReactServerProvider {
|
|
|
50
52
|
protected readonly pageDescriptorProvider = $inject(PageDescriptorProvider);
|
|
51
53
|
protected readonly serverStaticProvider = $inject(ServerStaticProvider);
|
|
52
54
|
protected readonly serverRouterProvider = $inject(ServerRouterProvider);
|
|
53
|
-
protected readonly headProvider = $inject(ServerHeadProvider);
|
|
54
55
|
protected readonly serverTimingProvider = $inject(ServerTimingProvider);
|
|
55
56
|
protected readonly env = $inject(envSchema);
|
|
56
57
|
protected readonly ROOT_DIV_REGEX = new RegExp(
|
|
@@ -59,7 +60,7 @@ export class ReactServerProvider {
|
|
|
59
60
|
);
|
|
60
61
|
|
|
61
62
|
public readonly onConfigure = $hook({
|
|
62
|
-
|
|
63
|
+
on: "configure",
|
|
63
64
|
handler: async () => {
|
|
64
65
|
const pages = this.alepha.getDescriptorValues($page);
|
|
65
66
|
|
|
@@ -71,11 +72,7 @@ export class ReactServerProvider {
|
|
|
71
72
|
for (const { key, instance, value } of pages) {
|
|
72
73
|
const name = value[OPTIONS].name ?? key;
|
|
73
74
|
|
|
74
|
-
instance[key].
|
|
75
|
-
|
|
76
|
-
if (this.alepha.isTest()) {
|
|
77
|
-
instance[key].render = this.createRenderFunction(name);
|
|
78
|
-
}
|
|
75
|
+
instance[key].render = this.createRenderFunction(name);
|
|
79
76
|
}
|
|
80
77
|
|
|
81
78
|
// development mode
|
|
@@ -129,7 +126,10 @@ export class ReactServerProvider {
|
|
|
129
126
|
});
|
|
130
127
|
|
|
131
128
|
public get template() {
|
|
132
|
-
return
|
|
129
|
+
return (
|
|
130
|
+
this.alepha.state("ReactServerProvider.template") ??
|
|
131
|
+
"<!DOCTYPE html><html lang='en'><head></head><body></body></html>"
|
|
132
|
+
);
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
protected async registerPages(templateLoader: TemplateLoader) {
|
|
@@ -193,12 +193,7 @@ export class ReactServerProvider {
|
|
|
193
193
|
* For testing purposes, creates a render function that can be used.
|
|
194
194
|
*/
|
|
195
195
|
protected createRenderFunction(name: string, withIndex = false) {
|
|
196
|
-
return async (
|
|
197
|
-
options: {
|
|
198
|
-
params?: Record<string, string>;
|
|
199
|
-
query?: Record<string, string>;
|
|
200
|
-
} = {},
|
|
201
|
-
) => {
|
|
196
|
+
return async (options: PageDescriptorRenderOptions = {}) => {
|
|
202
197
|
const page = this.pageDescriptorProvider.page(name);
|
|
203
198
|
const url = new URL(this.pageDescriptorProvider.url(name, options));
|
|
204
199
|
const context: PageRequest = {
|
|
@@ -209,12 +204,16 @@ export class ReactServerProvider {
|
|
|
209
204
|
onError: () => null,
|
|
210
205
|
};
|
|
211
206
|
|
|
207
|
+
await this.alepha.emit("react:server:render:begin", {
|
|
208
|
+
context,
|
|
209
|
+
});
|
|
210
|
+
|
|
212
211
|
const state = await this.pageDescriptorProvider.createLayers(
|
|
213
212
|
page,
|
|
214
213
|
context,
|
|
215
214
|
);
|
|
216
215
|
|
|
217
|
-
if (!withIndex) {
|
|
216
|
+
if (!withIndex && !options.html) {
|
|
218
217
|
return {
|
|
219
218
|
context,
|
|
220
219
|
html: renderToString(
|
|
@@ -223,10 +222,22 @@ export class ReactServerProvider {
|
|
|
223
222
|
};
|
|
224
223
|
}
|
|
225
224
|
|
|
226
|
-
|
|
225
|
+
const html = this.renderToHtml(
|
|
226
|
+
this.template ?? "",
|
|
227
|
+
state,
|
|
227
228
|
context,
|
|
228
|
-
|
|
229
|
+
options.hydration,
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
const result = {
|
|
233
|
+
context,
|
|
234
|
+
state,
|
|
235
|
+
html,
|
|
229
236
|
};
|
|
237
|
+
|
|
238
|
+
await this.alepha.emit("react:server:render:end", result);
|
|
239
|
+
|
|
240
|
+
return result;
|
|
230
241
|
};
|
|
231
242
|
}
|
|
232
243
|
|
|
@@ -287,16 +298,10 @@ export class ReactServerProvider {
|
|
|
287
298
|
// return;
|
|
288
299
|
// }
|
|
289
300
|
|
|
290
|
-
await this.alepha.emit(
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
pageRequest: context,
|
|
295
|
-
},
|
|
296
|
-
{
|
|
297
|
-
log: false,
|
|
298
|
-
},
|
|
299
|
-
);
|
|
301
|
+
await this.alepha.emit("react:server:render:begin", {
|
|
302
|
+
request: serverRequest,
|
|
303
|
+
context,
|
|
304
|
+
});
|
|
300
305
|
|
|
301
306
|
this.serverTimingProvider.beginTiming("createLayers");
|
|
302
307
|
|
|
@@ -327,6 +332,13 @@ export class ReactServerProvider {
|
|
|
327
332
|
|
|
328
333
|
const html = this.renderToHtml(template, state, context);
|
|
329
334
|
|
|
335
|
+
await this.alepha.emit("react:server:render:end", {
|
|
336
|
+
request: serverRequest,
|
|
337
|
+
context,
|
|
338
|
+
state,
|
|
339
|
+
html,
|
|
340
|
+
});
|
|
341
|
+
|
|
330
342
|
page.afterHandler?.(serverRequest);
|
|
331
343
|
|
|
332
344
|
return html;
|
|
@@ -337,6 +349,7 @@ export class ReactServerProvider {
|
|
|
337
349
|
template: string,
|
|
338
350
|
state: RouterState,
|
|
339
351
|
context: PageReactContext,
|
|
352
|
+
hydration = true,
|
|
340
353
|
) {
|
|
341
354
|
const element = this.pageDescriptorProvider.root(state, context);
|
|
342
355
|
|
|
@@ -352,41 +365,36 @@ export class ReactServerProvider {
|
|
|
352
365
|
|
|
353
366
|
this.serverTimingProvider.endTiming("renderToString");
|
|
354
367
|
|
|
355
|
-
const hydrationData: ReactHydrationState = {
|
|
356
|
-
links: context.links,
|
|
357
|
-
layers: state.layers.map((it) => ({
|
|
358
|
-
...it,
|
|
359
|
-
error: it.error
|
|
360
|
-
? {
|
|
361
|
-
...it.error,
|
|
362
|
-
name: it.error.name,
|
|
363
|
-
message: it.error.message,
|
|
364
|
-
stack: it.error.stack, // TODO: Hide stack in production ?
|
|
365
|
-
}
|
|
366
|
-
: undefined,
|
|
367
|
-
index: undefined,
|
|
368
|
-
path: undefined,
|
|
369
|
-
element: undefined,
|
|
370
|
-
})),
|
|
371
|
-
};
|
|
372
|
-
|
|
373
|
-
// create hydration data
|
|
374
|
-
const script = `<script>window.__ssr=${JSON.stringify(hydrationData)}</script>`;
|
|
375
|
-
|
|
376
368
|
const response = {
|
|
377
369
|
html: template,
|
|
378
370
|
};
|
|
379
371
|
|
|
380
|
-
|
|
381
|
-
|
|
372
|
+
if (hydration) {
|
|
373
|
+
const hydrationData: ReactHydrationState = {
|
|
374
|
+
links: context.links,
|
|
375
|
+
layers: state.layers.map((it) => ({
|
|
376
|
+
...it,
|
|
377
|
+
error: it.error
|
|
378
|
+
? {
|
|
379
|
+
...it.error,
|
|
380
|
+
name: it.error.name,
|
|
381
|
+
message: it.error.message,
|
|
382
|
+
stack: it.error.stack, // TODO: Hide stack in production ?
|
|
383
|
+
}
|
|
384
|
+
: undefined,
|
|
385
|
+
index: undefined,
|
|
386
|
+
path: undefined,
|
|
387
|
+
element: undefined,
|
|
388
|
+
route: undefined,
|
|
389
|
+
})),
|
|
390
|
+
};
|
|
382
391
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
response.html = this.headProvider.renderHead(response.html, context.head);
|
|
386
|
-
}
|
|
392
|
+
// create hydration data
|
|
393
|
+
const script = `<script>window.__ssr=${JSON.stringify(hydrationData)}</script>`;
|
|
387
394
|
|
|
388
|
-
|
|
389
|
-
|
|
395
|
+
// inject app into template
|
|
396
|
+
this.fillTemplate(response, app, script);
|
|
397
|
+
}
|
|
390
398
|
|
|
391
399
|
return response.html;
|
|
392
400
|
}
|