@alepha/react 0.10.6 → 0.10.7
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 +27 -42
- package/dist/index.browser.js.map +1 -1
- package/dist/index.d.ts +48 -48
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -57
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
- package/src/components/ClientOnly.tsx +12 -12
- package/src/components/ErrorBoundary.tsx +43 -43
- package/src/components/ErrorViewer.tsx +140 -140
- package/src/components/Link.tsx +7 -7
- package/src/components/NestedView.tsx +177 -177
- package/src/components/NotFound.tsx +19 -19
- package/src/contexts/RouterLayerContext.ts +3 -3
- package/src/descriptors/$page.ts +292 -290
- package/src/errors/Redirection.ts +5 -5
- package/src/hooks/useActive.ts +41 -41
- package/src/hooks/useAlepha.ts +7 -7
- package/src/hooks/useClient.ts +5 -5
- package/src/hooks/useInject.ts +2 -2
- package/src/hooks/useQueryParams.ts +37 -37
- package/src/hooks/useRouter.ts +1 -1
- package/src/hooks/useRouterEvents.ts +46 -46
- package/src/hooks/useRouterState.ts +5 -5
- package/src/hooks/useSchema.ts +55 -55
- package/src/hooks/useStore.ts +25 -25
- package/src/index.browser.ts +18 -18
- package/src/index.ts +49 -49
- package/src/providers/ReactBrowserProvider.ts +268 -268
- package/src/providers/ReactBrowserRendererProvider.ts +15 -15
- package/src/providers/ReactBrowserRouterProvider.ts +124 -124
- package/src/providers/ReactPageProvider.ts +616 -615
- package/src/providers/ReactServerProvider.ts +505 -505
- package/src/services/ReactRouter.ts +191 -191
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
$env,
|
|
3
|
+
$hook,
|
|
4
|
+
$inject,
|
|
5
|
+
Alepha,
|
|
6
|
+
AlephaError,
|
|
7
|
+
type Static,
|
|
8
|
+
type TSchema,
|
|
9
|
+
t,
|
|
9
10
|
} from "@alepha/core";
|
|
10
11
|
import { $logger } from "@alepha/logger";
|
|
11
12
|
import { createElement, type ReactNode, StrictMode } from "react";
|
|
@@ -16,651 +17,651 @@ import NotFoundPage from "../components/NotFound.tsx";
|
|
|
16
17
|
import { AlephaContext } from "../contexts/AlephaContext.ts";
|
|
17
18
|
import { RouterLayerContext } from "../contexts/RouterLayerContext.ts";
|
|
18
19
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
$page,
|
|
21
|
+
type ErrorHandler,
|
|
22
|
+
type PageDescriptor,
|
|
23
|
+
type PageDescriptorOptions,
|
|
23
24
|
} from "../descriptors/$page.ts";
|
|
24
25
|
import { Redirection } from "../errors/Redirection.ts";
|
|
25
26
|
|
|
26
27
|
const envSchema = t.object({
|
|
27
|
-
|
|
28
|
+
REACT_STRICT_MODE: t.boolean({ default: true }),
|
|
28
29
|
});
|
|
29
30
|
|
|
30
31
|
declare module "@alepha/core" {
|
|
31
|
-
|
|
32
|
+
export interface Env extends Partial<Static<typeof envSchema>> {}
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export class ReactPageProvider {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
36
|
+
protected readonly log = $logger();
|
|
37
|
+
protected readonly env = $env(envSchema);
|
|
38
|
+
protected readonly alepha = $inject(Alepha);
|
|
39
|
+
protected readonly pages: PageRoute[] = [];
|
|
40
|
+
|
|
41
|
+
public getPages(): PageRoute[] {
|
|
42
|
+
return this.pages;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public page(name: string): PageRoute {
|
|
46
|
+
for (const page of this.pages) {
|
|
47
|
+
if (page.name === name) {
|
|
48
|
+
return page;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
throw new Error(`Page ${name} not found`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public pathname(
|
|
56
|
+
name: string,
|
|
57
|
+
options: {
|
|
58
|
+
params?: Record<string, string>;
|
|
59
|
+
query?: Record<string, string>;
|
|
60
|
+
} = {},
|
|
61
|
+
) {
|
|
62
|
+
const page = this.page(name);
|
|
63
|
+
if (!page) {
|
|
64
|
+
throw new Error(`Page ${name} not found`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let url = page.path ?? "";
|
|
68
|
+
let parent = page.parent;
|
|
69
|
+
while (parent) {
|
|
70
|
+
url = `${parent.path ?? ""}/${url}`;
|
|
71
|
+
parent = parent.parent;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
url = this.compile(url, options.params ?? {});
|
|
75
|
+
|
|
76
|
+
if (options.query) {
|
|
77
|
+
const query = new URLSearchParams(options.query);
|
|
78
|
+
if (query.toString()) {
|
|
79
|
+
url += `?${query.toString()}`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return url.replace(/\/\/+/g, "/") || "/";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public url(
|
|
87
|
+
name: string,
|
|
88
|
+
options: { params?: Record<string, string>; host?: string } = {},
|
|
89
|
+
): URL {
|
|
90
|
+
return new URL(
|
|
91
|
+
this.pathname(name, options),
|
|
92
|
+
// use provided base or default to http://localhost
|
|
93
|
+
options.host ?? `http://localhost`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public root(state: ReactRouterState): ReactNode {
|
|
98
|
+
const root = createElement(
|
|
99
|
+
AlephaContext.Provider,
|
|
100
|
+
{ value: this.alepha },
|
|
101
|
+
createElement(NestedView, {}, state.layers[0]?.element),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (this.env.REACT_STRICT_MODE) {
|
|
105
|
+
return createElement(StrictMode, {}, root);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return root;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
protected convertStringObjectToObject = (
|
|
112
|
+
schema?: TSchema,
|
|
113
|
+
value?: any,
|
|
114
|
+
): any => {
|
|
115
|
+
if (t.schema.isObject(schema) && typeof value === "object") {
|
|
116
|
+
for (const key in schema.properties) {
|
|
117
|
+
if (
|
|
118
|
+
t.schema.isObject(schema.properties[key]) &&
|
|
119
|
+
typeof value[key] === "string"
|
|
120
|
+
) {
|
|
121
|
+
try {
|
|
122
|
+
value[key] = this.alepha.parse(
|
|
123
|
+
schema.properties[key],
|
|
124
|
+
decodeURIComponent(value[key]),
|
|
125
|
+
);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
// ignore
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return value;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Create a new RouterState based on a given route and request.
|
|
137
|
+
* This method resolves the layers for the route, applying any query and params schemas defined in the route.
|
|
138
|
+
* It also handles errors and redirects.
|
|
139
|
+
*/
|
|
140
|
+
public async createLayers(
|
|
141
|
+
route: PageRoute,
|
|
142
|
+
state: ReactRouterState,
|
|
143
|
+
previous: PreviousLayerData[] = [],
|
|
144
|
+
): Promise<CreateLayersResult> {
|
|
145
|
+
let context: Record<string, any> = {}; // all props
|
|
146
|
+
const stack: Array<RouterStackItem> = [{ route }]; // stack of routes
|
|
147
|
+
|
|
148
|
+
let parent = route.parent;
|
|
149
|
+
while (parent) {
|
|
150
|
+
stack.unshift({ route: parent });
|
|
151
|
+
parent = parent.parent;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
let forceRefresh = false;
|
|
155
|
+
|
|
156
|
+
for (let i = 0; i < stack.length; i++) {
|
|
157
|
+
const it = stack[i];
|
|
158
|
+
const route = it.route;
|
|
159
|
+
const config: Record<string, any> = {};
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
this.convertStringObjectToObject(route.schema?.query, state.query);
|
|
163
|
+
config.query = route.schema?.query
|
|
164
|
+
? this.alepha.parse(route.schema.query, state.query)
|
|
165
|
+
: {};
|
|
166
|
+
} catch (e) {
|
|
167
|
+
it.error = e as Error;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
config.params = route.schema?.params
|
|
173
|
+
? this.alepha.parse(route.schema.params, state.params)
|
|
174
|
+
: {};
|
|
175
|
+
} catch (e) {
|
|
176
|
+
it.error = e as Error;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// save config
|
|
181
|
+
it.config = {
|
|
182
|
+
...config,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// check if previous layer is the same, reuse if possible
|
|
186
|
+
if (previous?.[i] && !forceRefresh && previous[i].name === route.name) {
|
|
187
|
+
const url = (str?: string) => (str ? str.replace(/\/\/+/g, "/") : "/");
|
|
188
|
+
|
|
189
|
+
const prev = JSON.stringify({
|
|
190
|
+
part: url(previous[i].part),
|
|
191
|
+
params: previous[i].config?.params ?? {},
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const curr = JSON.stringify({
|
|
195
|
+
part: url(route.path),
|
|
196
|
+
params: config.params ?? {},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (prev === curr) {
|
|
200
|
+
// part is the same, reuse previous layer
|
|
201
|
+
it.props = previous[i].props;
|
|
202
|
+
it.error = previous[i].error;
|
|
203
|
+
it.cache = true;
|
|
204
|
+
context = {
|
|
205
|
+
...context,
|
|
206
|
+
...it.props,
|
|
207
|
+
};
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// part is different, force refresh of next layers
|
|
212
|
+
forceRefresh = true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// no resolve, render a basic view by default
|
|
216
|
+
if (!route.resolve) {
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
const args = Object.create(state);
|
|
222
|
+
Object.assign(args, config, context);
|
|
223
|
+
const props = (await route.resolve?.(args)) ?? {};
|
|
224
|
+
|
|
225
|
+
// save props
|
|
226
|
+
it.props = {
|
|
227
|
+
...props,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// add props to context
|
|
231
|
+
context = {
|
|
232
|
+
...context,
|
|
233
|
+
...props,
|
|
234
|
+
};
|
|
235
|
+
} catch (e) {
|
|
236
|
+
// check if we need to redirect
|
|
237
|
+
if (e instanceof Redirection) {
|
|
238
|
+
return {
|
|
239
|
+
redirect: e.redirect,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
this.log.error("Page resolver has failed", e);
|
|
244
|
+
|
|
245
|
+
it.error = e as Error;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let acc = "";
|
|
251
|
+
for (let i = 0; i < stack.length; i++) {
|
|
252
|
+
const it = stack[i];
|
|
253
|
+
const props = it.props ?? {};
|
|
254
|
+
|
|
255
|
+
const params = { ...it.config?.params };
|
|
256
|
+
for (const key of Object.keys(params)) {
|
|
257
|
+
params[key] = String(params[key]);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
acc += "/";
|
|
261
|
+
acc += it.route.path ? this.compile(it.route.path, params) : "";
|
|
262
|
+
const path = acc.replace(/\/+/, "/");
|
|
263
|
+
const localErrorHandler = this.getErrorHandler(it.route);
|
|
264
|
+
if (localErrorHandler) {
|
|
265
|
+
const onErrorParent = state.onError;
|
|
266
|
+
state.onError = (error, context) => {
|
|
267
|
+
const result = localErrorHandler(error, context);
|
|
268
|
+
// if nothing happen, call the parent
|
|
269
|
+
if (result === undefined) {
|
|
270
|
+
return onErrorParent(error, context);
|
|
271
|
+
}
|
|
272
|
+
return result;
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// normal use case
|
|
277
|
+
if (!it.error) {
|
|
278
|
+
try {
|
|
279
|
+
const element = await this.createElement(it.route, {
|
|
280
|
+
...props,
|
|
281
|
+
...context,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
state.layers.push({
|
|
285
|
+
name: it.route.name,
|
|
286
|
+
props,
|
|
287
|
+
part: it.route.path,
|
|
288
|
+
config: it.config,
|
|
289
|
+
element: this.renderView(i + 1, path, element, it.route),
|
|
290
|
+
index: i + 1,
|
|
291
|
+
path,
|
|
292
|
+
route: it.route,
|
|
293
|
+
cache: it.cache,
|
|
294
|
+
});
|
|
295
|
+
} catch (e) {
|
|
296
|
+
it.error = e as Error;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// handler has thrown an error, render an error view
|
|
301
|
+
if (it.error) {
|
|
302
|
+
try {
|
|
303
|
+
let element: ReactNode | Redirection | undefined =
|
|
304
|
+
await state.onError(it.error, state);
|
|
305
|
+
|
|
306
|
+
if (element === undefined) {
|
|
307
|
+
throw it.error;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (element instanceof Redirection) {
|
|
311
|
+
return {
|
|
312
|
+
redirect: element.redirect,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (element === null) {
|
|
317
|
+
element = this.renderError(it.error);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
state.layers.push({
|
|
321
|
+
props,
|
|
322
|
+
error: it.error,
|
|
323
|
+
name: it.route.name,
|
|
324
|
+
part: it.route.path,
|
|
325
|
+
config: it.config,
|
|
326
|
+
element: this.renderView(i + 1, path, element, it.route),
|
|
327
|
+
index: i + 1,
|
|
328
|
+
path,
|
|
329
|
+
route: it.route,
|
|
330
|
+
});
|
|
331
|
+
break;
|
|
332
|
+
} catch (e) {
|
|
333
|
+
if (e instanceof Redirection) {
|
|
334
|
+
return {
|
|
335
|
+
redirect: e.redirect,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
throw e;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return { state };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
protected createRedirectionLayer(redirect: string): CreateLayersResult {
|
|
347
|
+
return {
|
|
348
|
+
redirect,
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
protected getErrorHandler(route: PageRoute): ErrorHandler | undefined {
|
|
353
|
+
if (route.errorHandler) return route.errorHandler;
|
|
354
|
+
let parent = route.parent;
|
|
355
|
+
while (parent) {
|
|
356
|
+
if (parent.errorHandler) return parent.errorHandler;
|
|
357
|
+
parent = parent.parent;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
protected async createElement(
|
|
362
|
+
page: PageRoute,
|
|
363
|
+
props: Record<string, any>,
|
|
364
|
+
): Promise<ReactNode> {
|
|
365
|
+
if (page.lazy && page.component) {
|
|
366
|
+
this.log.warn(
|
|
367
|
+
`Page ${page.name} has both lazy and component options, lazy will be used`,
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (page.lazy) {
|
|
372
|
+
const component = await page.lazy(); // load component
|
|
373
|
+
return createElement(component.default, props);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (page.component) {
|
|
377
|
+
return createElement(page.component, props);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
public renderError(error: Error): ReactNode {
|
|
384
|
+
return createElement(ErrorViewer, { error, alepha: this.alepha });
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
public renderEmptyView(): ReactNode {
|
|
388
|
+
return createElement(NestedView, {});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
public href(
|
|
392
|
+
page: { options: { name?: string } },
|
|
393
|
+
params: Record<string, any> = {},
|
|
394
|
+
): string {
|
|
395
|
+
const found = this.pages.find((it) => it.name === page.options.name);
|
|
396
|
+
if (!found) {
|
|
397
|
+
throw new Error(`Page ${page.options.name} not found`);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
let url = found.path ?? "";
|
|
401
|
+
let parent = found.parent;
|
|
402
|
+
while (parent) {
|
|
403
|
+
url = `${parent.path ?? ""}/${url}`;
|
|
404
|
+
parent = parent.parent;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
url = this.compile(url, params);
|
|
408
|
+
|
|
409
|
+
return url.replace(/\/\/+/g, "/") || "/";
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
public compile(path: string, params: Record<string, string> = {}) {
|
|
413
|
+
for (const [key, value] of Object.entries(params)) {
|
|
414
|
+
path = path.replace(`:${key}`, value);
|
|
415
|
+
}
|
|
416
|
+
return path;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
protected renderView(
|
|
420
|
+
index: number,
|
|
421
|
+
path: string,
|
|
422
|
+
view: ReactNode | undefined,
|
|
423
|
+
page: PageRoute,
|
|
424
|
+
): ReactNode {
|
|
425
|
+
view ??= this.renderEmptyView();
|
|
426
|
+
|
|
427
|
+
const element = page.client
|
|
428
|
+
? createElement(
|
|
429
|
+
ClientOnly,
|
|
430
|
+
typeof page.client === "object" ? page.client : {},
|
|
431
|
+
view,
|
|
432
|
+
)
|
|
433
|
+
: view;
|
|
434
|
+
|
|
435
|
+
return createElement(
|
|
436
|
+
RouterLayerContext.Provider,
|
|
437
|
+
{
|
|
438
|
+
value: {
|
|
439
|
+
index,
|
|
440
|
+
path,
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
element,
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
protected readonly configure = $hook({
|
|
448
|
+
on: "configure",
|
|
449
|
+
handler: () => {
|
|
450
|
+
let hasNotFoundHandler = false;
|
|
451
|
+
const pages = this.alepha.descriptors($page);
|
|
452
|
+
|
|
453
|
+
const hasParent = (it: PageDescriptor) => {
|
|
454
|
+
if (it.options.parent) {
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
for (const page of pages) {
|
|
459
|
+
const children = page.options.children
|
|
460
|
+
? Array.isArray(page.options.children)
|
|
461
|
+
? page.options.children
|
|
462
|
+
: page.options.children()
|
|
463
|
+
: [];
|
|
464
|
+
if (children.includes(it)) {
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
for (const page of pages) {
|
|
471
|
+
if (page.options.path === "/*") {
|
|
472
|
+
hasNotFoundHandler = true;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// skip children, we only want root pages
|
|
476
|
+
if (hasParent(page)) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
this.add(this.map(pages, page));
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (!hasNotFoundHandler && pages.length > 0) {
|
|
484
|
+
// add a default 404 page if not already defined
|
|
485
|
+
this.add({
|
|
486
|
+
path: "/*",
|
|
487
|
+
name: "notFound",
|
|
488
|
+
cache: true,
|
|
489
|
+
component: NotFoundPage,
|
|
490
|
+
onServerResponse: ({ reply }) => {
|
|
491
|
+
reply.status = 404;
|
|
492
|
+
},
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
protected map(
|
|
499
|
+
pages: Array<PageDescriptor>,
|
|
500
|
+
target: PageDescriptor,
|
|
501
|
+
): PageRouteEntry {
|
|
502
|
+
const children = target.options.children
|
|
503
|
+
? Array.isArray(target.options.children)
|
|
504
|
+
? target.options.children
|
|
505
|
+
: target.options.children()
|
|
506
|
+
: [];
|
|
507
|
+
|
|
508
|
+
const getChildrenFromParent = (it: PageDescriptor): PageDescriptor[] => {
|
|
509
|
+
const children = [];
|
|
510
|
+
for (const page of pages) {
|
|
511
|
+
if (page.options.parent === it) {
|
|
512
|
+
children.push(page);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return children;
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
children.push(...getChildrenFromParent(target));
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
...target.options,
|
|
522
|
+
name: target.name,
|
|
523
|
+
parent: undefined,
|
|
524
|
+
children: children.map((it) => this.map(pages, it)),
|
|
525
|
+
} as PageRoute;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
public add(entry: PageRouteEntry) {
|
|
529
|
+
if (this.alepha.isReady()) {
|
|
530
|
+
throw new AlephaError("Router is already initialized");
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
entry.name ??= this.nextId();
|
|
534
|
+
const page = entry as PageRoute;
|
|
535
|
+
|
|
536
|
+
page.match = this.createMatch(page);
|
|
537
|
+
this.pages.push(page);
|
|
538
|
+
|
|
539
|
+
if (page.children) {
|
|
540
|
+
for (const child of page.children) {
|
|
541
|
+
(child as PageRoute).parent = page;
|
|
542
|
+
this.add(child);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
protected createMatch(page: PageRoute): string {
|
|
548
|
+
let url = page.path ?? "/";
|
|
549
|
+
let target = page.parent;
|
|
550
|
+
while (target) {
|
|
551
|
+
url = `${target.path ?? ""}/${url}`;
|
|
552
|
+
target = target.parent;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
let path = url.replace(/\/\/+/g, "/");
|
|
556
|
+
|
|
557
|
+
if (path.endsWith("/") && path !== "/") {
|
|
558
|
+
// remove trailing slash
|
|
559
|
+
path = path.slice(0, -1);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return path;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
protected _next = 0;
|
|
566
|
+
|
|
567
|
+
protected nextId(): string {
|
|
568
|
+
this._next += 1;
|
|
569
|
+
return `P${this._next}`;
|
|
570
|
+
}
|
|
570
571
|
}
|
|
571
572
|
|
|
572
573
|
export const isPageRoute = (it: any): it is PageRoute => {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
574
|
+
return (
|
|
575
|
+
it &&
|
|
576
|
+
typeof it === "object" &&
|
|
577
|
+
typeof it.path === "string" &&
|
|
578
|
+
typeof it.page === "object"
|
|
579
|
+
);
|
|
579
580
|
};
|
|
580
581
|
|
|
581
582
|
export interface PageRouteEntry
|
|
582
|
-
|
|
583
|
-
|
|
583
|
+
extends Omit<PageDescriptorOptions, "children" | "parent"> {
|
|
584
|
+
children?: PageRouteEntry[];
|
|
584
585
|
}
|
|
585
586
|
|
|
586
587
|
export interface PageRoute extends PageRouteEntry {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
588
|
+
type: "page";
|
|
589
|
+
name: string;
|
|
590
|
+
parent?: PageRoute;
|
|
591
|
+
match: string;
|
|
591
592
|
}
|
|
592
593
|
|
|
593
594
|
export interface Layer {
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
595
|
+
config?: {
|
|
596
|
+
query?: Record<string, any>;
|
|
597
|
+
params?: Record<string, any>;
|
|
598
|
+
// stack of resolved props
|
|
599
|
+
context?: Record<string, any>;
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
name: string;
|
|
603
|
+
props?: Record<string, any>;
|
|
604
|
+
error?: Error;
|
|
605
|
+
part?: string;
|
|
606
|
+
element: ReactNode;
|
|
607
|
+
index: number;
|
|
608
|
+
path: string;
|
|
609
|
+
route?: PageRoute;
|
|
610
|
+
cache?: boolean;
|
|
610
611
|
}
|
|
611
612
|
|
|
612
613
|
export type PreviousLayerData = Omit<Layer, "element" | "index" | "path">;
|
|
613
614
|
|
|
614
615
|
export interface AnchorProps {
|
|
615
|
-
|
|
616
|
-
|
|
616
|
+
href: string;
|
|
617
|
+
onClick: (ev?: any) => any;
|
|
617
618
|
}
|
|
618
619
|
|
|
619
620
|
export interface ReactRouterState {
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
621
|
+
/**
|
|
622
|
+
* Stack of layers for the current page.
|
|
623
|
+
*/
|
|
624
|
+
layers: Array<Layer>;
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* URL of the current page.
|
|
628
|
+
*/
|
|
629
|
+
url: URL;
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Error handler for the current page.
|
|
633
|
+
*/
|
|
634
|
+
onError: ErrorHandler;
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Params extracted from the URL for the current page.
|
|
638
|
+
*/
|
|
639
|
+
params: Record<string, any>;
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Query parameters extracted from the URL for the current page.
|
|
643
|
+
*/
|
|
644
|
+
query: Record<string, string>;
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Optional meta information associated with the current page.
|
|
648
|
+
*/
|
|
649
|
+
meta: Record<string, any>;
|
|
649
650
|
}
|
|
650
651
|
|
|
651
652
|
export interface RouterStackItem {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
653
|
+
route: PageRoute;
|
|
654
|
+
config?: Record<string, any>;
|
|
655
|
+
props?: Record<string, any>;
|
|
656
|
+
error?: Error;
|
|
657
|
+
cache?: boolean;
|
|
657
658
|
}
|
|
658
659
|
|
|
659
660
|
export interface TransitionOptions {
|
|
660
|
-
|
|
661
|
+
previous?: PreviousLayerData[];
|
|
661
662
|
}
|
|
662
663
|
|
|
663
664
|
export interface CreateLayersResult {
|
|
664
|
-
|
|
665
|
-
|
|
665
|
+
redirect?: string;
|
|
666
|
+
state?: ReactRouterState;
|
|
666
667
|
}
|