@alepha/react 0.9.2 → 0.9.3
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 +136 -78
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +159 -86
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +232 -158
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +230 -156
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +160 -87
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
- package/src/components/ErrorViewer.tsx +1 -1
- package/src/components/Link.tsx +4 -24
- package/src/components/NestedView.tsx +11 -2
- package/src/components/NotFound.tsx +4 -1
- package/src/descriptors/$page.ts +71 -9
- package/src/errors/{RedirectionError.ts → Redirection.ts} +1 -1
- package/src/hooks/RouterHookApi.ts +35 -11
- package/src/hooks/useActive.ts +22 -15
- package/src/hooks/useClient.ts +2 -0
- package/src/hooks/useRouter.ts +2 -1
- package/src/hooks/useRouterEvents.ts +5 -2
- package/src/hooks/useStore.ts +9 -1
- package/src/index.shared.ts +2 -2
- package/src/providers/BrowserRouterProvider.ts +9 -0
- package/src/providers/PageDescriptorProvider.ts +111 -31
- package/src/providers/ReactBrowserProvider.ts +17 -11
- package/src/providers/ReactServerProvider.ts +47 -10
|
@@ -18,10 +18,12 @@ import { RouterContext } from "../contexts/RouterContext.ts";
|
|
|
18
18
|
import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
|
|
19
19
|
import {
|
|
20
20
|
$page,
|
|
21
|
+
type ErrorHandler,
|
|
21
22
|
type PageDescriptor,
|
|
22
23
|
type PageDescriptorOptions,
|
|
23
24
|
} from "../descriptors/$page.ts";
|
|
24
|
-
import {
|
|
25
|
+
import { Redirection } from "../errors/Redirection.ts";
|
|
26
|
+
import type { HrefLike } from "../hooks/RouterHookApi.ts";
|
|
25
27
|
|
|
26
28
|
const envSchema = t.object({
|
|
27
29
|
REACT_STRICT_MODE: t.boolean({ default: true }),
|
|
@@ -51,10 +53,13 @@ export class PageDescriptorProvider {
|
|
|
51
53
|
throw new Error(`Page ${name} not found`);
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
public
|
|
56
|
+
public pathname(
|
|
55
57
|
name: string,
|
|
56
|
-
options: {
|
|
57
|
-
|
|
58
|
+
options: {
|
|
59
|
+
params?: Record<string, string>;
|
|
60
|
+
query?: Record<string, string>;
|
|
61
|
+
} = {},
|
|
62
|
+
) {
|
|
58
63
|
const page = this.page(name);
|
|
59
64
|
if (!page) {
|
|
60
65
|
throw new Error(`Page ${name} not found`);
|
|
@@ -69,8 +74,23 @@ export class PageDescriptorProvider {
|
|
|
69
74
|
|
|
70
75
|
url = this.compile(url, options.params ?? {});
|
|
71
76
|
|
|
77
|
+
if (options.query) {
|
|
78
|
+
const query = new URLSearchParams(options.query);
|
|
79
|
+
if (query.toString()) {
|
|
80
|
+
url += `?${query.toString()}`;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return url.replace(/\/\/+/g, "/") || "/";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public url(
|
|
88
|
+
name: string,
|
|
89
|
+
options: { params?: Record<string, string>; base?: string } = {},
|
|
90
|
+
): URL {
|
|
72
91
|
return new URL(
|
|
73
|
-
|
|
92
|
+
this.pathname(name, options),
|
|
93
|
+
// use provided base or default to http://localhost
|
|
74
94
|
options.base ?? `http://localhost`,
|
|
75
95
|
);
|
|
76
96
|
}
|
|
@@ -200,13 +220,11 @@ export class PageDescriptorProvider {
|
|
|
200
220
|
};
|
|
201
221
|
} catch (e) {
|
|
202
222
|
// check if we need to redirect
|
|
203
|
-
if (e instanceof
|
|
204
|
-
return {
|
|
205
|
-
layers: [],
|
|
206
|
-
redirect: typeof e.page === "string" ? e.page : this.href(e.page),
|
|
223
|
+
if (e instanceof Redirection) {
|
|
224
|
+
return this.createRedirectionLayer(e.page, {
|
|
207
225
|
pathname,
|
|
208
226
|
search,
|
|
209
|
-
};
|
|
227
|
+
});
|
|
210
228
|
}
|
|
211
229
|
|
|
212
230
|
this.log.error(e);
|
|
@@ -231,28 +249,56 @@ export class PageDescriptorProvider {
|
|
|
231
249
|
const path = acc.replace(/\/+/, "/");
|
|
232
250
|
const localErrorHandler = this.getErrorHandler(it.route);
|
|
233
251
|
if (localErrorHandler) {
|
|
234
|
-
request.onError
|
|
252
|
+
const onErrorParent = request.onError;
|
|
253
|
+
request.onError = (error, context) => {
|
|
254
|
+
const result = localErrorHandler(error, context);
|
|
255
|
+
// if nothing happen, call the parent
|
|
256
|
+
if (result === undefined) {
|
|
257
|
+
return onErrorParent(error, context);
|
|
258
|
+
}
|
|
259
|
+
return result;
|
|
260
|
+
};
|
|
235
261
|
}
|
|
236
262
|
|
|
237
263
|
// handler has thrown an error, render an error view
|
|
238
264
|
if (it.error) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
265
|
+
try {
|
|
266
|
+
let element: ReactNode | Redirection | undefined =
|
|
267
|
+
await request.onError(it.error, request);
|
|
243
268
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
269
|
+
if (element === undefined) {
|
|
270
|
+
throw it.error;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (element instanceof Redirection) {
|
|
274
|
+
return this.createRedirectionLayer(element.page, {
|
|
275
|
+
pathname,
|
|
276
|
+
search,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (element === null) {
|
|
281
|
+
element = this.renderError(it.error);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
layers.push({
|
|
285
|
+
props,
|
|
286
|
+
error: it.error,
|
|
287
|
+
name: it.route.name,
|
|
288
|
+
part: it.route.path,
|
|
289
|
+
config: it.config,
|
|
290
|
+
element: this.renderView(i + 1, path, element, it.route),
|
|
291
|
+
index: i + 1,
|
|
292
|
+
path,
|
|
293
|
+
route: it.route,
|
|
294
|
+
});
|
|
295
|
+
break;
|
|
296
|
+
} catch (e) {
|
|
297
|
+
if (e instanceof Redirection) {
|
|
298
|
+
return this.createRedirectionLayer(e.page, { pathname, search });
|
|
299
|
+
}
|
|
300
|
+
throw e;
|
|
301
|
+
}
|
|
256
302
|
}
|
|
257
303
|
|
|
258
304
|
// normal use case
|
|
@@ -278,7 +324,22 @@ export class PageDescriptorProvider {
|
|
|
278
324
|
return { layers, pathname, search };
|
|
279
325
|
}
|
|
280
326
|
|
|
281
|
-
protected
|
|
327
|
+
protected createRedirectionLayer(
|
|
328
|
+
href: HrefLike,
|
|
329
|
+
context: {
|
|
330
|
+
pathname: string;
|
|
331
|
+
search: string;
|
|
332
|
+
},
|
|
333
|
+
) {
|
|
334
|
+
return {
|
|
335
|
+
layers: [],
|
|
336
|
+
redirect: typeof href === "string" ? href : this.href(href),
|
|
337
|
+
pathname: context.pathname,
|
|
338
|
+
search: context.search,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
protected getErrorHandler(route: PageRoute): ErrorHandler | undefined {
|
|
282
343
|
if (route.errorHandler) return route.errorHandler;
|
|
283
344
|
let parent = route.parent;
|
|
284
345
|
while (parent) {
|
|
@@ -374,6 +435,10 @@ export class PageDescriptorProvider {
|
|
|
374
435
|
const pages = this.alepha.descriptors($page);
|
|
375
436
|
|
|
376
437
|
const hasParent = (it: PageDescriptor) => {
|
|
438
|
+
if (it.options.parent) {
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
|
|
377
442
|
for (const page of pages) {
|
|
378
443
|
const children = page.options.children
|
|
379
444
|
? Array.isArray(page.options.children)
|
|
@@ -406,7 +471,7 @@ export class PageDescriptorProvider {
|
|
|
406
471
|
name: "notFound",
|
|
407
472
|
cache: true,
|
|
408
473
|
component: NotFoundPage,
|
|
409
|
-
|
|
474
|
+
onServerResponse: ({ reply }) => {
|
|
410
475
|
reply.status = 404;
|
|
411
476
|
},
|
|
412
477
|
});
|
|
@@ -424,6 +489,18 @@ export class PageDescriptorProvider {
|
|
|
424
489
|
: target.options.children()
|
|
425
490
|
: [];
|
|
426
491
|
|
|
492
|
+
const getChildrenFromParent = (it: PageDescriptor): PageDescriptor[] => {
|
|
493
|
+
const children = [];
|
|
494
|
+
for (const page of pages) {
|
|
495
|
+
if (page.options.parent === it) {
|
|
496
|
+
children.push(page);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
return children;
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
children.push(...getChildrenFromParent(target));
|
|
503
|
+
|
|
427
504
|
return {
|
|
428
505
|
...target.options,
|
|
429
506
|
name: target.name,
|
|
@@ -521,7 +598,7 @@ export type PreviousLayerData = Omit<Layer, "element" | "index" | "path">;
|
|
|
521
598
|
|
|
522
599
|
export interface AnchorProps {
|
|
523
600
|
href: string;
|
|
524
|
-
onClick: (ev
|
|
601
|
+
onClick: (ev?: any) => any;
|
|
525
602
|
}
|
|
526
603
|
|
|
527
604
|
export interface RouterState {
|
|
@@ -568,6 +645,9 @@ export interface CreateLayersResult extends RouterState {
|
|
|
568
645
|
*/
|
|
569
646
|
export interface PageReactContext {
|
|
570
647
|
url: URL;
|
|
571
|
-
onError:
|
|
648
|
+
onError: ErrorHandler;
|
|
572
649
|
links?: ApiLinksResponse;
|
|
650
|
+
|
|
651
|
+
params: Record<string, any>;
|
|
652
|
+
query: Record<string, string>;
|
|
573
653
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $hook, $inject, $logger, Alepha } from "@alepha/core";
|
|
1
|
+
import { $hook, $inject, $logger, Alepha, type State } from "@alepha/core";
|
|
2
2
|
import type { ApiLinksResponse } from "@alepha/server";
|
|
3
3
|
import { LinkProvider } from "@alepha/server-links";
|
|
4
4
|
import type { Root } from "react-dom/client";
|
|
@@ -97,18 +97,12 @@ export class ReactBrowserProvider {
|
|
|
97
97
|
});
|
|
98
98
|
|
|
99
99
|
// when redirecting in browser
|
|
100
|
-
if (result.context.url.pathname !== url) {
|
|
101
|
-
|
|
102
|
-
this.pushState(result.context.url.pathname);
|
|
100
|
+
if (result.context.url.pathname + result.context.url.search !== url) {
|
|
101
|
+
this.pushState(result.context.url.pathname + result.context.url.search);
|
|
103
102
|
return;
|
|
104
103
|
}
|
|
105
104
|
|
|
106
|
-
|
|
107
|
-
this.pushState(url);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
this.pushState(url);
|
|
105
|
+
this.pushState(url, options.replace);
|
|
112
106
|
}
|
|
113
107
|
|
|
114
108
|
protected async render(
|
|
@@ -157,9 +151,20 @@ export class ReactBrowserProvider {
|
|
|
157
151
|
const hydration = this.getHydrationState();
|
|
158
152
|
const previous = hydration?.layers ?? [];
|
|
159
153
|
|
|
154
|
+
if (hydration) {
|
|
155
|
+
for (const [key, value] of Object.entries(hydration)) {
|
|
156
|
+
if (key !== "layers" && key !== "links") {
|
|
157
|
+
this.alepha.state(key as keyof State, value);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
160
162
|
if (hydration?.links) {
|
|
161
163
|
for (const link of hydration.links.links) {
|
|
162
|
-
this.client.pushLink(
|
|
164
|
+
this.client.pushLink({
|
|
165
|
+
...link,
|
|
166
|
+
prefix: hydration.links.prefix,
|
|
167
|
+
});
|
|
163
168
|
}
|
|
164
169
|
}
|
|
165
170
|
|
|
@@ -190,6 +195,7 @@ export interface RouterGoOptions {
|
|
|
190
195
|
replace?: boolean;
|
|
191
196
|
match?: TransitionOptions;
|
|
192
197
|
params?: Record<string, string>;
|
|
198
|
+
query?: Record<string, string>;
|
|
193
199
|
}
|
|
194
200
|
|
|
195
201
|
export interface ReactHydrationState {
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
$page,
|
|
23
23
|
type PageDescriptorRenderOptions,
|
|
24
24
|
} from "../descriptors/$page.ts";
|
|
25
|
+
import { Redirection } from "../errors/Redirection.ts";
|
|
25
26
|
import {
|
|
26
27
|
PageDescriptorProvider,
|
|
27
28
|
type PageReactContext,
|
|
@@ -231,6 +232,10 @@ export class ReactServerProvider {
|
|
|
231
232
|
options.hydration,
|
|
232
233
|
);
|
|
233
234
|
|
|
235
|
+
if (html instanceof Redirection) {
|
|
236
|
+
throw new Error("Redirection is not supported in this context");
|
|
237
|
+
}
|
|
238
|
+
|
|
234
239
|
const result = {
|
|
235
240
|
context,
|
|
236
241
|
state,
|
|
@@ -254,6 +259,10 @@ export class ReactServerProvider {
|
|
|
254
259
|
throw new Error("Template not found");
|
|
255
260
|
}
|
|
256
261
|
|
|
262
|
+
this.log.trace("Rendering page", {
|
|
263
|
+
name: page.name,
|
|
264
|
+
});
|
|
265
|
+
|
|
257
266
|
const context: PageRequest = {
|
|
258
267
|
url,
|
|
259
268
|
params,
|
|
@@ -300,6 +309,11 @@ export class ReactServerProvider {
|
|
|
300
309
|
// return;
|
|
301
310
|
// }
|
|
302
311
|
|
|
312
|
+
await this.alepha.emit("react:transition:begin", {
|
|
313
|
+
request: serverRequest,
|
|
314
|
+
context,
|
|
315
|
+
});
|
|
316
|
+
|
|
303
317
|
await this.alepha.emit("react:server:render:begin", {
|
|
304
318
|
request: serverRequest,
|
|
305
319
|
context,
|
|
@@ -333,17 +347,31 @@ export class ReactServerProvider {
|
|
|
333
347
|
}
|
|
334
348
|
|
|
335
349
|
const html = this.renderToHtml(template, state, context);
|
|
350
|
+
if (html instanceof Redirection) {
|
|
351
|
+
reply.redirect(
|
|
352
|
+
typeof html.page === "string"
|
|
353
|
+
? html.page
|
|
354
|
+
: this.pageDescriptorProvider.href(html.page),
|
|
355
|
+
);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
336
358
|
|
|
337
|
-
|
|
359
|
+
const event = {
|
|
338
360
|
request: serverRequest,
|
|
339
361
|
context,
|
|
340
362
|
state,
|
|
341
363
|
html,
|
|
342
|
-
}
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
await this.alepha.emit("react:server:render:end", event);
|
|
343
367
|
|
|
344
|
-
page.
|
|
368
|
+
page.onServerResponse?.(serverRequest);
|
|
369
|
+
|
|
370
|
+
this.log.trace("Page rendered", {
|
|
371
|
+
name: page.name,
|
|
372
|
+
});
|
|
345
373
|
|
|
346
|
-
return html;
|
|
374
|
+
return event.html;
|
|
347
375
|
};
|
|
348
376
|
}
|
|
349
377
|
|
|
@@ -352,7 +380,7 @@ export class ReactServerProvider {
|
|
|
352
380
|
state: RouterState,
|
|
353
381
|
context: PageReactContext,
|
|
354
382
|
hydration = true,
|
|
355
|
-
) {
|
|
383
|
+
): string | Redirection {
|
|
356
384
|
const element = this.pageDescriptorProvider.root(state, context);
|
|
357
385
|
|
|
358
386
|
this.serverTimingProvider.beginTiming("renderToString");
|
|
@@ -362,7 +390,13 @@ export class ReactServerProvider {
|
|
|
362
390
|
app = renderToString(element);
|
|
363
391
|
} catch (error) {
|
|
364
392
|
this.log.error("Error during SSR", error);
|
|
365
|
-
|
|
393
|
+
const element = context.onError(error as Error, context);
|
|
394
|
+
if (element instanceof Redirection) {
|
|
395
|
+
// if the error is a redirection, return the redirection URL
|
|
396
|
+
return element;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
app = renderToString(element);
|
|
366
400
|
}
|
|
367
401
|
|
|
368
402
|
this.serverTimingProvider.endTiming("renderToString");
|
|
@@ -372,8 +406,11 @@ export class ReactServerProvider {
|
|
|
372
406
|
};
|
|
373
407
|
|
|
374
408
|
if (hydration) {
|
|
409
|
+
const { request, context, ...rest } =
|
|
410
|
+
this.alepha.context.als?.getStore() ?? {};
|
|
411
|
+
|
|
375
412
|
const hydrationData: ReactHydrationState = {
|
|
376
|
-
|
|
413
|
+
...rest,
|
|
377
414
|
layers: state.layers.map((it) => ({
|
|
378
415
|
...it,
|
|
379
416
|
error: it.error
|
|
@@ -381,7 +418,7 @@ export class ReactServerProvider {
|
|
|
381
418
|
...it.error,
|
|
382
419
|
name: it.error.name,
|
|
383
420
|
message: it.error.message,
|
|
384
|
-
stack: it.error.stack
|
|
421
|
+
stack: !this.alepha.isProduction() ? it.error.stack : undefined,
|
|
385
422
|
}
|
|
386
423
|
: undefined,
|
|
387
424
|
index: undefined,
|
|
@@ -418,7 +455,7 @@ export class ReactServerProvider {
|
|
|
418
455
|
const bodyOpenTag = /<body([^>]*)>/i;
|
|
419
456
|
if (bodyOpenTag.test(response.html)) {
|
|
420
457
|
response.html = response.html.replace(bodyOpenTag, (match) => {
|
|
421
|
-
return `${match}
|
|
458
|
+
return `${match}<div id="${this.env.REACT_ROOT_ID}">${app}</div>`;
|
|
422
459
|
});
|
|
423
460
|
}
|
|
424
461
|
}
|
|
@@ -427,7 +464,7 @@ export class ReactServerProvider {
|
|
|
427
464
|
if (bodyCloseTagRegex.test(response.html)) {
|
|
428
465
|
response.html = response.html.replace(
|
|
429
466
|
bodyCloseTagRegex,
|
|
430
|
-
`${script}
|
|
467
|
+
`${script}</body>`,
|
|
431
468
|
);
|
|
432
469
|
}
|
|
433
470
|
}
|