slicejs-web-framework 4.1.1 → 4.2.0
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/Slice/Components/Structural/Controller/Controller.js +1427 -1406
- package/Slice/Components/Structural/EventManager/EventManager.js +652 -651
- package/Slice/Components/Structural/Router/Router.js +982 -820
- package/Slice/Components/Structural/Router/applyRouteMetadata.js +151 -8
- package/Slice/Components/Structural/Translator/Translator.js +1111 -0
- package/Slice/Components/Structural/Translator/TranslatorDebugger.js +511 -0
- package/Slice/Components/Structural/Translator/content.js +188 -0
- package/Slice/Components/Structural/Translator/dom.js +253 -0
- package/Slice/Components/Structural/Translator/localePath.js +90 -0
- package/Slice/Components/Structural/Translator/localized.js +80 -0
- package/Slice/Components/Structural/Translator/markers.js +148 -0
- package/Slice/Components/Structural/Translator/messages.js +211 -0
- package/Slice/Slice.js +826 -711
- package/api/framework/content.js +73 -0
- package/api/framework/seo.js +394 -43
- package/api/framework/server.js +205 -28
- package/package.json +2 -1
- package/sliceConfig.schema.json +150 -122
- package/types/index.d.ts +347 -219
|
@@ -1,820 +1,982 @@
|
|
|
1
|
-
import { applyRouteMetadata } from './applyRouteMetadata.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @typedef {Object} RouteConfig
|
|
5
|
-
* @property {string} path
|
|
6
|
-
* @property {string} component
|
|
7
|
-
* @property {RouteConfig[]} [children]
|
|
8
|
-
* @property {Object} [metadata]
|
|
9
|
-
* @property {string} [fullPath]
|
|
10
|
-
* @property {string|null} [parentPath]
|
|
11
|
-
* @property {RouteConfig|null} [parentRoute]
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* @typedef {Object} RouteInfo
|
|
16
|
-
* @property {string} path
|
|
17
|
-
* @property {string} component
|
|
18
|
-
* @property {Object} params
|
|
19
|
-
* @property {Object} query
|
|
20
|
-
* @property {Object} metadata
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
* @
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
* @
|
|
33
|
-
* @property {RouteConfig}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
* @
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
this.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
this.
|
|
65
|
-
this.
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
this.
|
|
69
|
-
this.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
this.
|
|
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
|
-
this.
|
|
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
|
-
* @param {
|
|
168
|
-
* @param {string|null} [
|
|
169
|
-
* @returns {
|
|
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
|
-
this.
|
|
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
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
*
|
|
576
|
-
*
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
const
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
}
|
|
1
|
+
import { applyRouteMetadata } from './applyRouteMetadata.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {Object} RouteConfig
|
|
5
|
+
* @property {string} path
|
|
6
|
+
* @property {string} component
|
|
7
|
+
* @property {RouteConfig[]} [children]
|
|
8
|
+
* @property {Object} [metadata]
|
|
9
|
+
* @property {string} [fullPath]
|
|
10
|
+
* @property {string|null} [parentPath]
|
|
11
|
+
* @property {RouteConfig|null} [parentRoute]
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} RouteInfo
|
|
16
|
+
* @property {string} path the app path, without locale prefix
|
|
17
|
+
* @property {string} component
|
|
18
|
+
* @property {Object} params
|
|
19
|
+
* @property {Object} query
|
|
20
|
+
* @property {Object} metadata
|
|
21
|
+
* @property {string|null} locale the locale of this navigation (null without slice.translator)
|
|
22
|
+
* @property {string} url what the address bar shows (`/en/pricing` with routing)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {Object} GuardRedirect
|
|
27
|
+
* @property {string} path
|
|
28
|
+
* @property {boolean} [replace]
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {Object} RouteMatch
|
|
33
|
+
* @property {RouteConfig|null} route
|
|
34
|
+
* @property {Object} params
|
|
35
|
+
* @property {RouteConfig} [childRoute]
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @callback RouterNext
|
|
40
|
+
* @param {void|false|string|{ path: string, replace?: boolean }} [arg]
|
|
41
|
+
* @returns {void}
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
export default class Router {
|
|
45
|
+
/**
|
|
46
|
+
* @param {RouteConfig[]} routes
|
|
47
|
+
*/
|
|
48
|
+
constructor(routes) {
|
|
49
|
+
this.routes = routes;
|
|
50
|
+
this.activeRoute = null;
|
|
51
|
+
/** @type {Object|null} metadata of the route being shown */
|
|
52
|
+
this.activeMetadata = null;
|
|
53
|
+
this.pathToRouteMap = this.createPathToRouteMap(routes);
|
|
54
|
+
|
|
55
|
+
// O(1) case-insensitive lookup for static routes, so a miss on the exact map
|
|
56
|
+
// doesn't scan every route on each navigation. Param routes (which carry a
|
|
57
|
+
// precompiled `compiled` regex) are excluded — they go through the param loop.
|
|
58
|
+
this._staticLowerIndex = new Map();
|
|
59
|
+
for (const [pattern, route] of this.pathToRouteMap.entries()) {
|
|
60
|
+
if (!route.compiled) this._staticLowerIndex.set(pattern.toLowerCase(), route);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Navigation Guards
|
|
64
|
+
this._beforeEachGuard = null;
|
|
65
|
+
this._afterEachGuard = null;
|
|
66
|
+
|
|
67
|
+
// Router state
|
|
68
|
+
this._started = false;
|
|
69
|
+
this._autoStartTimeout = null;
|
|
70
|
+
|
|
71
|
+
// Sistema de caché optimizado
|
|
72
|
+
this.routeContainersCache = new Map();
|
|
73
|
+
this.lastCacheUpdate = 0;
|
|
74
|
+
this.CACHE_DURATION = 100; // ms - caché muy corto pero efectivo
|
|
75
|
+
|
|
76
|
+
// Observer para invalidar caché automáticamente
|
|
77
|
+
this.setupMutationObserver();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Inicializa el router
|
|
82
|
+
* Si el usuario no llama start() manualmente, se auto-inicia despues de un delay
|
|
83
|
+
* @returns {void}
|
|
84
|
+
*/
|
|
85
|
+
init() {
|
|
86
|
+
window.addEventListener('popstate', () => {
|
|
87
|
+
try {
|
|
88
|
+
this.onRouteChange();
|
|
89
|
+
} catch (error) {
|
|
90
|
+
slice.logger.error('Router', 'Error in popstate handler', error);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
this._autoStartTimeout = setTimeout(async () => {
|
|
95
|
+
if (!this._started) {
|
|
96
|
+
try {
|
|
97
|
+
slice.logger.info('Router', 'Auto-starting router (no manual start() called)');
|
|
98
|
+
await this.start();
|
|
99
|
+
} catch (error) {
|
|
100
|
+
slice.logger.error('Router', 'Auto-start failed', error);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}, 50);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Inicia el router y carga la ruta inicial
|
|
108
|
+
* OPCIONAL: Solo necesario si usas guards (beforeEach/afterEach)
|
|
109
|
+
* Si no lo llamas, el router se auto-inicia despues de 50ms
|
|
110
|
+
* @returns {Promise<void>}
|
|
111
|
+
*/
|
|
112
|
+
async start() {
|
|
113
|
+
// Prevenir múltiples llamadas
|
|
114
|
+
if (this._started) {
|
|
115
|
+
slice.logger.logWarning('Router', 'start() already called');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Cancelar auto-start si existe
|
|
120
|
+
if (this._autoStartTimeout) {
|
|
121
|
+
clearTimeout(this._autoStartTimeout);
|
|
122
|
+
this._autoStartTimeout = null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this._started = true;
|
|
126
|
+
await this.loadInitialRoute();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ============================================
|
|
130
|
+
// URL API — the router is the only reader of the URL
|
|
131
|
+
// ============================================
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The current app path: `window.location.pathname` without the locale prefix
|
|
135
|
+
* (`/en/pricing` → `/pricing`). Components compare this with their routes instead of
|
|
136
|
+
* reading `window.location` themselves.
|
|
137
|
+
* @returns {string}
|
|
138
|
+
*/
|
|
139
|
+
get path() {
|
|
140
|
+
return this._appPath(window.location.pathname);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The URL to put in an `<a href>` for an app path: `/pricing` → `/en/pricing` when the
|
|
145
|
+
* locale is in the URL, `/pricing` otherwise. `{ locale }` links to another language.
|
|
146
|
+
* External URLs, fragments and relative paths are returned as given.
|
|
147
|
+
* @param {string} path
|
|
148
|
+
* @param {{ locale?: string }} [options]
|
|
149
|
+
* @returns {string}
|
|
150
|
+
*/
|
|
151
|
+
href(path, options = {}) {
|
|
152
|
+
const translator = slice.translator;
|
|
153
|
+
return typeof translator?.localizePath === 'function' ? translator.localizePath(path, options.locale) : path;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* @param {string} url
|
|
158
|
+
* @returns {string}
|
|
159
|
+
*/
|
|
160
|
+
_appPath(url) {
|
|
161
|
+
const translator = slice.translator;
|
|
162
|
+
return typeof translator?.splitPath === 'function' ? translator.splitPath(url).path : url;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The locale a navigation lands in and the URL it shows.
|
|
167
|
+
* @param {string} appPath
|
|
168
|
+
* @param {string|null} [locale]
|
|
169
|
+
* @returns {{ locale: string|null, url: string }}
|
|
170
|
+
*/
|
|
171
|
+
_localeAndUrl(appPath, locale = null) {
|
|
172
|
+
const translator = slice.translator;
|
|
173
|
+
const active = translator?.enabled ? locale || translator.locale : null;
|
|
174
|
+
return { locale: active, url: this.href(appPath, active ? { locale: active } : {}) };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ============================================
|
|
178
|
+
// NAVIGATION GUARDS API
|
|
179
|
+
// ============================================
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Registra un guard que se ejecuta ANTES de cada navegacion.
|
|
183
|
+
* Puede bloquear o redirigir la navegacion mediante next().
|
|
184
|
+
* @param {(to: RouteInfo, from: RouteInfo, next: RouterNext) => void|Promise<void>} guard
|
|
185
|
+
* @returns {void}
|
|
186
|
+
*/
|
|
187
|
+
beforeEach(guard) {
|
|
188
|
+
if (typeof guard !== 'function') {
|
|
189
|
+
slice.logger.logError('Router', 'beforeEach expects a function');
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
this._beforeEachGuard = guard;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Registra un guard que se ejecuta DESPUES de cada navegacion.
|
|
197
|
+
* No puede bloquear la navegacion.
|
|
198
|
+
* @param {(to: RouteInfo, from: RouteInfo) => void} guard
|
|
199
|
+
* @returns {void}
|
|
200
|
+
*/
|
|
201
|
+
afterEach(guard) {
|
|
202
|
+
if (typeof guard !== 'function') {
|
|
203
|
+
slice.logger.logError('Router', 'afterEach expects a function');
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
this._afterEachGuard = guard;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Crea un objeto con información de ruta para los guards
|
|
211
|
+
* @param {Object} route - Objeto de ruta
|
|
212
|
+
* @param {Object} params - Parámetros de la ruta
|
|
213
|
+
* @param {String} requestedPath - Path original solicitado
|
|
214
|
+
* @returns {Object} Objeto con path, component, params, query, metadata
|
|
215
|
+
*/
|
|
216
|
+
/**
|
|
217
|
+
* Build route info used by guards and events.
|
|
218
|
+
* @param {RouteConfig|null} route
|
|
219
|
+
* @param {Object} [params]
|
|
220
|
+
* @param {string|null} [requestedPath]
|
|
221
|
+
* @returns {RouteInfo}
|
|
222
|
+
*/
|
|
223
|
+
_createRouteInfo(route, params = {}, requestedPath = null, locale = undefined) {
|
|
224
|
+
const path = route ? requestedPath || route.fullPath || route.path : requestedPath || '/404';
|
|
225
|
+
const where = this._localeAndUrl(path, locale);
|
|
226
|
+
|
|
227
|
+
if (!route) {
|
|
228
|
+
return {
|
|
229
|
+
path,
|
|
230
|
+
component: 'NotFound',
|
|
231
|
+
params: {},
|
|
232
|
+
query: this._parseQueryParams(),
|
|
233
|
+
metadata: {},
|
|
234
|
+
locale: where.locale,
|
|
235
|
+
url: where.url,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
path,
|
|
241
|
+
component: route.parentRoute ? route.parentRoute.component : route.component,
|
|
242
|
+
params: params,
|
|
243
|
+
query: this._parseQueryParams(),
|
|
244
|
+
metadata: route.metadata || {},
|
|
245
|
+
locale: where.locale,
|
|
246
|
+
url: where.url,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Parsea los query parameters de la URL actual
|
|
252
|
+
* @returns {Object} Objeto con los query params
|
|
253
|
+
*/
|
|
254
|
+
/**
|
|
255
|
+
* Parse query params from current URL.
|
|
256
|
+
* @returns {Object}
|
|
257
|
+
*/
|
|
258
|
+
_parseQueryParams() {
|
|
259
|
+
const queryString = window.location.search;
|
|
260
|
+
if (!queryString) return {};
|
|
261
|
+
|
|
262
|
+
const params = {};
|
|
263
|
+
const urlParams = new URLSearchParams(queryString);
|
|
264
|
+
|
|
265
|
+
for (const [key, value] of urlParams) {
|
|
266
|
+
params[key] = value;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return params;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Ejecuta el beforeEach guard si existe
|
|
274
|
+
* @param {Object} to - Información de ruta destino
|
|
275
|
+
* @param {Object} from - Información de ruta origen
|
|
276
|
+
* @returns {Object|null} Objeto con redirectPath y options, o null si continúa
|
|
277
|
+
*/
|
|
278
|
+
/**
|
|
279
|
+
* Execute beforeEach guard if defined.
|
|
280
|
+
* @param {RouteInfo} to
|
|
281
|
+
* @param {RouteInfo} from
|
|
282
|
+
* @returns {Promise<{ path: string|false, options: { replace?: boolean } }|null>}
|
|
283
|
+
*/
|
|
284
|
+
async _executeBeforeEachGuard(to, from) {
|
|
285
|
+
if (!this._beforeEachGuard) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
let redirectPath = null;
|
|
290
|
+
let redirectOptions = {};
|
|
291
|
+
let nextCalled = false;
|
|
292
|
+
|
|
293
|
+
const next = (arg) => {
|
|
294
|
+
if (nextCalled) {
|
|
295
|
+
slice.logger.logWarning('Router', 'next() called multiple times in guard');
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
nextCalled = true;
|
|
299
|
+
|
|
300
|
+
// Caso 1: Sin argumentos - continuar navegación
|
|
301
|
+
if (arg === undefined) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Caso 2: false - cancelar navegación
|
|
306
|
+
if (arg === false) {
|
|
307
|
+
redirectPath = false;
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Caso 3: String - redirección simple (backward compatibility)
|
|
312
|
+
if (typeof arg === 'string') {
|
|
313
|
+
redirectPath = arg;
|
|
314
|
+
redirectOptions = { replace: false };
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Caso 4: Objeto - redirección con opciones
|
|
319
|
+
if (typeof arg === 'object' && arg.path) {
|
|
320
|
+
redirectPath = arg.path;
|
|
321
|
+
redirectOptions = {
|
|
322
|
+
replace: arg.replace || false,
|
|
323
|
+
};
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Argumento inválido
|
|
328
|
+
slice.logger.logError(
|
|
329
|
+
'Router',
|
|
330
|
+
'Invalid argument passed to next(). Expected string, object with path, false, or undefined.'
|
|
331
|
+
);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
try {
|
|
335
|
+
await this._beforeEachGuard(to, from, next);
|
|
336
|
+
|
|
337
|
+
// Si no se llamó next(), loguear advertencia pero continuar
|
|
338
|
+
if (!nextCalled) {
|
|
339
|
+
slice.logger.logWarning('Router', 'beforeEach guard did not call next(). Navigation will continue.');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Retornar tanto el path como las opciones
|
|
343
|
+
if (redirectPath === false) return { path: false, options: {} };
|
|
344
|
+
return redirectPath ? { path: redirectPath, options: redirectOptions } : null;
|
|
345
|
+
} catch (error) {
|
|
346
|
+
slice.logger.error('Router', `Error in beforeEach guard from "${from?.path}" to "${to?.path}"`, error);
|
|
347
|
+
return { path: false, options: {} };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Ejecuta el afterEach guard si existe
|
|
353
|
+
* @param {Object} to - Información de ruta destino
|
|
354
|
+
* @param {Object} from - Información de ruta origen
|
|
355
|
+
*/
|
|
356
|
+
/**
|
|
357
|
+
* Execute afterEach guard if defined.
|
|
358
|
+
* @param {RouteInfo} to
|
|
359
|
+
* @param {RouteInfo} from
|
|
360
|
+
* @returns {void}
|
|
361
|
+
*/
|
|
362
|
+
_executeAfterEachGuard(to, from) {
|
|
363
|
+
if (!this._afterEachGuard) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
this._afterEachGuard(to, from);
|
|
369
|
+
} catch (error) {
|
|
370
|
+
slice.logger.logError('Router', 'Error in afterEach guard', error);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ============================================
|
|
375
|
+
// ROUTING CORE (MODIFICADO CON GUARDS)
|
|
376
|
+
// ============================================
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Navigate to a route path (guards run automatically).
|
|
380
|
+
*
|
|
381
|
+
* `path` is an app path (`/pricing`); the active locale is added to the URL when the
|
|
382
|
+
* translator has `routing`. `{ locale: 'en' }` navigates to another language: its
|
|
383
|
+
* dictionaries load before the URL changes, so the new page renders translated.
|
|
384
|
+
* @param {string} path
|
|
385
|
+
* @param {{ replace?: boolean, locale?: string }} [options] - `{ replace: true }` replaces history instead of pushing.
|
|
386
|
+
* @returns {Promise<void>}
|
|
387
|
+
*/
|
|
388
|
+
async navigate(path, options = {}, legacyOptions) {
|
|
389
|
+
// Backward compatibility with the previous signature navigate(path, _redirectChain, _options):
|
|
390
|
+
// if the 2nd argument is the internal redirect chain (an array) or a 3rd argument is passed,
|
|
391
|
+
// use the 3rd argument as the options object.
|
|
392
|
+
if (Array.isArray(options)) {
|
|
393
|
+
options = legacyOptions || {};
|
|
394
|
+
} else if (legacyOptions !== undefined) {
|
|
395
|
+
options = legacyOptions || options;
|
|
396
|
+
}
|
|
397
|
+
return this._navigateWithGuards(path, options || {}, []);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Internal navigation that tracks the guard redirection chain (loop protection).
|
|
402
|
+
* @param {string} path
|
|
403
|
+
* @param {{ replace?: boolean }} options
|
|
404
|
+
* @param {string[]} redirectChain
|
|
405
|
+
* @returns {Promise<void>}
|
|
406
|
+
*/
|
|
407
|
+
async _navigateWithGuards(path, options, redirectChain) {
|
|
408
|
+
const currentPath = this.path;
|
|
409
|
+
const translator = slice.translator;
|
|
410
|
+
|
|
411
|
+
// A path given with a prefix (`/en/pricing`) is the app path in that locale.
|
|
412
|
+
const split = typeof translator?.splitPath === 'function' ? translator.splitPath(path) : { locale: null, path };
|
|
413
|
+
path = split.path;
|
|
414
|
+
let targetLocale = options.locale || split.locale || null;
|
|
415
|
+
if (targetLocale && translator?.enabled) {
|
|
416
|
+
try {
|
|
417
|
+
targetLocale = await translator.preload(targetLocale);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
slice.logger.logError('Router', `Cannot navigate to "${path}" in "${targetLocale}"`, error);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
targetLocale = null;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Detectar loops infinitos: si ya visitamos esta ruta en la cadena de redirecciones
|
|
427
|
+
if (redirectChain.includes(path)) {
|
|
428
|
+
slice.logger.logError('Router', `Guard redirection loop detected: ${redirectChain.join(' → ')} → ${path}`);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Límite de seguridad: máximo 10 redirecciones
|
|
433
|
+
if (redirectChain.length >= 10) {
|
|
434
|
+
slice.logger.logError('Router', `Too many redirections: ${redirectChain.join(' → ')} → ${path}`);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Obtener información de ruta actual
|
|
439
|
+
const { route: fromRoute, params: fromParams } = this.matchRoute(currentPath);
|
|
440
|
+
const from = this._createRouteInfo(fromRoute, fromParams, currentPath);
|
|
441
|
+
|
|
442
|
+
// Obtener información de ruta destino
|
|
443
|
+
const { route: toRoute, params: toParams } = this.matchRoute(path);
|
|
444
|
+
const to = this._createRouteInfo(toRoute, toParams, path, targetLocale);
|
|
445
|
+
|
|
446
|
+
// EJECUTAR BEFORE EACH GUARD
|
|
447
|
+
const guardResult = await this._executeBeforeEachGuard(to, from);
|
|
448
|
+
|
|
449
|
+
// Si el guard redirige
|
|
450
|
+
if (guardResult && guardResult.path) {
|
|
451
|
+
// A guard redirect stays in the language the user was going to.
|
|
452
|
+
const redirectOptions = { ...(guardResult.options || {}) };
|
|
453
|
+
if (targetLocale && !redirectOptions.locale) redirectOptions.locale = targetLocale;
|
|
454
|
+
return this._navigateWithGuards(guardResult.path, redirectOptions, [...redirectChain, path]);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Si el guard cancela la navegación (next(false))
|
|
458
|
+
if (guardResult && guardResult.path === false) {
|
|
459
|
+
slice.logger.logInfo('Router', 'Navigation cancelled by guard');
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// No hay redirección - continuar con la navegación normal
|
|
464
|
+
// Usar replace o push según las opciones
|
|
465
|
+
if (options.replace) {
|
|
466
|
+
window.history.replaceState({}, path, window.location.origin + to.url);
|
|
467
|
+
} else {
|
|
468
|
+
window.history.pushState({}, path, window.location.origin + to.url);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Without routing the locale is not in the URL: switch it here. With routing,
|
|
472
|
+
// onRouteChange() follows the URL (dictionaries are already loaded).
|
|
473
|
+
if (targetLocale && !translator?.localePrefix && targetLocale !== translator.locale) {
|
|
474
|
+
await translator.setLocale(targetLocale);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
await this._performNavigation(to, from);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Método interno para ejecutar la navegación después de pasar los guards
|
|
482
|
+
* @param {Object} to - Información de ruta destino
|
|
483
|
+
* @param {Object} from - Información de ruta origen
|
|
484
|
+
*/
|
|
485
|
+
/**
|
|
486
|
+
* Perform navigation after guards.
|
|
487
|
+
* @param {RouteInfo} to
|
|
488
|
+
* @param {RouteInfo} from
|
|
489
|
+
* @returns {Promise<void>}
|
|
490
|
+
*/
|
|
491
|
+
async _performNavigation(to, from) {
|
|
492
|
+
// Renderizar la nueva ruta
|
|
493
|
+
await this.onRouteChange();
|
|
494
|
+
|
|
495
|
+
// EJECUTAR AFTER EACH GUARD
|
|
496
|
+
this._executeAfterEachGuard(to, from);
|
|
497
|
+
|
|
498
|
+
// Emitir evento de cambio de ruta
|
|
499
|
+
this._emitRouteChange(to, from);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* React to URL changes and render routes.
|
|
504
|
+
* @returns {Promise<void>}
|
|
505
|
+
*/
|
|
506
|
+
async onRouteChange() {
|
|
507
|
+
if (this.routeChangeTimeout) {
|
|
508
|
+
clearTimeout(this.routeChangeTimeout);
|
|
509
|
+
// This call supersedes a still-pending one; resolve the superseded
|
|
510
|
+
// promise so anything awaiting it doesn't hang — the new call does the
|
|
511
|
+
// real work.
|
|
512
|
+
if (this._routeChangeResolve) {
|
|
513
|
+
this._routeChangeResolve();
|
|
514
|
+
this._routeChangeResolve = null;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// Resolve only AFTER the debounced route handling finishes, so a caller
|
|
519
|
+
// that awaits this (e.g. _performNavigation) sees slice.router.activeRoute
|
|
520
|
+
// already updated before it emits router:change. (Previously the awaited
|
|
521
|
+
// promise resolved the instant the timer was scheduled, so router:change
|
|
522
|
+
// fired ~10ms before activeRoute was set.)
|
|
523
|
+
return new Promise((resolve) => {
|
|
524
|
+
this._routeChangeResolve = resolve;
|
|
525
|
+
this.routeChangeTimeout = setTimeout(async () => {
|
|
526
|
+
try {
|
|
527
|
+
await this._followUrlLocale();
|
|
528
|
+
const path = this.path;
|
|
529
|
+
const routeContainersFlag = await this.renderRoutesComponentsInPage();
|
|
530
|
+
|
|
531
|
+
if (routeContainersFlag) {
|
|
532
|
+
// An App Shell (Route/MultiRoute in the page) owns the view, so
|
|
533
|
+
// handleRoute() is skipped. The title still belongs to the
|
|
534
|
+
// matched section: apply its metadata, and keep activeMetadata
|
|
535
|
+
// so the translator can re-apply it on a locale change.
|
|
536
|
+
this._applyMatchedMetadata(path);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const { route, params, childRoute } = this.matchRoute(path);
|
|
541
|
+
if (route) {
|
|
542
|
+
await this.handleRoute(childRoute ? { ...childRoute, parentRoute: route } : route, params);
|
|
543
|
+
}
|
|
544
|
+
} catch (error) {
|
|
545
|
+
slice.logger.error('Router', `Route change failed for ${window.location.pathname}`, error);
|
|
546
|
+
} finally {
|
|
547
|
+
this._routeChangeResolve = null;
|
|
548
|
+
resolve();
|
|
549
|
+
}
|
|
550
|
+
}, 10);
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* With `routing`, the URL decides the locale: back/forward from `/en/x` to `/x`, or a
|
|
556
|
+
* navigation to another language, switches it before the route renders. An URL without
|
|
557
|
+
* the prefix "prefix" requires gets it (replaceState).
|
|
558
|
+
* @returns {Promise<void>}
|
|
559
|
+
*/
|
|
560
|
+
async _followUrlLocale() {
|
|
561
|
+
const translator = slice.translator;
|
|
562
|
+
if (!translator?.localePrefix) return;
|
|
563
|
+
const urlLocale = translator.localeOfUrl(window.location.pathname);
|
|
564
|
+
if (urlLocale && urlLocale !== translator.locale) {
|
|
565
|
+
try {
|
|
566
|
+
await translator.setLocale(urlLocale);
|
|
567
|
+
} catch (error) {
|
|
568
|
+
slice.logger.logError('Router', `Could not switch to "${urlLocale}" from the URL`, error);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
translator.syncUrl();
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Write a route's metadata (title, description) to the document. `{ "t": "key" }`
|
|
576
|
+
* fields are translated with slice.translator.
|
|
577
|
+
* @param {Object|null} metadata
|
|
578
|
+
* @returns {void}
|
|
579
|
+
*/
|
|
580
|
+
applyMetadata(metadata) {
|
|
581
|
+
applyRouteMetadata(metadata);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Apply the metadata of the route that owns `path`, without rendering it.
|
|
586
|
+
* Used when an App Shell (Route/MultiRoute in the page) owns the view: the
|
|
587
|
+
* section title (and any `{ t }` key) must still follow the URL, and
|
|
588
|
+
* `activeMetadata` is what the translator re-applies after a locale change.
|
|
589
|
+
* @param {string} path
|
|
590
|
+
* @returns {void}
|
|
591
|
+
*/
|
|
592
|
+
_applyMatchedMetadata(path) {
|
|
593
|
+
const match = this.matchRoute(path);
|
|
594
|
+
// A nested route renders through its parent shell, but the title belongs
|
|
595
|
+
// to the leaf the user navigated to.
|
|
596
|
+
const route = match.childRoute || match.route;
|
|
597
|
+
if (!route) return;
|
|
598
|
+
this.activeMetadata = route.metadata ?? null;
|
|
599
|
+
if (slice.routerConfig?.updateTitles !== false) {
|
|
600
|
+
this.applyMetadata(this.activeMetadata);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Build or update the active route component.
|
|
606
|
+
* @param {RouteConfig} route
|
|
607
|
+
* @param {Object} params
|
|
608
|
+
* @returns {Promise<void>}
|
|
609
|
+
*/
|
|
610
|
+
async handleRoute(route, params) {
|
|
611
|
+
if (!route) {
|
|
612
|
+
slice.logger.logError('Router', `No route matches "${this.path}" and no /404 route is declared`);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
// Titles are on by default: without this, a SPA keeps the first page's
|
|
616
|
+
// title while the user navigates. `sliceConfig.router.updateTitles: false`
|
|
617
|
+
// opts out; a `router:change` listener still wins, because this runs
|
|
618
|
+
// before the event.
|
|
619
|
+
// Kept so slice.translator can re-apply it (translated) after a locale change.
|
|
620
|
+
this.activeMetadata = route?.metadata ?? null;
|
|
621
|
+
if (slice.routerConfig?.updateTitles !== false) {
|
|
622
|
+
this.applyMetadata(this.activeMetadata);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const targetElement = document.querySelector('#app');
|
|
626
|
+
|
|
627
|
+
const componentName = route.parentRoute ? route.parentRoute.component : route.component;
|
|
628
|
+
const sliceId = `route-${componentName}`;
|
|
629
|
+
|
|
630
|
+
if (!targetElement) {
|
|
631
|
+
slice.logger.logError('Router', `Cannot render route "${route?.path ?? componentName}": no #app element in the document`);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const existingComponent = slice.controller.getComponent(sliceId);
|
|
636
|
+
|
|
637
|
+
if (slice.loading) {
|
|
638
|
+
slice.loading.start();
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
try {
|
|
642
|
+
if (existingComponent) {
|
|
643
|
+
targetElement.innerHTML = '';
|
|
644
|
+
if (existingComponent.update) {
|
|
645
|
+
existingComponent.props = { ...existingComponent.props, ...params };
|
|
646
|
+
await existingComponent.update();
|
|
647
|
+
}
|
|
648
|
+
targetElement.appendChild(existingComponent);
|
|
649
|
+
await this.renderRoutesInComponent(existingComponent);
|
|
650
|
+
} else {
|
|
651
|
+
const component = await slice.build(componentName, {
|
|
652
|
+
params,
|
|
653
|
+
sliceId: sliceId,
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// build() returns null whenever the component could not be produced
|
|
657
|
+
// (not registered, a resource fetch failed or was aborted, init()
|
|
658
|
+
// threw). Appending null raises "parameter 1 is not of type 'Node'",
|
|
659
|
+
// which buries the real cause. Report it and keep the current view.
|
|
660
|
+
if (!component) {
|
|
661
|
+
slice.logger.logError(
|
|
662
|
+
'Router',
|
|
663
|
+
`Cannot render route "${route?.path ?? componentName}": slice.build('${componentName}') returned null`
|
|
664
|
+
);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
targetElement.innerHTML = '';
|
|
669
|
+
targetElement.appendChild(component);
|
|
670
|
+
|
|
671
|
+
await this.renderRoutesInComponent(component);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Invalidar caché después de cambios importantes en el DOM
|
|
675
|
+
this.invalidateCache();
|
|
676
|
+
slice.router.activeRoute = route;
|
|
677
|
+
} finally {
|
|
678
|
+
if (slice.loading) {
|
|
679
|
+
slice.loading.stop();
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Load initial route and run guards.
|
|
686
|
+
* @returns {Promise<void>}
|
|
687
|
+
*/
|
|
688
|
+
async loadInitialRoute() {
|
|
689
|
+
const path = this.path;
|
|
690
|
+
const match = this.matchRoute(path);
|
|
691
|
+
const { route, params } = match;
|
|
692
|
+
if (!route) {
|
|
693
|
+
slice.logger.logError('Router', `No route matches "${path}" and no /404 route is declared`);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// Para la carga inicial, también ejecutar guards
|
|
698
|
+
const from = this._createRouteInfo(null, {}, null);
|
|
699
|
+
const to = this._createRouteInfo(route, params, path);
|
|
700
|
+
|
|
701
|
+
// EJECUTAR BEFORE EACH GUARD en carga inicial
|
|
702
|
+
const guardResult = await this._executeBeforeEachGuard(to, from);
|
|
703
|
+
|
|
704
|
+
if (guardResult && guardResult.path) {
|
|
705
|
+
return this.navigate(guardResult.path, guardResult.options || {});
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Si el guard cancela la navegación inicial (caso raro pero posible)
|
|
709
|
+
if (guardResult && guardResult.path === false) {
|
|
710
|
+
slice.logger.logWarning('Router', 'Initial route navigation cancelled by guard');
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
await this.handleRoute(match.childRoute ? { ...match.childRoute, parentRoute: route } : route, params);
|
|
715
|
+
|
|
716
|
+
// EJECUTAR AFTER EACH GUARD en carga inicial
|
|
717
|
+
this._executeAfterEachGuard(to, from);
|
|
718
|
+
|
|
719
|
+
// Emitir evento de cambio de ruta
|
|
720
|
+
this._emitRouteChange(to, from);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Emitir evento de cambio de ruta
|
|
725
|
+
* @param {Object} to
|
|
726
|
+
* @param {Object} from
|
|
727
|
+
*/
|
|
728
|
+
/**
|
|
729
|
+
* Emit route change event.
|
|
730
|
+
* @param {RouteInfo} to
|
|
731
|
+
* @param {RouteInfo} from
|
|
732
|
+
* @returns {void}
|
|
733
|
+
*/
|
|
734
|
+
_emitRouteChange(to, from) {
|
|
735
|
+
const payload = { to, from };
|
|
736
|
+
|
|
737
|
+
if (slice.eventsConfig?.enabled && slice.events && typeof slice.events.emit === 'function') {
|
|
738
|
+
slice.events.emit('router:change', payload);
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
window.dispatchEvent(new CustomEvent('router:change', { detail: payload }));
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// ============================================
|
|
746
|
+
// MÉTODOS EXISTENTES (SIN CAMBIOS)
|
|
747
|
+
// ============================================
|
|
748
|
+
|
|
749
|
+
setupMutationObserver() {
|
|
750
|
+
if (typeof MutationObserver !== 'undefined') {
|
|
751
|
+
this.observer = new MutationObserver((mutations) => {
|
|
752
|
+
try {
|
|
753
|
+
let shouldInvalidateCache = false;
|
|
754
|
+
|
|
755
|
+
mutations.forEach((mutation) => {
|
|
756
|
+
if (mutation.type === 'childList') {
|
|
757
|
+
const addedNodes = Array.from(mutation.addedNodes || []);
|
|
758
|
+
const removedNodes = Array.from(mutation.removedNodes || []);
|
|
759
|
+
|
|
760
|
+
const hasRouteNodes = [...addedNodes, ...removedNodes].some(
|
|
761
|
+
(node) =>
|
|
762
|
+
node.nodeType === Node.ELEMENT_NODE &&
|
|
763
|
+
(node.tagName === 'SLICE-ROUTE' ||
|
|
764
|
+
node.tagName === 'SLICE-MULTI-ROUTE' ||
|
|
765
|
+
node.querySelector?.('slice-route, slice-multi-route'))
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
if (hasRouteNodes) {
|
|
769
|
+
shouldInvalidateCache = true;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
if (shouldInvalidateCache) {
|
|
775
|
+
this.invalidateCache();
|
|
776
|
+
}
|
|
777
|
+
} catch (error) {
|
|
778
|
+
slice.logger.error('Router', 'Error in MutationObserver callback', error);
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
this.observer.observe(document.body, {
|
|
783
|
+
childList: true,
|
|
784
|
+
subtree: true,
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
invalidateCache() {
|
|
790
|
+
this.routeContainersCache.clear();
|
|
791
|
+
this.lastCacheUpdate = 0;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
createPathToRouteMap(routes, basePath = '', parentRoute = null) {
|
|
795
|
+
const pathToRouteMap = new Map();
|
|
796
|
+
|
|
797
|
+
for (const route of routes) {
|
|
798
|
+
const fullPath = `${basePath}${route.path}`.replace(/\/+/g, '/');
|
|
799
|
+
|
|
800
|
+
const routeWithParent = {
|
|
801
|
+
...route,
|
|
802
|
+
fullPath,
|
|
803
|
+
parentPath: parentRoute ? parentRoute.fullPath : null,
|
|
804
|
+
parentRoute: parentRoute,
|
|
805
|
+
};
|
|
806
|
+
|
|
807
|
+
// Compile parameterized patterns once at map-build time instead of on every
|
|
808
|
+
// navigation. Static routes leave `compiled` undefined.
|
|
809
|
+
if (fullPath.includes('${')) {
|
|
810
|
+
routeWithParent.compiled = this.compilePathPattern(fullPath);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
pathToRouteMap.set(fullPath, routeWithParent);
|
|
814
|
+
|
|
815
|
+
if (route.children) {
|
|
816
|
+
const childPathToRouteMap = this.createPathToRouteMap(route.children, fullPath, routeWithParent);
|
|
817
|
+
|
|
818
|
+
for (const [childPath, childRoute] of childPathToRouteMap.entries()) {
|
|
819
|
+
pathToRouteMap.set(childPath, childRoute);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return pathToRouteMap;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Render any Route/MultiRoute components in a container.
|
|
829
|
+
* @param {Document|HTMLElement} [searchContainer]
|
|
830
|
+
* @returns {Promise<boolean>}
|
|
831
|
+
*/
|
|
832
|
+
async renderRoutesComponentsInPage(searchContainer = document) {
|
|
833
|
+
let routerContainersFlag = false;
|
|
834
|
+
const routeContainers = this.getCachedRouteContainers(searchContainer);
|
|
835
|
+
|
|
836
|
+
for (const routeContainer of routeContainers) {
|
|
837
|
+
try {
|
|
838
|
+
if (!routeContainer.isConnected) {
|
|
839
|
+
this.invalidateCache();
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
let response = await routeContainer.renderIfCurrentRoute();
|
|
844
|
+
if (response) {
|
|
845
|
+
// The container owns the view; the route object (not its props) is what
|
|
846
|
+
// `activeRoute` promises to callers.
|
|
847
|
+
const match = this.matchRoute(this.path);
|
|
848
|
+
this.activeRoute = match.childRoute || match.route || null;
|
|
849
|
+
routerContainersFlag = true;
|
|
850
|
+
}
|
|
851
|
+
} catch (error) {
|
|
852
|
+
const containerName = routeContainer?.tagName || routeContainer?.constructor?.name || 'unknown';
|
|
853
|
+
slice.logger.error('Router', `Error rendering route container "${containerName}"`, error);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
return routerContainersFlag;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
getCachedRouteContainers(container) {
|
|
861
|
+
const containerKey = container === document ? 'document' : container.sliceId || 'anonymous';
|
|
862
|
+
const now = Date.now();
|
|
863
|
+
|
|
864
|
+
if (this.routeContainersCache.has(containerKey) && now - this.lastCacheUpdate < this.CACHE_DURATION) {
|
|
865
|
+
return this.routeContainersCache.get(containerKey);
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const routeContainers = this.findAllRouteContainersOptimized(container);
|
|
869
|
+
this.routeContainersCache.set(containerKey, routeContainers);
|
|
870
|
+
this.lastCacheUpdate = now;
|
|
871
|
+
|
|
872
|
+
return routeContainers;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
findAllRouteContainersOptimized(container) {
|
|
876
|
+
const routeContainers = [];
|
|
877
|
+
|
|
878
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, {
|
|
879
|
+
acceptNode: (node) => {
|
|
880
|
+
if (node.tagName === 'SLICE-ROUTE' || node.tagName === 'SLICE-MULTI-ROUTE') {
|
|
881
|
+
return NodeFilter.FILTER_ACCEPT;
|
|
882
|
+
}
|
|
883
|
+
return NodeFilter.FILTER_SKIP;
|
|
884
|
+
},
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
let node;
|
|
888
|
+
while ((node = walker.nextNode())) {
|
|
889
|
+
routeContainers.push(node);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
return routeContainers;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Render route containers inside a component.
|
|
897
|
+
* @param {HTMLElement} component
|
|
898
|
+
* @returns {Promise<boolean>}
|
|
899
|
+
*/
|
|
900
|
+
async renderRoutesInComponent(component) {
|
|
901
|
+
if (!component) {
|
|
902
|
+
slice.logger.logWarning('Router', 'No component provided for route rendering');
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
return await this.renderRoutesComponentsInPage(component);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Match a path to a configured route.
|
|
911
|
+
* @param {string} path
|
|
912
|
+
* @returns {RouteMatch}
|
|
913
|
+
*/
|
|
914
|
+
matchRoute(path) {
|
|
915
|
+
// Routes are declared without locale: drop a known prefix ('/en/about' -> '/about'),
|
|
916
|
+
// and the query / hash, which never take part in matching.
|
|
917
|
+
path = this._appPath(String(path || '/')).split(/[?#]/)[0] || '/';
|
|
918
|
+
// Normalize a trailing slash ('/about/' -> '/about'); keep root '/' as-is.
|
|
919
|
+
path = path.length > 1 ? path.replace(/\/+$/, '') : path;
|
|
920
|
+
// Exact match first (fast path), then a case-insensitive match on static paths
|
|
921
|
+
// so '/About' resolves to a route declared as '/about'.
|
|
922
|
+
let exactMatch = this.pathToRouteMap.get(path);
|
|
923
|
+
if (!exactMatch) {
|
|
924
|
+
exactMatch = this._staticLowerIndex.get(path.toLowerCase());
|
|
925
|
+
}
|
|
926
|
+
if (exactMatch) {
|
|
927
|
+
if (exactMatch.parentRoute) {
|
|
928
|
+
return {
|
|
929
|
+
route: exactMatch.parentRoute,
|
|
930
|
+
params: {},
|
|
931
|
+
childRoute: exactMatch,
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
return { route: exactMatch, params: {} };
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
for (const route of this.pathToRouteMap.values()) {
|
|
938
|
+
if (route.compiled) {
|
|
939
|
+
const { regex, paramNames } = route.compiled;
|
|
940
|
+
const match = path.match(regex);
|
|
941
|
+
if (match) {
|
|
942
|
+
const params = {};
|
|
943
|
+
paramNames.forEach((name, i) => {
|
|
944
|
+
params[name] = match[i + 1];
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
if (route.parentRoute) {
|
|
948
|
+
return {
|
|
949
|
+
route: route.parentRoute,
|
|
950
|
+
params: params,
|
|
951
|
+
childRoute: route,
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
return { route, params };
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
const notFoundRoute = this.pathToRouteMap.get('/404');
|
|
961
|
+
return { route: notFoundRoute, params: {} };
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* Compile a path pattern with ${param} segments.
|
|
966
|
+
* @param {string} pattern
|
|
967
|
+
* @returns {{ regex: RegExp, paramNames: string[] }}
|
|
968
|
+
*/
|
|
969
|
+
compilePathPattern(pattern) {
|
|
970
|
+
const paramNames = [];
|
|
971
|
+
const regexPattern =
|
|
972
|
+
'^' +
|
|
973
|
+
pattern.replace(/\$\{([^}]+)\}/g, (_, paramName) => {
|
|
974
|
+
paramNames.push(paramName);
|
|
975
|
+
return '([^/]+)';
|
|
976
|
+
}) +
|
|
977
|
+
'$';
|
|
978
|
+
|
|
979
|
+
// 'i' flag: paths match case-insensitively. Captured param values keep their original case.
|
|
980
|
+
return { regex: new RegExp(regexPattern, 'i'), paramNames };
|
|
981
|
+
}
|
|
982
|
+
}
|