@alepha/react 0.9.2 → 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 +378 -325
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +570 -458
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +305 -213
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +304 -212
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +567 -460
- package/dist/index.js.map +1 -1
- package/package.json +16 -13
- package/src/components/ErrorViewer.tsx +1 -1
- package/src/components/Link.tsx +4 -24
- package/src/components/NestedView.tsx +20 -9
- package/src/components/NotFound.tsx +5 -2
- package/src/descriptors/$page.ts +86 -12
- package/src/errors/Redirection.ts +13 -0
- package/src/hooks/useActive.ts +28 -30
- package/src/hooks/useAlepha.ts +16 -2
- package/src/hooks/useClient.ts +7 -2
- package/src/hooks/useInject.ts +4 -1
- package/src/hooks/useQueryParams.ts +9 -6
- package/src/hooks/useRouter.ts +18 -30
- package/src/hooks/useRouterEvents.ts +7 -4
- package/src/hooks/useRouterState.ts +8 -20
- package/src/hooks/useSchema.ts +10 -15
- package/src/hooks/useStore.ts +9 -8
- package/src/index.browser.ts +11 -11
- package/src/index.shared.ts +4 -5
- package/src/index.ts +21 -30
- package/src/providers/ReactBrowserProvider.ts +155 -65
- package/src/providers/ReactBrowserRouterProvider.ts +132 -0
- package/src/providers/{PageDescriptorProvider.ts → ReactPageProvider.ts} +164 -112
- package/src/providers/ReactServerProvider.ts +100 -68
- package/src/{hooks/RouterHookApi.ts → services/ReactRouter.ts} +75 -61
- package/src/contexts/RouterContext.ts +0 -14
- package/src/errors/RedirectionError.ts +0 -10
- package/src/providers/BrowserRouterProvider.ts +0 -146
- package/src/providers/ReactBrowserRenderer.ts +0 -93
|
@@ -1,27 +1,19 @@
|
|
|
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,
|
|
12
|
+
type ErrorHandler,
|
|
21
13
|
type PageDescriptor,
|
|
22
14
|
type PageDescriptorOptions,
|
|
23
15
|
} from "../descriptors/$page.ts";
|
|
24
|
-
import {
|
|
16
|
+
import { Redirection } from "../errors/Redirection.ts";
|
|
25
17
|
|
|
26
18
|
const envSchema = t.object({
|
|
27
19
|
REACT_STRICT_MODE: t.boolean({ default: true }),
|
|
@@ -31,7 +23,7 @@ declare module "@alepha/core" {
|
|
|
31
23
|
export interface Env extends Partial<Static<typeof envSchema>> {}
|
|
32
24
|
}
|
|
33
25
|
|
|
34
|
-
export class
|
|
26
|
+
export class ReactPageProvider {
|
|
35
27
|
protected readonly log = $logger();
|
|
36
28
|
protected readonly env = $env(envSchema);
|
|
37
29
|
protected readonly alepha = $inject(Alepha);
|
|
@@ -51,10 +43,13 @@ export class PageDescriptorProvider {
|
|
|
51
43
|
throw new Error(`Page ${name} not found`);
|
|
52
44
|
}
|
|
53
45
|
|
|
54
|
-
public
|
|
46
|
+
public pathname(
|
|
55
47
|
name: string,
|
|
56
|
-
options: {
|
|
57
|
-
|
|
48
|
+
options: {
|
|
49
|
+
params?: Record<string, string>;
|
|
50
|
+
query?: Record<string, string>;
|
|
51
|
+
} = {},
|
|
52
|
+
) {
|
|
58
53
|
const page = this.page(name);
|
|
59
54
|
if (!page) {
|
|
60
55
|
throw new Error(`Page ${name} not found`);
|
|
@@ -69,26 +64,32 @@ export class PageDescriptorProvider {
|
|
|
69
64
|
|
|
70
65
|
url = this.compile(url, options.params ?? {});
|
|
71
66
|
|
|
67
|
+
if (options.query) {
|
|
68
|
+
const query = new URLSearchParams(options.query);
|
|
69
|
+
if (query.toString()) {
|
|
70
|
+
url += `?${query.toString()}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return url.replace(/\/\/+/g, "/") || "/";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public url(
|
|
78
|
+
name: string,
|
|
79
|
+
options: { params?: Record<string, string>; host?: string } = {},
|
|
80
|
+
): URL {
|
|
72
81
|
return new URL(
|
|
73
|
-
|
|
74
|
-
|
|
82
|
+
this.pathname(name, options),
|
|
83
|
+
// use provided base or default to http://localhost
|
|
84
|
+
options.host ?? `http://localhost`,
|
|
75
85
|
);
|
|
76
86
|
}
|
|
77
87
|
|
|
78
|
-
public root(state:
|
|
88
|
+
public root(state: ReactRouterState): ReactNode {
|
|
79
89
|
const root = createElement(
|
|
80
90
|
AlephaContext.Provider,
|
|
81
91
|
{ value: this.alepha },
|
|
82
|
-
createElement(
|
|
83
|
-
RouterContext.Provider,
|
|
84
|
-
{
|
|
85
|
-
value: {
|
|
86
|
-
state,
|
|
87
|
-
context,
|
|
88
|
-
},
|
|
89
|
-
},
|
|
90
|
-
createElement(NestedView, {}, state.layers[0]?.element),
|
|
91
|
-
),
|
|
92
|
+
createElement(NestedView, {}, state.layers[0]?.element),
|
|
92
93
|
);
|
|
93
94
|
|
|
94
95
|
if (this.env.REACT_STRICT_MODE) {
|
|
@@ -98,15 +99,18 @@ export class PageDescriptorProvider {
|
|
|
98
99
|
return root;
|
|
99
100
|
}
|
|
100
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
|
+
*/
|
|
101
107
|
public async createLayers(
|
|
102
108
|
route: PageRoute,
|
|
103
|
-
|
|
109
|
+
state: ReactRouterState,
|
|
110
|
+
previous: PreviousLayerData[] = [],
|
|
104
111
|
): Promise<CreateLayersResult> {
|
|
105
|
-
const { pathname, search } = request.url;
|
|
106
|
-
const layers: Layer[] = []; // result layers
|
|
107
112
|
let context: Record<string, any> = {}; // all props
|
|
108
113
|
const stack: Array<RouterStackItem> = [{ route }]; // stack of routes
|
|
109
|
-
request.onError = (error) => this.renderError(error); // error handler
|
|
110
114
|
|
|
111
115
|
let parent = route.parent;
|
|
112
116
|
while (parent) {
|
|
@@ -123,7 +127,7 @@ export class PageDescriptorProvider {
|
|
|
123
127
|
|
|
124
128
|
try {
|
|
125
129
|
config.query = route.schema?.query
|
|
126
|
-
? this.alepha.parse(route.schema.query,
|
|
130
|
+
? this.alepha.parse(route.schema.query, state.query)
|
|
127
131
|
: {};
|
|
128
132
|
} catch (e) {
|
|
129
133
|
it.error = e as Error;
|
|
@@ -132,7 +136,7 @@ export class PageDescriptorProvider {
|
|
|
132
136
|
|
|
133
137
|
try {
|
|
134
138
|
config.params = route.schema?.params
|
|
135
|
-
? this.alepha.parse(route.schema.params,
|
|
139
|
+
? this.alepha.parse(route.schema.params, state.params)
|
|
136
140
|
: {};
|
|
137
141
|
} catch (e) {
|
|
138
142
|
it.error = e as Error;
|
|
@@ -145,7 +149,6 @@ export class PageDescriptorProvider {
|
|
|
145
149
|
};
|
|
146
150
|
|
|
147
151
|
// check if previous layer is the same, reuse if possible
|
|
148
|
-
const previous = request.previous;
|
|
149
152
|
if (previous?.[i] && !forceRefresh && previous[i].name === route.name) {
|
|
150
153
|
const url = (str?: string) => (str ? str.replace(/\/\/+/g, "/") : "/");
|
|
151
154
|
|
|
@@ -183,7 +186,7 @@ export class PageDescriptorProvider {
|
|
|
183
186
|
try {
|
|
184
187
|
const props =
|
|
185
188
|
(await route.resolve?.({
|
|
186
|
-
...
|
|
189
|
+
...state, // request
|
|
187
190
|
...config, // params, query
|
|
188
191
|
...context, // previous props
|
|
189
192
|
} as any)) ?? {};
|
|
@@ -200,16 +203,13 @@ export class PageDescriptorProvider {
|
|
|
200
203
|
};
|
|
201
204
|
} catch (e) {
|
|
202
205
|
// check if we need to redirect
|
|
203
|
-
if (e instanceof
|
|
206
|
+
if (e instanceof Redirection) {
|
|
204
207
|
return {
|
|
205
|
-
|
|
206
|
-
redirect: typeof e.page === "string" ? e.page : this.href(e.page),
|
|
207
|
-
pathname,
|
|
208
|
-
search,
|
|
208
|
+
redirect: e.redirect,
|
|
209
209
|
};
|
|
210
210
|
}
|
|
211
211
|
|
|
212
|
-
this.log.error(e);
|
|
212
|
+
this.log.error("Page resolver has failed", e);
|
|
213
213
|
|
|
214
214
|
it.error = e as Error;
|
|
215
215
|
break;
|
|
@@ -231,54 +231,94 @@ export class PageDescriptorProvider {
|
|
|
231
231
|
const path = acc.replace(/\/+/, "/");
|
|
232
232
|
const localErrorHandler = this.getErrorHandler(it.route);
|
|
233
233
|
if (localErrorHandler) {
|
|
234
|
-
|
|
234
|
+
const onErrorParent = state.onError;
|
|
235
|
+
state.onError = (error, context) => {
|
|
236
|
+
const result = localErrorHandler(error, context);
|
|
237
|
+
// if nothing happen, call the parent
|
|
238
|
+
if (result === undefined) {
|
|
239
|
+
return onErrorParent(error, context);
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
};
|
|
243
|
+
}
|
|
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
|
+
}
|
|
235
267
|
}
|
|
236
268
|
|
|
237
269
|
// handler has thrown an error, render an error view
|
|
238
270
|
if (it.error) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
}
|
|
271
|
+
try {
|
|
272
|
+
let element: ReactNode | Redirection | undefined =
|
|
273
|
+
await state.onError(it.error, state);
|
|
243
274
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
name: it.route.name,
|
|
248
|
-
part: it.route.path,
|
|
249
|
-
config: it.config,
|
|
250
|
-
element: this.renderView(i + 1, path, element, it.route),
|
|
251
|
-
index: i + 1,
|
|
252
|
-
path,
|
|
253
|
-
route: it.route,
|
|
254
|
-
});
|
|
255
|
-
break;
|
|
256
|
-
}
|
|
275
|
+
if (element === undefined) {
|
|
276
|
+
throw it.error;
|
|
277
|
+
}
|
|
257
278
|
|
|
258
|
-
|
|
279
|
+
if (element instanceof Redirection) {
|
|
280
|
+
return {
|
|
281
|
+
redirect: element.redirect,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (element === null) {
|
|
286
|
+
element = this.renderError(it.error);
|
|
287
|
+
}
|
|
259
288
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
289
|
+
state.layers.push({
|
|
290
|
+
props,
|
|
291
|
+
error: it.error,
|
|
292
|
+
name: it.route.name,
|
|
293
|
+
part: it.route.path,
|
|
294
|
+
config: it.config,
|
|
295
|
+
element: this.renderView(i + 1, path, element, it.route),
|
|
296
|
+
index: i + 1,
|
|
297
|
+
path,
|
|
298
|
+
route: it.route,
|
|
299
|
+
});
|
|
300
|
+
break;
|
|
301
|
+
} catch (e) {
|
|
302
|
+
if (e instanceof Redirection) {
|
|
303
|
+
return {
|
|
304
|
+
redirect: e.redirect,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
throw e;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
276
310
|
}
|
|
277
311
|
|
|
278
|
-
return {
|
|
312
|
+
return { state };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
protected createRedirectionLayer(redirect: string): CreateLayersResult {
|
|
316
|
+
return {
|
|
317
|
+
redirect,
|
|
318
|
+
};
|
|
279
319
|
}
|
|
280
320
|
|
|
281
|
-
protected getErrorHandler(route: PageRoute) {
|
|
321
|
+
protected getErrorHandler(route: PageRoute): ErrorHandler | undefined {
|
|
282
322
|
if (route.errorHandler) return route.errorHandler;
|
|
283
323
|
let parent = route.parent;
|
|
284
324
|
while (parent) {
|
|
@@ -374,6 +414,10 @@ export class PageDescriptorProvider {
|
|
|
374
414
|
const pages = this.alepha.descriptors($page);
|
|
375
415
|
|
|
376
416
|
const hasParent = (it: PageDescriptor) => {
|
|
417
|
+
if (it.options.parent) {
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
|
|
377
421
|
for (const page of pages) {
|
|
378
422
|
const children = page.options.children
|
|
379
423
|
? Array.isArray(page.options.children)
|
|
@@ -406,7 +450,7 @@ export class PageDescriptorProvider {
|
|
|
406
450
|
name: "notFound",
|
|
407
451
|
cache: true,
|
|
408
452
|
component: NotFoundPage,
|
|
409
|
-
|
|
453
|
+
onServerResponse: ({ reply }) => {
|
|
410
454
|
reply.status = 404;
|
|
411
455
|
},
|
|
412
456
|
});
|
|
@@ -424,6 +468,18 @@ export class PageDescriptorProvider {
|
|
|
424
468
|
: target.options.children()
|
|
425
469
|
: [];
|
|
426
470
|
|
|
471
|
+
const getChildrenFromParent = (it: PageDescriptor): PageDescriptor[] => {
|
|
472
|
+
const children = [];
|
|
473
|
+
for (const page of pages) {
|
|
474
|
+
if (page.options.parent === it) {
|
|
475
|
+
children.push(page);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return children;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
children.push(...getChildrenFromParent(target));
|
|
482
|
+
|
|
427
483
|
return {
|
|
428
484
|
...target.options,
|
|
429
485
|
name: target.name,
|
|
@@ -521,19 +577,34 @@ export type PreviousLayerData = Omit<Layer, "element" | "index" | "path">;
|
|
|
521
577
|
|
|
522
578
|
export interface AnchorProps {
|
|
523
579
|
href: string;
|
|
524
|
-
onClick: (ev
|
|
580
|
+
onClick: (ev?: any) => any;
|
|
525
581
|
}
|
|
526
582
|
|
|
527
|
-
export interface
|
|
528
|
-
|
|
529
|
-
|
|
583
|
+
export interface ReactRouterState {
|
|
584
|
+
/**
|
|
585
|
+
* Stack of layers for the current page.
|
|
586
|
+
*/
|
|
530
587
|
layers: Array<Layer>;
|
|
531
|
-
}
|
|
532
588
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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>;
|
|
537
608
|
}
|
|
538
609
|
|
|
539
610
|
export interface RouterStackItem {
|
|
@@ -544,30 +615,11 @@ export interface RouterStackItem {
|
|
|
544
615
|
cache?: boolean;
|
|
545
616
|
}
|
|
546
617
|
|
|
547
|
-
export interface
|
|
548
|
-
state: RouterState;
|
|
549
|
-
context: PageReactContext;
|
|
550
|
-
redirect?: string;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
export interface PageRequest extends PageReactContext {
|
|
554
|
-
params: Record<string, any>;
|
|
555
|
-
query: Record<string, string>;
|
|
556
|
-
|
|
557
|
-
// previous layers (browser history or browser hydration, always null on server)
|
|
618
|
+
export interface TransitionOptions {
|
|
558
619
|
previous?: PreviousLayerData[];
|
|
559
620
|
}
|
|
560
621
|
|
|
561
|
-
export interface CreateLayersResult
|
|
622
|
+
export interface CreateLayersResult {
|
|
562
623
|
redirect?: string;
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
/**
|
|
566
|
-
* It's like RouterState, but publicly available in React context.
|
|
567
|
-
* This is where we store all plugin data!
|
|
568
|
-
*/
|
|
569
|
-
export interface PageReactContext {
|
|
570
|
-
url: URL;
|
|
571
|
-
onError: (error: Error) => ReactNode;
|
|
572
|
-
links?: ApiLinksResponse;
|
|
624
|
+
state?: ReactRouterState;
|
|
573
625
|
}
|