@alepha/react 0.10.5 → 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 +32 -48
- package/dist/index.browser.js.map +1 -1
- package/dist/index.d.ts +39 -39
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -63
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
- 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 -261
- package/src/providers/ReactBrowserRendererProvider.ts +15 -15
- package/src/providers/ReactBrowserRouterProvider.ts +124 -124
- package/src/providers/ReactPageProvider.ts +616 -618
- 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,654 +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
|
-
|
|
570
|
-
this._next += 1;
|
|
571
|
-
return `P${this._next}`;
|
|
572
|
-
}
|
|
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
|
+
}
|
|
573
571
|
}
|
|
574
572
|
|
|
575
573
|
export const isPageRoute = (it: any): it is PageRoute => {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
574
|
+
return (
|
|
575
|
+
it &&
|
|
576
|
+
typeof it === "object" &&
|
|
577
|
+
typeof it.path === "string" &&
|
|
578
|
+
typeof it.page === "object"
|
|
579
|
+
);
|
|
582
580
|
};
|
|
583
581
|
|
|
584
582
|
export interface PageRouteEntry
|
|
585
|
-
|
|
586
|
-
|
|
583
|
+
extends Omit<PageDescriptorOptions, "children" | "parent"> {
|
|
584
|
+
children?: PageRouteEntry[];
|
|
587
585
|
}
|
|
588
586
|
|
|
589
587
|
export interface PageRoute extends PageRouteEntry {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
588
|
+
type: "page";
|
|
589
|
+
name: string;
|
|
590
|
+
parent?: PageRoute;
|
|
591
|
+
match: string;
|
|
594
592
|
}
|
|
595
593
|
|
|
596
594
|
export interface Layer {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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;
|
|
613
611
|
}
|
|
614
612
|
|
|
615
613
|
export type PreviousLayerData = Omit<Layer, "element" | "index" | "path">;
|
|
616
614
|
|
|
617
615
|
export interface AnchorProps {
|
|
618
|
-
|
|
619
|
-
|
|
616
|
+
href: string;
|
|
617
|
+
onClick: (ev?: any) => any;
|
|
620
618
|
}
|
|
621
619
|
|
|
622
620
|
export interface ReactRouterState {
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
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>;
|
|
652
650
|
}
|
|
653
651
|
|
|
654
652
|
export interface RouterStackItem {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
653
|
+
route: PageRoute;
|
|
654
|
+
config?: Record<string, any>;
|
|
655
|
+
props?: Record<string, any>;
|
|
656
|
+
error?: Error;
|
|
657
|
+
cache?: boolean;
|
|
660
658
|
}
|
|
661
659
|
|
|
662
660
|
export interface TransitionOptions {
|
|
663
|
-
|
|
661
|
+
previous?: PreviousLayerData[];
|
|
664
662
|
}
|
|
665
663
|
|
|
666
664
|
export interface CreateLayersResult {
|
|
667
|
-
|
|
668
|
-
|
|
665
|
+
redirect?: string;
|
|
666
|
+
state?: ReactRouterState;
|
|
669
667
|
}
|