@alepha/react 0.9.3 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/dist/index.browser.js +315 -320
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +496 -457
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +276 -258
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +274 -256
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +494 -460
- package/dist/index.js.map +1 -1
- package/package.json +13 -10
- package/src/components/NestedView.tsx +15 -13
- package/src/components/NotFound.tsx +1 -1
- package/src/descriptors/$page.ts +16 -4
- package/src/errors/Redirection.ts +8 -5
- package/src/hooks/useActive.ts +25 -34
- package/src/hooks/useAlepha.ts +16 -2
- package/src/hooks/useClient.ts +7 -4
- package/src/hooks/useInject.ts +4 -1
- package/src/hooks/useQueryParams.ts +9 -6
- package/src/hooks/useRouter.ts +18 -31
- package/src/hooks/useRouterEvents.ts +7 -7
- package/src/hooks/useRouterState.ts +8 -20
- package/src/hooks/useSchema.ts +10 -15
- package/src/hooks/useStore.ts +0 -7
- package/src/index.browser.ts +11 -11
- package/src/index.shared.ts +2 -3
- package/src/index.ts +21 -30
- package/src/providers/ReactBrowserProvider.ts +149 -65
- package/src/providers/ReactBrowserRouterProvider.ts +132 -0
- package/src/providers/{PageDescriptorProvider.ts → ReactPageProvider.ts} +84 -112
- package/src/providers/ReactServerProvider.ts +69 -74
- package/src/{hooks/RouterHookApi.ts → services/ReactRouter.ts} +44 -54
- package/src/contexts/RouterContext.ts +0 -14
- package/src/providers/BrowserRouterProvider.ts +0 -155
- package/src/providers/ReactBrowserRenderer.ts +0 -93
|
@@ -1,20 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
$hook,
|
|
4
|
-
$inject,
|
|
5
|
-
$logger,
|
|
6
|
-
Alepha,
|
|
7
|
-
type Static,
|
|
8
|
-
t,
|
|
9
|
-
} from "@alepha/core";
|
|
10
|
-
import type { ApiLinksResponse } from "@alepha/server";
|
|
1
|
+
import { $env, $hook, $inject, Alepha, type Static, t } from "@alepha/core";
|
|
2
|
+
import { $logger } from "@alepha/logger";
|
|
11
3
|
import { createElement, type ReactNode, StrictMode } from "react";
|
|
12
4
|
import ClientOnly from "../components/ClientOnly.tsx";
|
|
13
5
|
import ErrorViewer from "../components/ErrorViewer.tsx";
|
|
14
6
|
import NestedView from "../components/NestedView.tsx";
|
|
15
7
|
import NotFoundPage from "../components/NotFound.tsx";
|
|
16
8
|
import { AlephaContext } from "../contexts/AlephaContext.ts";
|
|
17
|
-
import { RouterContext } from "../contexts/RouterContext.ts";
|
|
18
9
|
import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
|
|
19
10
|
import {
|
|
20
11
|
$page,
|
|
@@ -23,7 +14,6 @@ import {
|
|
|
23
14
|
type PageDescriptorOptions,
|
|
24
15
|
} from "../descriptors/$page.ts";
|
|
25
16
|
import { Redirection } from "../errors/Redirection.ts";
|
|
26
|
-
import type { HrefLike } from "../hooks/RouterHookApi.ts";
|
|
27
17
|
|
|
28
18
|
const envSchema = t.object({
|
|
29
19
|
REACT_STRICT_MODE: t.boolean({ default: true }),
|
|
@@ -33,7 +23,7 @@ declare module "@alepha/core" {
|
|
|
33
23
|
export interface Env extends Partial<Static<typeof envSchema>> {}
|
|
34
24
|
}
|
|
35
25
|
|
|
36
|
-
export class
|
|
26
|
+
export class ReactPageProvider {
|
|
37
27
|
protected readonly log = $logger();
|
|
38
28
|
protected readonly env = $env(envSchema);
|
|
39
29
|
protected readonly alepha = $inject(Alepha);
|
|
@@ -86,29 +76,20 @@ export class PageDescriptorProvider {
|
|
|
86
76
|
|
|
87
77
|
public url(
|
|
88
78
|
name: string,
|
|
89
|
-
options: { params?: Record<string, string>;
|
|
79
|
+
options: { params?: Record<string, string>; host?: string } = {},
|
|
90
80
|
): URL {
|
|
91
81
|
return new URL(
|
|
92
82
|
this.pathname(name, options),
|
|
93
83
|
// use provided base or default to http://localhost
|
|
94
|
-
options.
|
|
84
|
+
options.host ?? `http://localhost`,
|
|
95
85
|
);
|
|
96
86
|
}
|
|
97
87
|
|
|
98
|
-
public root(state:
|
|
88
|
+
public root(state: ReactRouterState): ReactNode {
|
|
99
89
|
const root = createElement(
|
|
100
90
|
AlephaContext.Provider,
|
|
101
91
|
{ value: this.alepha },
|
|
102
|
-
createElement(
|
|
103
|
-
RouterContext.Provider,
|
|
104
|
-
{
|
|
105
|
-
value: {
|
|
106
|
-
state,
|
|
107
|
-
context,
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
createElement(NestedView, {}, state.layers[0]?.element),
|
|
111
|
-
),
|
|
92
|
+
createElement(NestedView, {}, state.layers[0]?.element),
|
|
112
93
|
);
|
|
113
94
|
|
|
114
95
|
if (this.env.REACT_STRICT_MODE) {
|
|
@@ -118,15 +99,18 @@ export class PageDescriptorProvider {
|
|
|
118
99
|
return root;
|
|
119
100
|
}
|
|
120
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Create a new RouterState based on a given route and request.
|
|
104
|
+
* This method resolves the layers for the route, applying any query and params schemas defined in the route.
|
|
105
|
+
* It also handles errors and redirects.
|
|
106
|
+
*/
|
|
121
107
|
public async createLayers(
|
|
122
108
|
route: PageRoute,
|
|
123
|
-
|
|
109
|
+
state: ReactRouterState,
|
|
110
|
+
previous: PreviousLayerData[] = [],
|
|
124
111
|
): Promise<CreateLayersResult> {
|
|
125
|
-
const { pathname, search } = request.url;
|
|
126
|
-
const layers: Layer[] = []; // result layers
|
|
127
112
|
let context: Record<string, any> = {}; // all props
|
|
128
113
|
const stack: Array<RouterStackItem> = [{ route }]; // stack of routes
|
|
129
|
-
request.onError = (error) => this.renderError(error); // error handler
|
|
130
114
|
|
|
131
115
|
let parent = route.parent;
|
|
132
116
|
while (parent) {
|
|
@@ -143,7 +127,7 @@ export class PageDescriptorProvider {
|
|
|
143
127
|
|
|
144
128
|
try {
|
|
145
129
|
config.query = route.schema?.query
|
|
146
|
-
? this.alepha.parse(route.schema.query,
|
|
130
|
+
? this.alepha.parse(route.schema.query, state.query)
|
|
147
131
|
: {};
|
|
148
132
|
} catch (e) {
|
|
149
133
|
it.error = e as Error;
|
|
@@ -152,7 +136,7 @@ export class PageDescriptorProvider {
|
|
|
152
136
|
|
|
153
137
|
try {
|
|
154
138
|
config.params = route.schema?.params
|
|
155
|
-
? this.alepha.parse(route.schema.params,
|
|
139
|
+
? this.alepha.parse(route.schema.params, state.params)
|
|
156
140
|
: {};
|
|
157
141
|
} catch (e) {
|
|
158
142
|
it.error = e as Error;
|
|
@@ -165,7 +149,6 @@ export class PageDescriptorProvider {
|
|
|
165
149
|
};
|
|
166
150
|
|
|
167
151
|
// check if previous layer is the same, reuse if possible
|
|
168
|
-
const previous = request.previous;
|
|
169
152
|
if (previous?.[i] && !forceRefresh && previous[i].name === route.name) {
|
|
170
153
|
const url = (str?: string) => (str ? str.replace(/\/\/+/g, "/") : "/");
|
|
171
154
|
|
|
@@ -203,7 +186,7 @@ export class PageDescriptorProvider {
|
|
|
203
186
|
try {
|
|
204
187
|
const props =
|
|
205
188
|
(await route.resolve?.({
|
|
206
|
-
...
|
|
189
|
+
...state, // request
|
|
207
190
|
...config, // params, query
|
|
208
191
|
...context, // previous props
|
|
209
192
|
} as any)) ?? {};
|
|
@@ -221,13 +204,12 @@ export class PageDescriptorProvider {
|
|
|
221
204
|
} catch (e) {
|
|
222
205
|
// check if we need to redirect
|
|
223
206
|
if (e instanceof Redirection) {
|
|
224
|
-
return
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
});
|
|
207
|
+
return {
|
|
208
|
+
redirect: e.redirect,
|
|
209
|
+
};
|
|
228
210
|
}
|
|
229
211
|
|
|
230
|
-
this.log.error(e);
|
|
212
|
+
this.log.error("Page resolver has failed", e);
|
|
231
213
|
|
|
232
214
|
it.error = e as Error;
|
|
233
215
|
break;
|
|
@@ -249,8 +231,8 @@ export class PageDescriptorProvider {
|
|
|
249
231
|
const path = acc.replace(/\/+/, "/");
|
|
250
232
|
const localErrorHandler = this.getErrorHandler(it.route);
|
|
251
233
|
if (localErrorHandler) {
|
|
252
|
-
const onErrorParent =
|
|
253
|
-
|
|
234
|
+
const onErrorParent = state.onError;
|
|
235
|
+
state.onError = (error, context) => {
|
|
254
236
|
const result = localErrorHandler(error, context);
|
|
255
237
|
// if nothing happen, call the parent
|
|
256
238
|
if (result === undefined) {
|
|
@@ -260,28 +242,51 @@ export class PageDescriptorProvider {
|
|
|
260
242
|
};
|
|
261
243
|
}
|
|
262
244
|
|
|
245
|
+
// normal use case
|
|
246
|
+
if (!it.error) {
|
|
247
|
+
try {
|
|
248
|
+
const element = await this.createElement(it.route, {
|
|
249
|
+
...props,
|
|
250
|
+
...context,
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
state.layers.push({
|
|
254
|
+
name: it.route.name,
|
|
255
|
+
props,
|
|
256
|
+
part: it.route.path,
|
|
257
|
+
config: it.config,
|
|
258
|
+
element: this.renderView(i + 1, path, element, it.route),
|
|
259
|
+
index: i + 1,
|
|
260
|
+
path,
|
|
261
|
+
route: it.route,
|
|
262
|
+
cache: it.cache,
|
|
263
|
+
});
|
|
264
|
+
} catch (e) {
|
|
265
|
+
it.error = e as Error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
263
269
|
// handler has thrown an error, render an error view
|
|
264
270
|
if (it.error) {
|
|
265
271
|
try {
|
|
266
272
|
let element: ReactNode | Redirection | undefined =
|
|
267
|
-
await
|
|
273
|
+
await state.onError(it.error, state);
|
|
268
274
|
|
|
269
275
|
if (element === undefined) {
|
|
270
276
|
throw it.error;
|
|
271
277
|
}
|
|
272
278
|
|
|
273
279
|
if (element instanceof Redirection) {
|
|
274
|
-
return
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
});
|
|
280
|
+
return {
|
|
281
|
+
redirect: element.redirect,
|
|
282
|
+
};
|
|
278
283
|
}
|
|
279
284
|
|
|
280
285
|
if (element === null) {
|
|
281
286
|
element = this.renderError(it.error);
|
|
282
287
|
}
|
|
283
288
|
|
|
284
|
-
layers.push({
|
|
289
|
+
state.layers.push({
|
|
285
290
|
props,
|
|
286
291
|
error: it.error,
|
|
287
292
|
name: it.route.name,
|
|
@@ -295,47 +300,21 @@ export class PageDescriptorProvider {
|
|
|
295
300
|
break;
|
|
296
301
|
} catch (e) {
|
|
297
302
|
if (e instanceof Redirection) {
|
|
298
|
-
return
|
|
303
|
+
return {
|
|
304
|
+
redirect: e.redirect,
|
|
305
|
+
};
|
|
299
306
|
}
|
|
300
307
|
throw e;
|
|
301
308
|
}
|
|
302
309
|
}
|
|
303
|
-
|
|
304
|
-
// normal use case
|
|
305
|
-
|
|
306
|
-
const element = await this.createElement(it.route, {
|
|
307
|
-
...props,
|
|
308
|
-
...context,
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
layers.push({
|
|
312
|
-
name: it.route.name,
|
|
313
|
-
props,
|
|
314
|
-
part: it.route.path,
|
|
315
|
-
config: it.config,
|
|
316
|
-
element: this.renderView(i + 1, path, element, it.route),
|
|
317
|
-
index: i + 1,
|
|
318
|
-
path,
|
|
319
|
-
route: it.route,
|
|
320
|
-
cache: it.cache,
|
|
321
|
-
});
|
|
322
310
|
}
|
|
323
311
|
|
|
324
|
-
return {
|
|
312
|
+
return { state };
|
|
325
313
|
}
|
|
326
314
|
|
|
327
|
-
protected createRedirectionLayer(
|
|
328
|
-
href: HrefLike,
|
|
329
|
-
context: {
|
|
330
|
-
pathname: string;
|
|
331
|
-
search: string;
|
|
332
|
-
},
|
|
333
|
-
) {
|
|
315
|
+
protected createRedirectionLayer(redirect: string): CreateLayersResult {
|
|
334
316
|
return {
|
|
335
|
-
|
|
336
|
-
redirect: typeof href === "string" ? href : this.href(href),
|
|
337
|
-
pathname: context.pathname,
|
|
338
|
-
search: context.search,
|
|
317
|
+
redirect,
|
|
339
318
|
};
|
|
340
319
|
}
|
|
341
320
|
|
|
@@ -601,16 +580,31 @@ export interface AnchorProps {
|
|
|
601
580
|
onClick: (ev?: any) => any;
|
|
602
581
|
}
|
|
603
582
|
|
|
604
|
-
export interface
|
|
605
|
-
|
|
606
|
-
|
|
583
|
+
export interface ReactRouterState {
|
|
584
|
+
/**
|
|
585
|
+
* Stack of layers for the current page.
|
|
586
|
+
*/
|
|
607
587
|
layers: Array<Layer>;
|
|
608
|
-
}
|
|
609
588
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
589
|
+
/**
|
|
590
|
+
* URL of the current page.
|
|
591
|
+
*/
|
|
592
|
+
url: URL;
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Error handler for the current page.
|
|
596
|
+
*/
|
|
597
|
+
onError: ErrorHandler;
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Params extracted from the URL for the current page.
|
|
601
|
+
*/
|
|
602
|
+
params: Record<string, any>;
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Query parameters extracted from the URL for the current page.
|
|
606
|
+
*/
|
|
607
|
+
query: Record<string, string>;
|
|
614
608
|
}
|
|
615
609
|
|
|
616
610
|
export interface RouterStackItem {
|
|
@@ -621,33 +615,11 @@ export interface RouterStackItem {
|
|
|
621
615
|
cache?: boolean;
|
|
622
616
|
}
|
|
623
617
|
|
|
624
|
-
export interface
|
|
625
|
-
state: RouterState;
|
|
626
|
-
context: PageReactContext;
|
|
627
|
-
redirect?: string;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
export interface PageRequest extends PageReactContext {
|
|
631
|
-
params: Record<string, any>;
|
|
632
|
-
query: Record<string, string>;
|
|
633
|
-
|
|
634
|
-
// previous layers (browser history or browser hydration, always null on server)
|
|
618
|
+
export interface TransitionOptions {
|
|
635
619
|
previous?: PreviousLayerData[];
|
|
636
620
|
}
|
|
637
621
|
|
|
638
|
-
export interface CreateLayersResult
|
|
622
|
+
export interface CreateLayersResult {
|
|
639
623
|
redirect?: string;
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
/**
|
|
643
|
-
* It's like RouterState, but publicly available in React context.
|
|
644
|
-
* This is where we store all plugin data!
|
|
645
|
-
*/
|
|
646
|
-
export interface PageReactContext {
|
|
647
|
-
url: URL;
|
|
648
|
-
onError: ErrorHandler;
|
|
649
|
-
links?: ApiLinksResponse;
|
|
650
|
-
|
|
651
|
-
params: Record<string, any>;
|
|
652
|
-
query: Record<string, string>;
|
|
624
|
+
state?: ReactRouterState;
|
|
653
625
|
}
|
|
@@ -4,13 +4,13 @@ import {
|
|
|
4
4
|
$env,
|
|
5
5
|
$hook,
|
|
6
6
|
$inject,
|
|
7
|
-
$logger,
|
|
8
7
|
Alepha,
|
|
8
|
+
AlephaError,
|
|
9
9
|
type Static,
|
|
10
10
|
t,
|
|
11
11
|
} from "@alepha/core";
|
|
12
|
+
import { $logger } from "@alepha/logger";
|
|
12
13
|
import {
|
|
13
|
-
apiLinksResponseSchema,
|
|
14
14
|
type ServerHandler,
|
|
15
15
|
ServerRouterProvider,
|
|
16
16
|
ServerTimingProvider,
|
|
@@ -23,14 +23,12 @@ import {
|
|
|
23
23
|
type PageDescriptorRenderOptions,
|
|
24
24
|
} from "../descriptors/$page.ts";
|
|
25
25
|
import { Redirection } from "../errors/Redirection.ts";
|
|
26
|
+
import type { ReactHydrationState } from "./ReactBrowserProvider.ts";
|
|
26
27
|
import {
|
|
27
|
-
PageDescriptorProvider,
|
|
28
|
-
type PageReactContext,
|
|
29
|
-
type PageRequest,
|
|
30
28
|
type PageRoute,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
ReactPageProvider,
|
|
30
|
+
type ReactRouterState,
|
|
31
|
+
} from "./ReactPageProvider.ts";
|
|
34
32
|
|
|
35
33
|
const envSchema = t.object({
|
|
36
34
|
REACT_SERVER_DIST: t.string({ default: "public" }),
|
|
@@ -54,7 +52,7 @@ declare module "@alepha/core" {
|
|
|
54
52
|
export class ReactServerProvider {
|
|
55
53
|
protected readonly log = $logger();
|
|
56
54
|
protected readonly alepha = $inject(Alepha);
|
|
57
|
-
protected readonly
|
|
55
|
+
protected readonly pageApi = $inject(ReactPageProvider);
|
|
58
56
|
protected readonly serverStaticProvider = $inject(ServerStaticProvider);
|
|
59
57
|
protected readonly serverRouterProvider = $inject(ServerRouterProvider);
|
|
60
58
|
protected readonly serverTimingProvider = $inject(ServerTimingProvider);
|
|
@@ -136,7 +134,7 @@ export class ReactServerProvider {
|
|
|
136
134
|
}
|
|
137
135
|
|
|
138
136
|
protected async registerPages(templateLoader: TemplateLoader) {
|
|
139
|
-
for (const page of this.
|
|
137
|
+
for (const page of this.pageApi.getPages()) {
|
|
140
138
|
if (page.children?.length) {
|
|
141
139
|
continue;
|
|
142
140
|
}
|
|
@@ -197,38 +195,48 @@ export class ReactServerProvider {
|
|
|
197
195
|
*/
|
|
198
196
|
protected createRenderFunction(name: string, withIndex = false) {
|
|
199
197
|
return async (options: PageDescriptorRenderOptions = {}) => {
|
|
200
|
-
const page = this.
|
|
201
|
-
const url = new URL(this.
|
|
202
|
-
|
|
198
|
+
const page = this.pageApi.page(name);
|
|
199
|
+
const url = new URL(this.pageApi.url(name, options));
|
|
200
|
+
|
|
201
|
+
const entry: Partial<ReactRouterState> = {
|
|
203
202
|
url,
|
|
204
203
|
params: options.params ?? {},
|
|
205
204
|
query: options.query ?? {},
|
|
206
|
-
head: {},
|
|
207
205
|
onError: () => null,
|
|
206
|
+
layers: [],
|
|
208
207
|
};
|
|
209
208
|
|
|
209
|
+
const state = entry as ReactRouterState;
|
|
210
|
+
|
|
211
|
+
this.log.trace("Rendering", {
|
|
212
|
+
url,
|
|
213
|
+
});
|
|
214
|
+
|
|
210
215
|
await this.alepha.emit("react:server:render:begin", {
|
|
211
|
-
|
|
216
|
+
state,
|
|
212
217
|
});
|
|
213
218
|
|
|
214
|
-
const
|
|
219
|
+
const { redirect } = await this.pageApi.createLayers(
|
|
215
220
|
page,
|
|
216
|
-
|
|
221
|
+
state as ReactRouterState,
|
|
217
222
|
);
|
|
218
223
|
|
|
224
|
+
if (redirect) {
|
|
225
|
+
throw new AlephaError("Redirection is not supported in this context");
|
|
226
|
+
}
|
|
227
|
+
|
|
219
228
|
if (!withIndex && !options.html) {
|
|
229
|
+
this.alepha.state("react.router.state", state);
|
|
230
|
+
|
|
220
231
|
return {
|
|
221
|
-
|
|
222
|
-
html: renderToString(
|
|
223
|
-
this.pageDescriptorProvider.root(state, context),
|
|
224
|
-
),
|
|
232
|
+
state,
|
|
233
|
+
html: renderToString(this.pageApi.root(state)),
|
|
225
234
|
};
|
|
226
235
|
}
|
|
227
236
|
|
|
228
237
|
const html = this.renderToHtml(
|
|
229
238
|
this.template ?? "",
|
|
230
239
|
state,
|
|
231
|
-
context,
|
|
232
240
|
options.hydration,
|
|
233
241
|
);
|
|
234
242
|
|
|
@@ -237,7 +245,6 @@ export class ReactServerProvider {
|
|
|
237
245
|
}
|
|
238
246
|
|
|
239
247
|
const result = {
|
|
240
|
-
context,
|
|
241
248
|
state,
|
|
242
249
|
html,
|
|
243
250
|
};
|
|
@@ -249,7 +256,7 @@ export class ReactServerProvider {
|
|
|
249
256
|
}
|
|
250
257
|
|
|
251
258
|
protected createHandler(
|
|
252
|
-
|
|
259
|
+
route: PageRoute,
|
|
253
260
|
templateLoader: TemplateLoader,
|
|
254
261
|
): ServerHandler {
|
|
255
262
|
return async (serverRequest) => {
|
|
@@ -260,36 +267,32 @@ export class ReactServerProvider {
|
|
|
260
267
|
}
|
|
261
268
|
|
|
262
269
|
this.log.trace("Rendering page", {
|
|
263
|
-
name:
|
|
270
|
+
name: route.name,
|
|
264
271
|
});
|
|
265
272
|
|
|
266
|
-
const
|
|
273
|
+
const entry: Partial<ReactRouterState> = {
|
|
267
274
|
url,
|
|
268
275
|
params,
|
|
269
276
|
query,
|
|
270
|
-
// plugins
|
|
271
|
-
head: {},
|
|
272
277
|
onError: () => null,
|
|
278
|
+
layers: [],
|
|
273
279
|
};
|
|
274
280
|
|
|
275
|
-
|
|
276
|
-
const srv = this.alepha.inject(ServerLinksProvider);
|
|
277
|
-
const schema = apiLinksResponseSchema as any;
|
|
281
|
+
const state = entry as ReactRouterState;
|
|
278
282
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
+
if (this.alepha.has(ServerLinksProvider)) {
|
|
284
|
+
this.alepha.state(
|
|
285
|
+
"api",
|
|
286
|
+
await this.alepha.inject(ServerLinksProvider).getUserApiLinks({
|
|
287
|
+
user: (serverRequest as any).user, // TODO: fix type
|
|
283
288
|
authorization: serverRequest.headers.authorization,
|
|
284
289
|
}),
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
this.alepha.context.set("links", context.links);
|
|
290
|
+
);
|
|
288
291
|
}
|
|
289
292
|
|
|
290
|
-
let target: PageRoute | undefined =
|
|
293
|
+
let target: PageRoute | undefined = route; // TODO: move to PageDescriptorProvider
|
|
291
294
|
while (target) {
|
|
292
|
-
if (
|
|
295
|
+
if (route.can && !route.can()) {
|
|
293
296
|
// if the page is not accessible, return 403
|
|
294
297
|
reply.status = 403;
|
|
295
298
|
reply.headers["content-type"] = "text/plain";
|
|
@@ -309,27 +312,19 @@ export class ReactServerProvider {
|
|
|
309
312
|
// return;
|
|
310
313
|
// }
|
|
311
314
|
|
|
312
|
-
await this.alepha.emit("react:transition:begin", {
|
|
313
|
-
request: serverRequest,
|
|
314
|
-
context,
|
|
315
|
-
});
|
|
316
|
-
|
|
317
315
|
await this.alepha.emit("react:server:render:begin", {
|
|
318
316
|
request: serverRequest,
|
|
319
|
-
|
|
317
|
+
state,
|
|
320
318
|
});
|
|
321
319
|
|
|
322
320
|
this.serverTimingProvider.beginTiming("createLayers");
|
|
323
321
|
|
|
324
|
-
const
|
|
325
|
-
page,
|
|
326
|
-
context,
|
|
327
|
-
);
|
|
322
|
+
const { redirect } = await this.pageApi.createLayers(route, state);
|
|
328
323
|
|
|
329
324
|
this.serverTimingProvider.endTiming("createLayers");
|
|
330
325
|
|
|
331
|
-
if (
|
|
332
|
-
return reply.redirect(
|
|
326
|
+
if (redirect) {
|
|
327
|
+
return reply.redirect(redirect);
|
|
333
328
|
}
|
|
334
329
|
|
|
335
330
|
reply.headers["content-type"] = "text/html";
|
|
@@ -341,34 +336,28 @@ export class ReactServerProvider {
|
|
|
341
336
|
reply.headers.pragma = "no-cache";
|
|
342
337
|
reply.headers.expires = "0";
|
|
343
338
|
|
|
344
|
-
|
|
345
|
-
if (page.cache && serverRequest.user) {
|
|
346
|
-
delete context.links;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
const html = this.renderToHtml(template, state, context);
|
|
339
|
+
const html = this.renderToHtml(template, state);
|
|
350
340
|
if (html instanceof Redirection) {
|
|
351
341
|
reply.redirect(
|
|
352
|
-
typeof html.
|
|
353
|
-
? html.
|
|
354
|
-
: this.
|
|
342
|
+
typeof html.redirect === "string"
|
|
343
|
+
? html.redirect
|
|
344
|
+
: this.pageApi.href(html.redirect),
|
|
355
345
|
);
|
|
356
346
|
return;
|
|
357
347
|
}
|
|
358
348
|
|
|
359
349
|
const event = {
|
|
360
350
|
request: serverRequest,
|
|
361
|
-
context,
|
|
362
351
|
state,
|
|
363
352
|
html,
|
|
364
353
|
};
|
|
365
354
|
|
|
366
355
|
await this.alepha.emit("react:server:render:end", event);
|
|
367
356
|
|
|
368
|
-
|
|
357
|
+
route.onServerResponse?.(serverRequest);
|
|
369
358
|
|
|
370
359
|
this.log.trace("Page rendered", {
|
|
371
|
-
name:
|
|
360
|
+
name: route.name,
|
|
372
361
|
});
|
|
373
362
|
|
|
374
363
|
return event.html;
|
|
@@ -377,28 +366,32 @@ export class ReactServerProvider {
|
|
|
377
366
|
|
|
378
367
|
public renderToHtml(
|
|
379
368
|
template: string,
|
|
380
|
-
state:
|
|
381
|
-
context: PageReactContext,
|
|
369
|
+
state: ReactRouterState,
|
|
382
370
|
hydration = true,
|
|
383
371
|
): string | Redirection {
|
|
384
|
-
const element = this.
|
|
372
|
+
const element = this.pageApi.root(state);
|
|
385
373
|
|
|
386
|
-
|
|
374
|
+
// attach react router state to the http request context
|
|
375
|
+
this.alepha.state("react.router.state", state);
|
|
387
376
|
|
|
377
|
+
this.serverTimingProvider.beginTiming("renderToString");
|
|
388
378
|
let app = "";
|
|
389
379
|
try {
|
|
390
380
|
app = renderToString(element);
|
|
391
381
|
} catch (error) {
|
|
392
|
-
this.log.error(
|
|
393
|
-
|
|
382
|
+
this.log.error(
|
|
383
|
+
"renderToString has failed, fallback to error handler",
|
|
384
|
+
error,
|
|
385
|
+
);
|
|
386
|
+
const element = state.onError(error as Error, state);
|
|
394
387
|
if (element instanceof Redirection) {
|
|
395
388
|
// if the error is a redirection, return the redirection URL
|
|
396
389
|
return element;
|
|
397
390
|
}
|
|
398
391
|
|
|
399
392
|
app = renderToString(element);
|
|
393
|
+
this.log.debug("Error handled successfully with fallback");
|
|
400
394
|
}
|
|
401
|
-
|
|
402
395
|
this.serverTimingProvider.endTiming("renderToString");
|
|
403
396
|
|
|
404
397
|
const response = {
|
|
@@ -406,11 +399,13 @@ export class ReactServerProvider {
|
|
|
406
399
|
};
|
|
407
400
|
|
|
408
401
|
if (hydration) {
|
|
409
|
-
const { request, context, ...
|
|
410
|
-
this.alepha.context.als?.getStore() ?? {};
|
|
402
|
+
const { request, context, ...store } =
|
|
403
|
+
this.alepha.context.als?.getStore() ?? {}; /// TODO: als must be protected, find a way to iterate on alepha.state
|
|
411
404
|
|
|
412
405
|
const hydrationData: ReactHydrationState = {
|
|
413
|
-
...
|
|
406
|
+
...store,
|
|
407
|
+
// map react.router.state to the hydration state
|
|
408
|
+
"react.router.state": undefined,
|
|
414
409
|
layers: state.layers.map((it) => ({
|
|
415
410
|
...it,
|
|
416
411
|
error: it.error
|