@sigmela/router 0.2.8 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -1
- package/lib/module/Drawer/Drawer.js +250 -0
- package/lib/module/Drawer/DrawerContext.js +4 -0
- package/lib/module/Drawer/DrawerIcon.web.js +47 -0
- package/lib/module/Drawer/RenderDrawer.native.js +244 -0
- package/lib/module/Drawer/RenderDrawer.web.js +197 -0
- package/lib/module/Drawer/useDrawer.js +11 -0
- package/lib/module/Navigation.js +4 -2
- package/lib/module/NavigationStack.js +14 -4
- package/lib/module/Router.js +214 -60
- package/lib/module/RouterContext.js +1 -1
- package/lib/module/ScreenStack/ScreenStack.web.js +78 -12
- package/lib/module/ScreenStackItem/ScreenStackItem.js +7 -7
- package/lib/module/ScreenStackItem/ScreenStackItem.web.js +65 -6
- package/lib/module/ScreenStackSheetItem/ScreenStackSheetItem.native.js +4 -5
- package/lib/module/SplitView/RenderSplitView.web.js +5 -7
- package/lib/module/SplitView/SplitView.js +10 -2
- package/lib/module/StackRenderer.js +10 -4
- package/lib/module/TabBar/RenderTabBar.native.js +55 -24
- package/lib/module/TabBar/RenderTabBar.web.js +25 -2
- package/lib/module/TabBar/TabBar.js +8 -1
- package/lib/module/TabBar/TabIcon.web.js +12 -7
- package/lib/module/index.js +2 -0
- package/lib/module/styles.css +255 -91
- package/lib/typescript/src/Drawer/Drawer.d.ts +100 -0
- package/lib/typescript/src/Drawer/DrawerContext.d.ts +3 -0
- package/lib/typescript/src/Drawer/DrawerIcon.web.d.ts +7 -0
- package/lib/typescript/src/Drawer/RenderDrawer.native.d.ts +8 -0
- package/lib/typescript/src/Drawer/RenderDrawer.web.d.ts +8 -0
- package/lib/typescript/src/Drawer/useDrawer.d.ts +2 -0
- package/lib/typescript/src/NavigationStack.d.ts +1 -0
- package/lib/typescript/src/Router.d.ts +13 -0
- package/lib/typescript/src/ScreenStack/ScreenStackContext.d.ts +1 -1
- package/lib/typescript/src/ScreenStack/animationHelpers.d.ts +1 -1
- package/lib/typescript/src/SplitView/SplitView.d.ts +2 -0
- package/lib/typescript/src/TabBar/TabBar.d.ts +10 -1
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/types.d.ts +12 -3
- package/package.json +28 -12
package/lib/module/Router.js
CHANGED
|
@@ -25,8 +25,10 @@ export class Router {
|
|
|
25
25
|
stackById = new Map();
|
|
26
26
|
routeById = new Map();
|
|
27
27
|
stackActivators = new Map();
|
|
28
|
+
compiledRouteById = new Map();
|
|
28
29
|
stackHistories = new Map();
|
|
29
30
|
activeRoute = null;
|
|
31
|
+
activeRouteListeners = new Set();
|
|
30
32
|
rootListeners = new Set();
|
|
31
33
|
rootTransition = undefined;
|
|
32
34
|
// Root swaps should behave like a fresh initial mount (no enter animation).
|
|
@@ -34,9 +36,16 @@ export class Router {
|
|
|
34
36
|
suppressRootTransitionOnNextRead = false;
|
|
35
37
|
lastBrowserIndex = 0;
|
|
36
38
|
suppressHistorySyncCount = 0;
|
|
37
|
-
|
|
38
39
|
// Used to prevent stale controller-driven navigations (controller calls present later).
|
|
39
40
|
navigationToken = 0;
|
|
41
|
+
// parsePath one-slot cache
|
|
42
|
+
lastParsedPath = null;
|
|
43
|
+
lastParsedResult = null;
|
|
44
|
+
|
|
45
|
+
// matchBaseRoute one-slot cache
|
|
46
|
+
lastMatchedPathname = null;
|
|
47
|
+
lastMatchedQuery = null;
|
|
48
|
+
lastMatchedResult = null;
|
|
40
49
|
constructor(config) {
|
|
41
50
|
this.debugEnabled = config.debug ?? false;
|
|
42
51
|
this.routerScreenOptions = config.screenOptions;
|
|
@@ -82,6 +91,11 @@ export class Router {
|
|
|
82
91
|
}
|
|
83
92
|
}
|
|
84
93
|
navigate = path => {
|
|
94
|
+
if (!path || typeof path !== 'string') {
|
|
95
|
+
if (__DEV__) throw new Error('Router.navigate: path must be a non-empty string');
|
|
96
|
+
console.warn('[Router] navigate called with invalid path');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
85
99
|
if (this.isWebEnv()) {
|
|
86
100
|
const syncWithUrl = this.shouldSyncPathWithUrl(path);
|
|
87
101
|
this.pushUrl(path, {
|
|
@@ -92,6 +106,11 @@ export class Router {
|
|
|
92
106
|
this.performNavigation(path, 'push');
|
|
93
107
|
};
|
|
94
108
|
replace = (path, dedupe) => {
|
|
109
|
+
if (!path || typeof path !== 'string') {
|
|
110
|
+
if (__DEV__) throw new Error('Router.replace: path must be a non-empty string');
|
|
111
|
+
console.warn('[Router] replace called with invalid path');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
95
114
|
if (this.isWebEnv()) {
|
|
96
115
|
const syncWithUrl = this.shouldSyncPathWithUrl(path);
|
|
97
116
|
this.replaceUrl(path, {
|
|
@@ -113,6 +132,11 @@ export class Router {
|
|
|
113
132
|
* and browser URL can get out of sync).
|
|
114
133
|
*/
|
|
115
134
|
reset = path => {
|
|
135
|
+
if (!path || typeof path !== 'string') {
|
|
136
|
+
if (__DEV__) throw new Error('Router.reset: path must be a non-empty string');
|
|
137
|
+
console.warn('[Router] reset called with invalid path');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
116
140
|
if (this.isWebEnv()) {
|
|
117
141
|
const prevHistory = this.state.history;
|
|
118
142
|
// Important: clear current history before matching routes for the new URL.
|
|
@@ -190,7 +214,7 @@ export class Router {
|
|
|
190
214
|
}
|
|
191
215
|
|
|
192
216
|
// Check if modal has a childNode (NavigationStack added via addModal)
|
|
193
|
-
const compiled = this.
|
|
217
|
+
const compiled = this.getCompiledRoute(modalItem.routeId);
|
|
194
218
|
const childNode = compiled?.childNode;
|
|
195
219
|
if (childNode) {
|
|
196
220
|
// Modal stack: remove all items from child stack AND the modal wrapper item
|
|
@@ -200,19 +224,23 @@ export class Router {
|
|
|
200
224
|
modalKey: modalItem.key
|
|
201
225
|
});
|
|
202
226
|
this.markStackDismissed(childStackId);
|
|
227
|
+
|
|
228
|
+
// Clear child stack's history cache BEFORE setState so listeners see clean state
|
|
229
|
+
this.stackHistories.delete(childStackId);
|
|
203
230
|
const newHistory = this.state.history.filter(item => item.stackId !== childStackId && item.key !== modalItem.key);
|
|
204
231
|
this.setState({
|
|
205
232
|
history: newHistory
|
|
206
233
|
});
|
|
207
|
-
|
|
208
|
-
// Clear child stack's history cache
|
|
209
|
-
this.stackHistories.delete(childStackId);
|
|
210
234
|
} else {
|
|
211
235
|
// Simple modal: just pop the modal item
|
|
236
|
+
// applyHistoryChange already calls recomputeActiveRoute + emit(this.listeners)
|
|
212
237
|
this.applyHistoryChange('pop', modalItem);
|
|
213
238
|
}
|
|
214
|
-
|
|
215
|
-
|
|
239
|
+
if (childNode) {
|
|
240
|
+
// childNode branch uses setState directly, so we need to emit manually
|
|
241
|
+
this.recomputeActiveRoute();
|
|
242
|
+
this.emit(this.listeners);
|
|
243
|
+
}
|
|
216
244
|
|
|
217
245
|
// Sync URL on web
|
|
218
246
|
if (this.isWebEnv()) {
|
|
@@ -228,6 +256,12 @@ export class Router {
|
|
|
228
256
|
this.listeners.delete(listener);
|
|
229
257
|
};
|
|
230
258
|
}
|
|
259
|
+
subscribeActiveRoute = listener => {
|
|
260
|
+
this.activeRouteListeners.add(listener);
|
|
261
|
+
return () => {
|
|
262
|
+
this.activeRouteListeners.delete(listener);
|
|
263
|
+
};
|
|
264
|
+
};
|
|
231
265
|
getStackHistory = stackId => {
|
|
232
266
|
if (!stackId) return EMPTY_ARRAY;
|
|
233
267
|
if (!this.stackHistories.has(stackId)) {
|
|
@@ -268,7 +302,7 @@ export class Router {
|
|
|
268
302
|
return () => this.rootListeners.delete(listener);
|
|
269
303
|
}
|
|
270
304
|
emitRootChange() {
|
|
271
|
-
this.rootListeners
|
|
305
|
+
this.emit(this.rootListeners);
|
|
272
306
|
}
|
|
273
307
|
getRootTransition() {
|
|
274
308
|
if (this.suppressRootTransitionOnNextRead) {
|
|
@@ -292,10 +326,18 @@ export class Router {
|
|
|
292
326
|
this.registry.length = 0;
|
|
293
327
|
this.stackById.clear();
|
|
294
328
|
this.routeById.clear();
|
|
329
|
+
this.compiledRouteById.clear();
|
|
295
330
|
this.stackActivators.clear();
|
|
331
|
+
this.lastMatchedPathname = null;
|
|
332
|
+
this.lastMatchedQuery = null;
|
|
333
|
+
this.lastMatchedResult = null;
|
|
296
334
|
this.state = {
|
|
297
335
|
history: []
|
|
298
336
|
};
|
|
337
|
+
this.stackHistories.clear();
|
|
338
|
+
this.sheetDismissers.clear();
|
|
339
|
+
this.dismissedStackIds.clear();
|
|
340
|
+
this.stackListeners.clear();
|
|
299
341
|
this.buildRegistry();
|
|
300
342
|
this.seedInitialHistory();
|
|
301
343
|
this.recomputeActiveRoute();
|
|
@@ -410,7 +452,7 @@ export class Router {
|
|
|
410
452
|
const top = history[history.length - 1];
|
|
411
453
|
if (top) {
|
|
412
454
|
const meta = this.routeById.get(top.routeId);
|
|
413
|
-
|
|
455
|
+
const newRoute = meta ? {
|
|
414
456
|
...meta,
|
|
415
457
|
routeId: top.routeId,
|
|
416
458
|
params: top.params,
|
|
@@ -423,12 +465,23 @@ export class Router {
|
|
|
423
465
|
query: top.query,
|
|
424
466
|
path: top.path
|
|
425
467
|
};
|
|
468
|
+
|
|
469
|
+
// Shallow compare to keep stable reference and avoid unnecessary re-renders
|
|
470
|
+
if (this.activeRoute && this.activeRoute.routeId === newRoute.routeId && this.activeRoute.stackId === newRoute.stackId && this.activeRoute.path === newRoute.path && this.activeRoute.params === newRoute.params && this.activeRoute.query === newRoute.query) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
this.activeRoute = newRoute;
|
|
474
|
+
this.emit(this.activeRouteListeners);
|
|
426
475
|
return;
|
|
427
476
|
}
|
|
428
477
|
}
|
|
429
|
-
this.activeRoute
|
|
478
|
+
if (this.activeRoute !== null) {
|
|
479
|
+
this.activeRoute = null;
|
|
480
|
+
this.emit(this.activeRouteListeners);
|
|
481
|
+
}
|
|
430
482
|
}
|
|
431
483
|
performNavigation(path, action, opts) {
|
|
484
|
+
if (this.destroyed) return;
|
|
432
485
|
const {
|
|
433
486
|
pathname,
|
|
434
487
|
query
|
|
@@ -453,6 +506,7 @@ export class Router {
|
|
|
453
506
|
if (__DEV__) {
|
|
454
507
|
throw new Error(`Route not found: "${pathname}"`);
|
|
455
508
|
}
|
|
509
|
+
console.warn(`[Router] Route not found: "${pathname}"`);
|
|
456
510
|
return;
|
|
457
511
|
}
|
|
458
512
|
|
|
@@ -507,7 +561,7 @@ export class Router {
|
|
|
507
561
|
...existing,
|
|
508
562
|
routeId: base.routeId,
|
|
509
563
|
params: normalizedParams,
|
|
510
|
-
query
|
|
564
|
+
query,
|
|
511
565
|
path: pathname,
|
|
512
566
|
component: base.component,
|
|
513
567
|
options: this.mergeOptions(base.options, base.stackId),
|
|
@@ -544,7 +598,7 @@ export class Router {
|
|
|
544
598
|
});
|
|
545
599
|
if (sameIdentity && sameQuery) {
|
|
546
600
|
this.log('dedupe: already at target, syncing state');
|
|
547
|
-
this.syncStateForSameRoute(base, pathname
|
|
601
|
+
this.syncStateForSameRoute(base, pathname);
|
|
548
602
|
return;
|
|
549
603
|
}
|
|
550
604
|
if (sameIdentity && !sameQuery) {
|
|
@@ -553,7 +607,7 @@ export class Router {
|
|
|
553
607
|
const updatedTop = {
|
|
554
608
|
...top,
|
|
555
609
|
params: normalizedParams,
|
|
556
|
-
query
|
|
610
|
+
query,
|
|
557
611
|
path: pathname
|
|
558
612
|
};
|
|
559
613
|
this.applyHistoryChange('replace', updatedTop);
|
|
@@ -567,7 +621,7 @@ export class Router {
|
|
|
567
621
|
const updatedExisting = {
|
|
568
622
|
...existing,
|
|
569
623
|
params: normalizedParams,
|
|
570
|
-
query
|
|
624
|
+
query,
|
|
571
625
|
path: pathname
|
|
572
626
|
};
|
|
573
627
|
this.log('dedupe: found existing item, calling popTo', {
|
|
@@ -622,7 +676,7 @@ export class Router {
|
|
|
622
676
|
component: matched.component,
|
|
623
677
|
options: this.mergeOptions(matched.options, matched.stackId),
|
|
624
678
|
params: normalizedParams,
|
|
625
|
-
query
|
|
679
|
+
query,
|
|
626
680
|
passProps,
|
|
627
681
|
stackId: matched.stackId,
|
|
628
682
|
pattern: matched.path,
|
|
@@ -631,7 +685,12 @@ export class Router {
|
|
|
631
685
|
}
|
|
632
686
|
applyHistoryChange(action, item) {
|
|
633
687
|
const stackId = item.stackId;
|
|
634
|
-
if (!stackId)
|
|
688
|
+
if (!stackId) {
|
|
689
|
+
if (__DEV__) {
|
|
690
|
+
console.warn(`[Router] Navigation dropped: item has no stackId (routeId: ${item.routeId}, path: ${item.path})`);
|
|
691
|
+
}
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
635
694
|
const prevHist = this.state.history;
|
|
636
695
|
let nextHist = prevHist;
|
|
637
696
|
this.log('applyHistoryChange', {
|
|
@@ -669,6 +728,7 @@ export class Router {
|
|
|
669
728
|
const copy = [...prevHist];
|
|
670
729
|
for (let i = copy.length - 1; i >= 0; i--) {
|
|
671
730
|
const h = copy[i];
|
|
731
|
+
if (!h) continue;
|
|
672
732
|
if (h.stackId === stackId) {
|
|
673
733
|
copy.splice(i, 1);
|
|
674
734
|
break;
|
|
@@ -682,6 +742,7 @@ export class Router {
|
|
|
682
742
|
let foundItem = null;
|
|
683
743
|
for (let i = prevHist.length - 1; i >= 0; i--) {
|
|
684
744
|
const h = prevHist[i];
|
|
745
|
+
if (!h) continue;
|
|
685
746
|
if (h.stackId !== stackId) {
|
|
686
747
|
continue;
|
|
687
748
|
}
|
|
@@ -705,27 +766,51 @@ export class Router {
|
|
|
705
766
|
if (itemIndex >= 0) {
|
|
706
767
|
copy.splice(itemIndex, 1);
|
|
707
768
|
}
|
|
708
|
-
copy.
|
|
769
|
+
let insertPos = copy.length;
|
|
770
|
+
for (let i = copy.length - 1; i >= 0; i--) {
|
|
771
|
+
if (copy[i]?.stackId === stackId) {
|
|
772
|
+
insertPos = i + 1;
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
copy.splice(insertPos, 0, updatedItem);
|
|
709
777
|
nextHist = copy;
|
|
710
778
|
}
|
|
711
779
|
this.setState({
|
|
712
780
|
history: nextHist
|
|
713
|
-
});
|
|
781
|
+
}, item.stackId);
|
|
714
782
|
this.recomputeActiveRoute();
|
|
715
783
|
this.emit(this.listeners);
|
|
716
784
|
}
|
|
717
|
-
setState(next) {
|
|
785
|
+
setState(next, affectedStackId) {
|
|
718
786
|
const prev = this.state;
|
|
719
787
|
const nextState = {
|
|
720
788
|
history: next.history ?? prev.history
|
|
721
789
|
};
|
|
722
790
|
this.state = nextState;
|
|
723
791
|
if (nextState.history !== prev.history) {
|
|
724
|
-
this.updateStackHistories();
|
|
792
|
+
this.updateStackHistories(affectedStackId);
|
|
725
793
|
}
|
|
726
794
|
this.log('setState', nextState);
|
|
727
795
|
}
|
|
728
|
-
updateStackHistories() {
|
|
796
|
+
updateStackHistories(affectedStackId) {
|
|
797
|
+
if (affectedStackId) {
|
|
798
|
+
// Fast path: only update the one affected stack — O(H) instead of O(S×H)
|
|
799
|
+
const newHist = this.state.history.filter(h => h.stackId === affectedStackId);
|
|
800
|
+
const oldHist = this.stackHistories.get(affectedStackId);
|
|
801
|
+
if (!this.areArraysEqual(oldHist, newHist)) {
|
|
802
|
+
if (newHist.length === 0) {
|
|
803
|
+
this.stackHistories.delete(affectedStackId);
|
|
804
|
+
} else {
|
|
805
|
+
this.stackHistories.set(affectedStackId, newHist);
|
|
806
|
+
}
|
|
807
|
+
const listeners = this.stackListeners.get(affectedStackId);
|
|
808
|
+
if (listeners) this.emit(listeners);
|
|
809
|
+
}
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// Slow path: full recompute — used by buildHistoryFromUrl, setRoot, etc.
|
|
729
814
|
const stackIds = new Set();
|
|
730
815
|
this.state.history.forEach(item => {
|
|
731
816
|
if (item.stackId) stackIds.add(item.stackId);
|
|
@@ -745,20 +830,21 @@ export class Router {
|
|
|
745
830
|
if (!currentStackIds.has(stackId) && history.length > 0) {
|
|
746
831
|
this.stackHistories.delete(stackId);
|
|
747
832
|
changedStackIds.add(stackId);
|
|
748
|
-
}
|
|
833
|
+
}
|
|
749
834
|
});
|
|
750
835
|
changedStackIds.forEach(stackId => {
|
|
751
836
|
this.emit(this.stackListeners.get(stackId));
|
|
752
837
|
});
|
|
753
838
|
}
|
|
754
839
|
areArraysEqual(a, b) {
|
|
840
|
+
if (!a) return false;
|
|
755
841
|
if (a.length !== b.length) return false;
|
|
756
842
|
for (let i = 0; i < a.length; i++) {
|
|
757
843
|
if (a[i] !== b[i]) return false;
|
|
758
844
|
}
|
|
759
845
|
return true;
|
|
760
846
|
}
|
|
761
|
-
syncStateForSameRoute(base, pathname
|
|
847
|
+
syncStateForSameRoute(base, pathname) {
|
|
762
848
|
if (!base.stackId) {
|
|
763
849
|
this.recomputeActiveRoute();
|
|
764
850
|
this.emit(this.listeners);
|
|
@@ -838,7 +924,7 @@ export class Router {
|
|
|
838
924
|
for (let i = stackHistory.length - 1; i >= 0; i--) {
|
|
839
925
|
const item = stackHistory[i];
|
|
840
926
|
if (item) {
|
|
841
|
-
const compiled = this.
|
|
927
|
+
const compiled = this.getCompiledRoute(item.routeId);
|
|
842
928
|
if (compiled?.childNode && canSwitchToRoute(compiled.childNode)) {
|
|
843
929
|
return compiled.childNode;
|
|
844
930
|
}
|
|
@@ -860,7 +946,7 @@ export class Router {
|
|
|
860
946
|
if (!stackNode) return;
|
|
861
947
|
const seed = this.getAutoSeed(stackNode);
|
|
862
948
|
if (!seed) return;
|
|
863
|
-
const compiled = this.
|
|
949
|
+
const compiled = this.getCompiledRoute(seed.routeId);
|
|
864
950
|
const meta = this.routeById.get(seed.routeId);
|
|
865
951
|
const path = compiled?.path ?? meta?.path ?? seed.path;
|
|
866
952
|
const seedStackId = seed.stackId ?? stackNode.getId();
|
|
@@ -878,8 +964,9 @@ export class Router {
|
|
|
878
964
|
const nextHist = [...prevHist, item];
|
|
879
965
|
this.setState({
|
|
880
966
|
history: nextHist
|
|
881
|
-
});
|
|
882
|
-
|
|
967
|
+
}, seedStackId);
|
|
968
|
+
// setState with affectedStackId already emits to the stack's listeners
|
|
969
|
+
|
|
883
970
|
if (seedStackId !== stackId && rootStackId) {
|
|
884
971
|
this.activateContainerForRoute(seed.routeId, rootStackId);
|
|
885
972
|
}
|
|
@@ -903,6 +990,7 @@ export class Router {
|
|
|
903
990
|
query: topOfStack.query,
|
|
904
991
|
path: topOfStack.path
|
|
905
992
|
};
|
|
993
|
+
this.emit(this.activeRouteListeners);
|
|
906
994
|
return;
|
|
907
995
|
}
|
|
908
996
|
}
|
|
@@ -922,19 +1010,29 @@ export class Router {
|
|
|
922
1010
|
query: {},
|
|
923
1011
|
path: targetRoute.path
|
|
924
1012
|
};
|
|
1013
|
+
this.emit(this.activeRouteListeners);
|
|
925
1014
|
}
|
|
926
1015
|
}
|
|
1016
|
+
emitDepth = 0;
|
|
927
1017
|
emit(set) {
|
|
928
1018
|
if (!set) return;
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1019
|
+
if (this.emitDepth > 5) {
|
|
1020
|
+
if (this.debugEnabled) console.warn('[Router] emit recursion limit reached');
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
this.emitDepth++;
|
|
1024
|
+
try {
|
|
1025
|
+
for (const l of Array.from(set)) {
|
|
1026
|
+
try {
|
|
1027
|
+
l();
|
|
1028
|
+
} catch (e) {
|
|
1029
|
+
if (this.debugEnabled) {
|
|
1030
|
+
console.error('[Router] listener error', e);
|
|
1031
|
+
}
|
|
936
1032
|
}
|
|
937
1033
|
}
|
|
1034
|
+
} finally {
|
|
1035
|
+
this.emitDepth--;
|
|
938
1036
|
}
|
|
939
1037
|
}
|
|
940
1038
|
getTopOfStack(stackId) {
|
|
@@ -1004,6 +1102,7 @@ export class Router {
|
|
|
1004
1102
|
childNode: r.childNode
|
|
1005
1103
|
};
|
|
1006
1104
|
this.registry.push(compiled);
|
|
1105
|
+
this.compiledRouteById.set(compiled.routeId, compiled);
|
|
1007
1106
|
if (stackId) {
|
|
1008
1107
|
this.routeById.set(r.routeId, {
|
|
1009
1108
|
path: compiled.path,
|
|
@@ -1128,7 +1227,7 @@ export class Router {
|
|
|
1128
1227
|
if (this.root) {
|
|
1129
1228
|
const seed = this.getAutoSeed(this.root);
|
|
1130
1229
|
if (seed) {
|
|
1131
|
-
const compiled = this.
|
|
1230
|
+
const compiled = this.getCompiledRoute(seed.routeId);
|
|
1132
1231
|
const meta = this.routeById.get(seed.routeId);
|
|
1133
1232
|
const path = compiled?.path ?? meta?.path ?? seed.path;
|
|
1134
1233
|
const stackId = seed.stackId ?? this.root.getId();
|
|
@@ -1148,7 +1247,9 @@ export class Router {
|
|
|
1148
1247
|
return;
|
|
1149
1248
|
}
|
|
1150
1249
|
}
|
|
1151
|
-
addChildNodeSeedsToItems(routeId, items, finalRouteId) {
|
|
1250
|
+
addChildNodeSeedsToItems(routeId, items, finalRouteId, visited = new Set()) {
|
|
1251
|
+
if (visited.has(routeId) || visited.size > 50) return;
|
|
1252
|
+
visited.add(routeId);
|
|
1152
1253
|
this.log('addChildNodeSeeds: called', {
|
|
1153
1254
|
routeId,
|
|
1154
1255
|
finalRouteId,
|
|
@@ -1159,7 +1260,7 @@ export class Router {
|
|
|
1159
1260
|
path: i.path
|
|
1160
1261
|
}))
|
|
1161
1262
|
});
|
|
1162
|
-
const compiled = this.
|
|
1263
|
+
const compiled = this.getCompiledRoute(routeId);
|
|
1163
1264
|
if (!compiled || !compiled.childNode) {
|
|
1164
1265
|
this.log('addChildNodeSeeds: no childNode', {
|
|
1165
1266
|
routeId
|
|
@@ -1185,7 +1286,7 @@ export class Router {
|
|
|
1185
1286
|
stackId: childSeed.stackId,
|
|
1186
1287
|
path: childSeed.path
|
|
1187
1288
|
});
|
|
1188
|
-
const childCompiled = this.
|
|
1289
|
+
const childCompiled = this.getCompiledRoute(childSeed.routeId);
|
|
1189
1290
|
const childMeta = this.routeById.get(childSeed.routeId);
|
|
1190
1291
|
const childPath = childCompiled?.path ?? childMeta?.path ?? childSeed.path;
|
|
1191
1292
|
const childStackId = childSeed.stackId ?? childNode.getId();
|
|
@@ -1233,16 +1334,18 @@ export class Router {
|
|
|
1233
1334
|
const insertIndex = parentIndex >= 0 ? parentIndex + 1 : items.length;
|
|
1234
1335
|
items.splice(insertIndex, 0, childItem);
|
|
1235
1336
|
if (childSeed.routeId) {
|
|
1236
|
-
this.addChildNodeSeedsToItems(childSeed.routeId, items, finalRouteId);
|
|
1337
|
+
this.addChildNodeSeedsToItems(childSeed.routeId, items, finalRouteId, visited);
|
|
1237
1338
|
}
|
|
1238
1339
|
}
|
|
1239
|
-
addChildNodeSeedsToHistory(routeId) {
|
|
1240
|
-
|
|
1340
|
+
addChildNodeSeedsToHistory(routeId, visited = new Set()) {
|
|
1341
|
+
if (visited.has(routeId) || visited.size > 50) return;
|
|
1342
|
+
visited.add(routeId);
|
|
1343
|
+
const compiled = this.getCompiledRoute(routeId);
|
|
1241
1344
|
if (!compiled || !compiled.childNode) return;
|
|
1242
1345
|
const childNode = compiled.childNode;
|
|
1243
1346
|
const childSeed = this.getAutoSeed(childNode);
|
|
1244
1347
|
if (!childSeed) return;
|
|
1245
|
-
const childCompiled = this.
|
|
1348
|
+
const childCompiled = this.getCompiledRoute(childSeed.routeId);
|
|
1246
1349
|
const childMeta = this.routeById.get(childSeed.routeId);
|
|
1247
1350
|
const childPath = childCompiled?.path ?? childMeta?.path ?? childSeed.path;
|
|
1248
1351
|
const childStackId = childSeed.stackId ?? childNode.getId();
|
|
@@ -1258,10 +1361,14 @@ export class Router {
|
|
|
1258
1361
|
};
|
|
1259
1362
|
this.applyHistoryChange('push', childItem);
|
|
1260
1363
|
if (childSeed.routeId) {
|
|
1261
|
-
this.addChildNodeSeedsToHistory(childSeed.routeId);
|
|
1364
|
+
this.addChildNodeSeedsToHistory(childSeed.routeId, visited);
|
|
1262
1365
|
}
|
|
1263
1366
|
}
|
|
1264
1367
|
matchBaseRoute(pathname, query) {
|
|
1368
|
+
// One-slot cache: uses reference equality for query object
|
|
1369
|
+
if (this.lastMatchedPathname !== null && pathname === this.lastMatchedPathname && query === this.lastMatchedQuery) {
|
|
1370
|
+
return this.lastMatchedResult ?? undefined;
|
|
1371
|
+
}
|
|
1265
1372
|
this.log('matchBaseRoute', {
|
|
1266
1373
|
pathname,
|
|
1267
1374
|
query
|
|
@@ -1405,17 +1512,26 @@ export class Router {
|
|
|
1405
1512
|
} else {
|
|
1406
1513
|
this.log('matchBaseRoute no match');
|
|
1407
1514
|
}
|
|
1408
|
-
|
|
1515
|
+
const result = best?.route;
|
|
1516
|
+
this.lastMatchedPathname = pathname;
|
|
1517
|
+
this.lastMatchedQuery = query;
|
|
1518
|
+
this.lastMatchedResult = result ?? null;
|
|
1519
|
+
return result;
|
|
1409
1520
|
}
|
|
1410
1521
|
generateKey() {
|
|
1411
1522
|
return `route-${nanoid()}`;
|
|
1412
1523
|
}
|
|
1413
1524
|
parsePath(path) {
|
|
1525
|
+
if (path === this.lastParsedPath && this.lastParsedResult) {
|
|
1526
|
+
return this.lastParsedResult;
|
|
1527
|
+
}
|
|
1414
1528
|
const parsed = qs.parseUrl(path);
|
|
1415
1529
|
const result = {
|
|
1416
1530
|
pathname: parsed.url,
|
|
1417
1531
|
query: parsed.query
|
|
1418
1532
|
};
|
|
1533
|
+
this.lastParsedPath = path;
|
|
1534
|
+
this.lastParsedResult = result;
|
|
1419
1535
|
this.log('parsePath', {
|
|
1420
1536
|
input: path,
|
|
1421
1537
|
output: result
|
|
@@ -1428,9 +1544,9 @@ export class Router {
|
|
|
1428
1544
|
const routerDefaults = this.routerScreenOptions;
|
|
1429
1545
|
if (!routerDefaults && !stackDefaults && !routeOptions) return undefined;
|
|
1430
1546
|
const merged = {
|
|
1547
|
+
...(routerDefaults ?? {}),
|
|
1431
1548
|
...(stackDefaults ?? {}),
|
|
1432
|
-
...(routeOptions ?? {})
|
|
1433
|
-
...(routerDefaults ?? {})
|
|
1549
|
+
...(routeOptions ?? {})
|
|
1434
1550
|
};
|
|
1435
1551
|
if (merged.stackPresentation === 'modal' && merged.convertModalToSheetForAndroid && Platform.OS === 'android') {
|
|
1436
1552
|
merged.stackPresentation = 'sheet';
|
|
@@ -1440,16 +1556,20 @@ export class Router {
|
|
|
1440
1556
|
findStackById(stackId) {
|
|
1441
1557
|
return this.stackById.get(stackId);
|
|
1442
1558
|
}
|
|
1559
|
+
getCompiledRoute(routeId) {
|
|
1560
|
+
return this.compiledRouteById.get(routeId);
|
|
1561
|
+
}
|
|
1443
1562
|
areShallowEqual(a, b) {
|
|
1444
1563
|
if (a === b) return true;
|
|
1445
|
-
|
|
1446
|
-
const
|
|
1447
|
-
const
|
|
1564
|
+
const na = a ?? {};
|
|
1565
|
+
const nb = b ?? {};
|
|
1566
|
+
const aKeys = Object.keys(na);
|
|
1567
|
+
const bKeys = Object.keys(nb);
|
|
1448
1568
|
if (aKeys.length !== bKeys.length) return false;
|
|
1449
1569
|
for (const k of aKeys) {
|
|
1450
|
-
if (!(k in
|
|
1451
|
-
const av =
|
|
1452
|
-
const bv =
|
|
1570
|
+
if (!(k in nb)) return false;
|
|
1571
|
+
const av = na[k];
|
|
1572
|
+
const bv = nb[k];
|
|
1453
1573
|
if (av === bv) continue;
|
|
1454
1574
|
const aArr = Array.isArray(av);
|
|
1455
1575
|
const bArr = Array.isArray(bv);
|
|
@@ -1560,7 +1680,9 @@ export class Router {
|
|
|
1560
1680
|
if (changed) {
|
|
1561
1681
|
try {
|
|
1562
1682
|
g.history?.replaceState(next, '', g.location?.href);
|
|
1563
|
-
} catch {
|
|
1683
|
+
} catch (e) {
|
|
1684
|
+
if (this.debugEnabled) console.warn('[Router] ensureHistoryIndex replaceState failed', e);
|
|
1685
|
+
}
|
|
1564
1686
|
}
|
|
1565
1687
|
}
|
|
1566
1688
|
getRouterPathFromHistory() {
|
|
@@ -1628,6 +1750,10 @@ export class Router {
|
|
|
1628
1750
|
}
|
|
1629
1751
|
replaceUrlSilently(to) {
|
|
1630
1752
|
if (!this.isWebEnv()) return;
|
|
1753
|
+
if (this.suppressHistorySyncCount > 10) {
|
|
1754
|
+
console.warn('[Router] suppressHistorySyncCount reset from', this.suppressHistorySyncCount);
|
|
1755
|
+
this.suppressHistorySyncCount = 0;
|
|
1756
|
+
}
|
|
1631
1757
|
this.suppressHistorySyncCount += 1;
|
|
1632
1758
|
this.replaceUrl(to, {
|
|
1633
1759
|
syncWithUrl: true
|
|
@@ -1672,6 +1798,12 @@ export class Router {
|
|
|
1672
1798
|
g[key] = true;
|
|
1673
1799
|
}
|
|
1674
1800
|
setupBrowserHistory() {
|
|
1801
|
+
// Abort previous listeners in case setupBrowserHistory is called twice
|
|
1802
|
+
this.abortController?.abort();
|
|
1803
|
+
this.abortController = new AbortController();
|
|
1804
|
+
const {
|
|
1805
|
+
signal
|
|
1806
|
+
} = this.abortController;
|
|
1675
1807
|
const g = globalThis;
|
|
1676
1808
|
const gAny = g;
|
|
1677
1809
|
const activeKey = Symbol.for('sigmela_router_active_instance');
|
|
@@ -1747,13 +1879,28 @@ export class Router {
|
|
|
1747
1879
|
this.lastBrowserIndex = idx;
|
|
1748
1880
|
}
|
|
1749
1881
|
};
|
|
1750
|
-
g.addEventListener?.('pushState', onHistory
|
|
1751
|
-
|
|
1752
|
-
|
|
1882
|
+
g.addEventListener?.('pushState', onHistory, {
|
|
1883
|
+
signal
|
|
1884
|
+
});
|
|
1885
|
+
g.addEventListener?.('replaceState', onHistory, {
|
|
1886
|
+
signal
|
|
1887
|
+
});
|
|
1888
|
+
g.addEventListener?.('popstate', onHistory, {
|
|
1889
|
+
signal
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
destroyed = false;
|
|
1893
|
+
destroy() {
|
|
1894
|
+
this.destroyed = true;
|
|
1895
|
+
this.abortController?.abort();
|
|
1896
|
+
this.listeners.clear();
|
|
1897
|
+
this.stackListeners.clear();
|
|
1898
|
+
this.activeRouteListeners.clear();
|
|
1899
|
+
this.rootListeners.clear();
|
|
1753
1900
|
}
|
|
1754
1901
|
syncUrlAfterInternalPop(popped) {
|
|
1755
1902
|
if (!this.isWebEnv()) return;
|
|
1756
|
-
const compiled = this.
|
|
1903
|
+
const compiled = this.getCompiledRoute(popped.routeId);
|
|
1757
1904
|
const isQueryRoute = compiled && compiled.queryPattern && Object.keys(compiled.queryPattern).length > 0;
|
|
1758
1905
|
if (isQueryRoute) {
|
|
1759
1906
|
const currentUrl = this.getCurrentUrl();
|
|
@@ -1803,6 +1950,7 @@ export class Router {
|
|
|
1803
1950
|
const stackHistory = this.getStackHistory(sid);
|
|
1804
1951
|
if (stackHistory.length > 0) {
|
|
1805
1952
|
const top = stackHistory[stackHistory.length - 1];
|
|
1953
|
+
if (!top) return null;
|
|
1806
1954
|
const isModalOrSheet = top.options?.stackPresentation === 'modal' || top.options?.stackPresentation === 'sheet';
|
|
1807
1955
|
if (stackHistory.length > 1 || isModalOrSheet) {
|
|
1808
1956
|
if (top.options?.stackPresentation === 'sheet') {
|
|
@@ -1845,6 +1993,7 @@ export class Router {
|
|
|
1845
1993
|
}
|
|
1846
1994
|
}
|
|
1847
1995
|
const top = stackHistory[stackHistory.length - 1];
|
|
1996
|
+
if (!top) return null;
|
|
1848
1997
|
this.applyHistoryChange('pop', top);
|
|
1849
1998
|
return top;
|
|
1850
1999
|
}
|
|
@@ -1865,6 +2014,8 @@ export class Router {
|
|
|
1865
2014
|
if (!deepest || isDeepestOverlay && !baseRoute) {
|
|
1866
2015
|
if (__DEV__) {
|
|
1867
2016
|
console.warn(`[Router] parse: no base route found for "${pathname}", seeding initial history`);
|
|
2017
|
+
} else {
|
|
2018
|
+
console.warn(`[Router] No base route found for "${pathname}"`);
|
|
1868
2019
|
}
|
|
1869
2020
|
this.seedInitialHistory();
|
|
1870
2021
|
this.recomputeActiveRoute();
|
|
@@ -1941,14 +2092,14 @@ export class Router {
|
|
|
1941
2092
|
}
|
|
1942
2093
|
if (items.length > 0) {
|
|
1943
2094
|
const lastItem = items[items.length - 1];
|
|
1944
|
-
const lastItemCompiled = lastItem ? this.
|
|
2095
|
+
const lastItemCompiled = lastItem ? this.getCompiledRoute(lastItem.routeId) : undefined;
|
|
1945
2096
|
const isLastItemOverlay = lastItem && lastItemCompiled && lastItemCompiled.queryPattern && Object.keys(lastItemCompiled.queryPattern).length > 0;
|
|
1946
2097
|
const finalRouteId = isLastItemOverlay && items.length > 1 ? items[items.length - 2]?.routeId : lastItem?.routeId;
|
|
1947
2098
|
const searchStartIndex = isLastItemOverlay ? items.length - 2 : items.length - 1;
|
|
1948
2099
|
for (let i = searchStartIndex; i >= 0; i--) {
|
|
1949
2100
|
const item = items[i];
|
|
1950
2101
|
if (item && item.routeId) {
|
|
1951
|
-
const compiled = this.
|
|
2102
|
+
const compiled = this.getCompiledRoute(item.routeId);
|
|
1952
2103
|
if (compiled && compiled.childNode) {
|
|
1953
2104
|
this.addChildNodeSeedsToItems(item.routeId, items, finalRouteId);
|
|
1954
2105
|
// Don't break: we may have multiple nested containers (e.g. TabBar -> SplitView).
|
|
@@ -1967,6 +2118,8 @@ export class Router {
|
|
|
1967
2118
|
if (!items.length) {
|
|
1968
2119
|
if (__DEV__) {
|
|
1969
2120
|
console.warn('[Router] parse: no items built for URL, seeding initial history');
|
|
2121
|
+
} else {
|
|
2122
|
+
console.warn('[Router] No items built for URL, seeding initial history');
|
|
1970
2123
|
}
|
|
1971
2124
|
this.seedInitialHistory();
|
|
1972
2125
|
this.recomputeActiveRoute();
|
|
@@ -1980,7 +2133,8 @@ export class Router {
|
|
|
1980
2133
|
this.setState({
|
|
1981
2134
|
history: items
|
|
1982
2135
|
});
|
|
1983
|
-
|
|
2136
|
+
// setState -> updateStackHistories already emits to changed stacks
|
|
2137
|
+
|
|
1984
2138
|
this.recomputeActiveRoute();
|
|
1985
2139
|
this.emit(this.listeners);
|
|
1986
2140
|
}
|
|
@@ -12,7 +12,7 @@ export const useRouter = () => {
|
|
|
12
12
|
};
|
|
13
13
|
export const useCurrentRoute = () => {
|
|
14
14
|
const router = useRouter();
|
|
15
|
-
const subscribe = React.useCallback(cb => router.
|
|
15
|
+
const subscribe = React.useCallback(cb => router.subscribeActiveRoute(cb), [router]);
|
|
16
16
|
const get = React.useCallback(() => router.getActiveRoute(), [router]);
|
|
17
17
|
return React.useSyncExternalStore(subscribe, get, get);
|
|
18
18
|
};
|