@buoy-gg/route-events 7.0.41 → 7.0.43
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 +11 -64
- package/lib/commonjs/RouteObserver.js +1 -54
- package/lib/commonjs/RouteParser.js +1 -314
- package/lib/commonjs/RouteTracker.js +1 -112
- package/lib/commonjs/RouteTracker.web.js +1 -12
- package/lib/commonjs/browserRouteRegistry.js +1 -30
- package/lib/commonjs/browserRoutes.js +1 -96
- package/lib/commonjs/components/NavigationStack.js +2 -572
- package/lib/commonjs/components/ReactNavigationRoutes.js +1 -836
- package/lib/commonjs/components/ReactNavigationRoutes.web.js +1 -12
- package/lib/commonjs/components/RouteEventDetail.js +1 -97
- package/lib/commonjs/components/RouteEventExpandedContent.js +1 -204
- package/lib/commonjs/components/RouteEventItemCompact.js +1 -191
- package/lib/commonjs/components/RouteEventsModalWithTabs.js +1 -734
- package/lib/commonjs/components/RouteEventsTimeline.js +1 -107
- package/lib/commonjs/components/RouteFilterViewV2.js +1 -42
- package/lib/commonjs/components/RoutesSitemap.js +2 -1035
- package/lib/commonjs/expoRouterStore.js +1 -121
- package/lib/commonjs/expoRouterStore.web.js +1 -44
- package/lib/commonjs/hooks/useRouteEvents.js +1 -57
- package/lib/commonjs/index.js +1 -168
- package/lib/commonjs/preset.js +1 -69
- package/lib/commonjs/stores/navigationStackStore.js +1 -45
- package/lib/commonjs/stores/routeEventStore.js +1 -136
- package/lib/commonjs/sync/routeEventsSyncAdapter.js +1 -275
- package/lib/commonjs/useNavigationStack.js +1 -408
- package/lib/commonjs/useNavigationStack.web.js +1 -32
- package/lib/commonjs/useRouteObserver.js +1 -73
- package/lib/commonjs/useRouteObserverReactNavigation.js +1 -109
- package/lib/commonjs/useRouteSitemap.js +1 -255
- package/lib/commonjs/utils/routeTemplate.js +1 -79
- package/lib/commonjs/utils/safeReactNavigation.js +1 -194
- package/lib/commonjs/utils/safeReactNavigation.web.js +1 -36
- package/lib/commonjs/utils/testIDs.js +1 -63
- package/lib/module/RouteObserver.js +1 -49
- package/lib/module/RouteParser.js +1 -309
- package/lib/module/RouteTracker.js +1 -109
- package/lib/module/RouteTracker.web.js +1 -8
- package/lib/module/browserRouteRegistry.js +1 -24
- package/lib/module/browserRoutes.js +1 -92
- package/lib/module/components/NavigationStack.js +2 -568
- package/lib/module/components/ReactNavigationRoutes.js +1 -832
- package/lib/module/components/ReactNavigationRoutes.web.js +1 -3
- package/lib/module/components/RouteEventDetail.js +1 -93
- package/lib/module/components/RouteEventExpandedContent.js +1 -200
- package/lib/module/components/RouteEventItemCompact.js +1 -187
- package/lib/module/components/RouteEventsModalWithTabs.js +1 -731
- package/lib/module/components/RouteEventsTimeline.js +1 -103
- package/lib/module/components/RouteFilterViewV2.js +1 -38
- package/lib/module/components/RoutesSitemap.js +2 -1033
- package/lib/module/expoRouterStore.js +1 -114
- package/lib/module/expoRouterStore.web.js +1 -37
- package/lib/module/hooks/useRouteEvents.js +1 -54
- package/lib/module/index.js +1 -70
- package/lib/module/preset.js +1 -66
- package/lib/module/stores/navigationStackStore.js +1 -41
- package/lib/module/stores/routeEventStore.js +1 -128
- package/lib/module/sync/routeEventsSyncAdapter.js +1 -273
- package/lib/module/useNavigationStack.js +1 -404
- package/lib/module/useNavigationStack.web.js +1 -28
- package/lib/module/useRouteObserver.js +1 -70
- package/lib/module/useRouteObserverReactNavigation.js +1 -106
- package/lib/module/useRouteSitemap.js +1 -250
- package/lib/module/utils/routeTemplate.js +1 -73
- package/lib/module/utils/safeReactNavigation.js +1 -185
- package/lib/module/utils/safeReactNavigation.web.js +1 -27
- package/lib/module/utils/testIDs.js +1 -55
- package/lib/web/index.mjs +4 -5
- package/package.json +6 -11
- package/lib/typescript/RouteObserver.d.ts.map +0 -1
- package/lib/typescript/RouteParser.d.ts.map +0 -1
- package/lib/typescript/RouteTracker.d.ts.map +0 -1
- package/lib/typescript/RouteTracker.web.d.ts.map +0 -1
- package/lib/typescript/browserRouteRegistry.d.ts.map +0 -1
- package/lib/typescript/browserRoutes.d.ts.map +0 -1
- package/lib/typescript/components/NavigationStack.d.ts.map +0 -1
- package/lib/typescript/components/ReactNavigationRoutes.d.ts.map +0 -1
- package/lib/typescript/components/ReactNavigationRoutes.web.d.ts.map +0 -1
- package/lib/typescript/components/RouteEventDetail.d.ts.map +0 -1
- package/lib/typescript/components/RouteEventExpandedContent.d.ts.map +0 -1
- package/lib/typescript/components/RouteEventItemCompact.d.ts.map +0 -1
- package/lib/typescript/components/RouteEventsModalWithTabs.d.ts.map +0 -1
- package/lib/typescript/components/RouteEventsTimeline.d.ts.map +0 -1
- package/lib/typescript/components/RouteFilterViewV2.d.ts.map +0 -1
- package/lib/typescript/components/RoutesSitemap.d.ts.map +0 -1
- package/lib/typescript/expoRouterStore.d.ts.map +0 -1
- package/lib/typescript/expoRouterStore.web.d.ts.map +0 -1
- package/lib/typescript/hooks/useRouteEvents.d.ts.map +0 -1
- package/lib/typescript/index.d.ts.map +0 -1
- package/lib/typescript/preset.d.ts.map +0 -1
- package/lib/typescript/stores/navigationStackStore.d.ts.map +0 -1
- package/lib/typescript/stores/routeEventStore.d.ts.map +0 -1
- package/lib/typescript/sync/routeEventsSyncAdapter.d.ts.map +0 -1
- package/lib/typescript/useNavigationStack.d.ts.map +0 -1
- package/lib/typescript/useNavigationStack.web.d.ts.map +0 -1
- package/lib/typescript/useRouteObserver.d.ts.map +0 -1
- package/lib/typescript/useRouteObserverReactNavigation.d.ts.map +0 -1
- package/lib/typescript/useRouteSitemap.d.ts.map +0 -1
- package/lib/typescript/utils/routeTemplate.d.ts.map +0 -1
- package/lib/typescript/utils/safeReactNavigation.d.ts.map +0 -1
- package/lib/typescript/utils/safeReactNavigation.web.d.ts.map +0 -1
- package/lib/typescript/utils/testIDs.d.ts.map +0 -1
|
@@ -1,273 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
import { getSafeRouter } from "@buoy-gg/shared-ui";
|
|
4
|
-
import { routeEventStore } from "../stores/routeEventStore";
|
|
5
|
-
import { navigationStackStore } from "../stores/navigationStackStore";
|
|
6
|
-
import { RouteParser } from "../RouteParser";
|
|
7
|
-
import { loadRouteNode, getRouteNodeMetadata } from "../expoRouterStore";
|
|
8
|
-
import { isOverWireBudget } from "@buoy-gg/shared-ui";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Navigation params larger than this are NOT streamed in the snapshot.
|
|
12
|
-
* Apps routinely pass whole user objects (or a stringified payload) through
|
|
13
|
-
* `navigate(..., { user })`; 500 of those on every route change exceed the
|
|
14
|
-
* 2MB emit budget and drop the Routes panel. Pathname/segments stay inline.
|
|
15
|
-
*/
|
|
16
|
-
export const SNAPSHOT_PARAM_INLINE_LIMIT = 16 * 1024;
|
|
17
|
-
const PARAMS_ON_DEVICE = Object.freeze({
|
|
18
|
-
__buoyOmitted: "route-params",
|
|
19
|
-
note: "Navigation params stay on the device."
|
|
20
|
-
});
|
|
21
|
-
function shouldOmitParams(params) {
|
|
22
|
-
if (params == null) return false;
|
|
23
|
-
return isOverWireBudget(params, SNAPSHOT_PARAM_INLINE_LIMIT);
|
|
24
|
-
}
|
|
25
|
-
function toWireParams(params) {
|
|
26
|
-
if (!shouldOmitParams(params)) return params;
|
|
27
|
-
return PARAMS_ON_DEVICE;
|
|
28
|
-
}
|
|
29
|
-
function toWireEvent(event) {
|
|
30
|
-
if (!shouldOmitParams(event.params)) return event;
|
|
31
|
-
return {
|
|
32
|
-
...event,
|
|
33
|
-
params: toWireParams(event.params)
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
function toWireStackItem(item) {
|
|
37
|
-
if (!shouldOmitParams(item.params)) return item;
|
|
38
|
-
return {
|
|
39
|
-
...item,
|
|
40
|
-
params: toWireParams(item.params)
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Serializable snapshot of the device's route tree, sent to the dashboard so
|
|
46
|
-
* the Routes tab can render a sitemap (the raw expo-router RouteNode itself is
|
|
47
|
-
* not JSON-serializable — it holds components/functions — so we send the parsed
|
|
48
|
-
* RouteInfo[] instead).
|
|
49
|
-
*/
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Payload shape for the route-events tool (adapter version 3). Consumers must
|
|
53
|
-
* tolerate older shapes: v1 sends a bare RouteChangeEvent[]; v2 omits `stack`.
|
|
54
|
-
*/
|
|
55
|
-
|
|
56
|
-
function getSitemapSnapshot() {
|
|
57
|
-
const metadata = getRouteNodeMetadata();
|
|
58
|
-
return {
|
|
59
|
-
routes: RouteParser.parseRouteTree(loadRouteNode()),
|
|
60
|
-
source: metadata.source,
|
|
61
|
-
lastUpdatedAt: metadata.lastLoadedAt
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Sync adapter for the route-events tool, consumed by @buoy-gg/external-sync's
|
|
67
|
-
* `useExternalSync` (structurally matches its ToolSyncAdapter interface so
|
|
68
|
-
* this package doesn't need a dependency on it).
|
|
69
|
-
*
|
|
70
|
-
* Subscribing attaches the routeObserver listener, so route changes are only
|
|
71
|
-
* recorded while a dashboard is watching. (Route events still require a
|
|
72
|
-
* <RouteTracker /> inside the navigation tree to be emitted at all.)
|
|
73
|
-
*
|
|
74
|
-
* The snapshot carries both the route-change events AND the parsed route
|
|
75
|
-
* sitemap, so the dashboard — which has no local expo-router store — can render
|
|
76
|
-
* the route tree. The `navigate` action lets the dashboard drive navigation on
|
|
77
|
-
* the device.
|
|
78
|
-
*/
|
|
79
|
-
export const routeEventsSyncAdapter = {
|
|
80
|
-
version: 3,
|
|
81
|
-
getSnapshot: () => ({
|
|
82
|
-
events: routeEventStore.getEvents().map(toWireEvent),
|
|
83
|
-
sitemap: getSitemapSnapshot(),
|
|
84
|
-
stack: navigationStackStore.getStack().map(toWireStackItem)
|
|
85
|
-
}),
|
|
86
|
-
subscribe: onChange => {
|
|
87
|
-
const unsubscribeEvents = routeEventStore.subscribeToEvents(onChange);
|
|
88
|
-
const unsubscribeStack = navigationStackStore.subscribe(onChange);
|
|
89
|
-
|
|
90
|
-
// The expo-router route tree can still be populating when a dashboard
|
|
91
|
-
// starts watching. Poll briefly until it's available and push a snapshot
|
|
92
|
-
// the moment it loads, so the Routes tab fills in without needing the user
|
|
93
|
-
// to navigate first. Stops as soon as the tree is found.
|
|
94
|
-
let pollTimer = null;
|
|
95
|
-
if (!loadRouteNode()) {
|
|
96
|
-
let retries = 0;
|
|
97
|
-
const maxRetries = 100; // ~10s
|
|
98
|
-
const poll = () => {
|
|
99
|
-
retries += 1;
|
|
100
|
-
if (loadRouteNode()) {
|
|
101
|
-
pollTimer = null;
|
|
102
|
-
onChange();
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
pollTimer = retries < maxRetries ? setTimeout(poll, 100) : null;
|
|
106
|
-
};
|
|
107
|
-
pollTimer = setTimeout(poll, 100);
|
|
108
|
-
}
|
|
109
|
-
return () => {
|
|
110
|
-
unsubscribeEvents();
|
|
111
|
-
unsubscribeStack();
|
|
112
|
-
if (pollTimer) clearTimeout(pollTimer);
|
|
113
|
-
};
|
|
114
|
-
},
|
|
115
|
-
actions: {
|
|
116
|
-
clearEvents: () => {
|
|
117
|
-
routeEventStore.clearEvents();
|
|
118
|
-
},
|
|
119
|
-
/**
|
|
120
|
-
* Just where the app is right now — no sitemap, no history.
|
|
121
|
-
*
|
|
122
|
-
* The full snapshot carries the parsed route tree, which is the wrong
|
|
123
|
-
* thing to hand a caller that polls (the macro recorder reads this several
|
|
124
|
-
* times a second to notice a screen change). Reading the last route event
|
|
125
|
-
* is also what makes this honest about apps with no <RouteTracker />: they
|
|
126
|
-
* get `{ path: null }` rather than a plausible-looking guess.
|
|
127
|
-
*/
|
|
128
|
-
getCurrentRoute: () => {
|
|
129
|
-
// BaseEventStore is newest-FIRST, and the store only captures while
|
|
130
|
-
// something is subscribed.
|
|
131
|
-
const latest = routeEventStore.getEvents()[0];
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* A RECORDED event only wins while the recorder is running.
|
|
135
|
-
*
|
|
136
|
-
* The store starts capturing when its first subscriber arrives and stops
|
|
137
|
-
* with the last one, so an app with Ask Buoy on the phone and no
|
|
138
|
-
* dashboard attached has NO route events at all — the stack fallback
|
|
139
|
-
* below is its normal path, not the fresh-launch edge case this code was
|
|
140
|
-
* written for. Worse, events left over from a closed capture window are
|
|
141
|
-
* stale the moment the user navigates again: a live QA session read
|
|
142
|
-
* `/(tabs)/menu` from an event six scenarios after the app had moved to
|
|
143
|
-
* `/cart`. The live stack cannot be stale, so it wins whenever nothing
|
|
144
|
-
* is recording.
|
|
145
|
-
*/
|
|
146
|
-
if (latest && routeEventStore.isCapturing()) {
|
|
147
|
-
return {
|
|
148
|
-
path: latest.pathname,
|
|
149
|
-
params: toWireParams(latest.params),
|
|
150
|
-
at: latest.timestamp,
|
|
151
|
-
source: "event"
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// The live stack knows even when the history is empty, which is the
|
|
156
|
-
// difference between a macro that can run from anywhere and one that
|
|
157
|
-
// only works if you happen to be on the screen it was recorded from.
|
|
158
|
-
const stack = navigationStackStore.getStack();
|
|
159
|
-
const focused = stack.find(item => item.isFocused) ?? stack[stack.length - 1];
|
|
160
|
-
if (focused?.pathname) {
|
|
161
|
-
return {
|
|
162
|
-
path: focused.pathname,
|
|
163
|
-
params: toWireParams(focused.params ?? {}),
|
|
164
|
-
at: null,
|
|
165
|
-
source: "stack"
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// No live stack (no <RouteTracker/>) and the recorder is off: an old
|
|
170
|
-
// event is all there is. Say that it may be stale rather than passing it
|
|
171
|
-
// off as where the app is.
|
|
172
|
-
if (latest) {
|
|
173
|
-
return {
|
|
174
|
-
path: latest.pathname,
|
|
175
|
-
params: toWireParams(latest.params),
|
|
176
|
-
at: latest.timestamp,
|
|
177
|
-
source: "event",
|
|
178
|
-
stale: true
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
return {
|
|
182
|
-
path: null,
|
|
183
|
-
params: {},
|
|
184
|
-
at: null,
|
|
185
|
-
source: null
|
|
186
|
-
};
|
|
187
|
-
},
|
|
188
|
-
/**
|
|
189
|
-
* Navigate the device to a concrete path. The dashboard resolves any
|
|
190
|
-
* dynamic params into a concrete path before invoking this.
|
|
191
|
-
*/
|
|
192
|
-
navigate: params => {
|
|
193
|
-
const p = params;
|
|
194
|
-
const path = p?.path;
|
|
195
|
-
if (!path) {
|
|
196
|
-
throw new Error("navigate requires a 'path' param");
|
|
197
|
-
}
|
|
198
|
-
const router = getSafeRouter();
|
|
199
|
-
if (!router) {
|
|
200
|
-
// Bare React Navigation: no paths, but the tracker captured the
|
|
201
|
-
// container ref, so navigate by SCREEN NAME — "/Settings" and
|
|
202
|
-
// "Settings" both resolve, and nested navigators are walked via the
|
|
203
|
-
// root state. This is half the market; "throw off expo-router" made
|
|
204
|
-
// navigate a dead verb for every RN CLI app.
|
|
205
|
-
const actions = navigationStackStore.getActions();
|
|
206
|
-
const name = path.replace(/^\//, "");
|
|
207
|
-
if (!actions?.navigateToRoute || !name) {
|
|
208
|
-
throw new Error("expo-router is not available here and no navigation container was captured — is <RouteTracker /> mounted inside your NavigationContainer?");
|
|
209
|
-
}
|
|
210
|
-
const ok = actions.navigateToRoute(name, p?.params);
|
|
211
|
-
if (!ok) {
|
|
212
|
-
throw new Error(`Could not navigate to "${name}" — no navigation container is live.`);
|
|
213
|
-
}
|
|
214
|
-
return {
|
|
215
|
-
navigated: path,
|
|
216
|
-
replaced: false,
|
|
217
|
-
via: "react-navigation"
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
// Push by default so navigate-then-back flows keep working. Callers that
|
|
221
|
-
// mean "leave this screen" (sitemap Go, agents resetting to home) pass
|
|
222
|
-
// replace: true — otherwise "Go to home" stacks another `/` on top.
|
|
223
|
-
const go = p?.replace === true && typeof router.replace === "function" ? router.replace : router.navigate;
|
|
224
|
-
go(path);
|
|
225
|
-
return {
|
|
226
|
-
navigated: path,
|
|
227
|
-
replaced: p?.replace === true
|
|
228
|
-
};
|
|
229
|
-
},
|
|
230
|
-
// ── Stack actions: delegate to the live navigation actions captured by
|
|
231
|
-
// <RouteTracker /> (they hold the React Navigation container ref). ──
|
|
232
|
-
stackNavigateToIndex: params => {
|
|
233
|
-
const index = params?.index;
|
|
234
|
-
if (typeof index !== "number") {
|
|
235
|
-
throw new Error("stackNavigateToIndex requires a numeric 'index'");
|
|
236
|
-
}
|
|
237
|
-
const actions = navigationStackStore.getActions();
|
|
238
|
-
if (!actions) throw new Error("navigation stack is not available");
|
|
239
|
-
actions.navigateToIndex(index);
|
|
240
|
-
return {
|
|
241
|
-
navigatedToIndex: index
|
|
242
|
-
};
|
|
243
|
-
},
|
|
244
|
-
stackPopToIndex: params => {
|
|
245
|
-
const index = params?.index;
|
|
246
|
-
if (typeof index !== "number") {
|
|
247
|
-
throw new Error("stackPopToIndex requires a numeric 'index'");
|
|
248
|
-
}
|
|
249
|
-
const actions = navigationStackStore.getActions();
|
|
250
|
-
if (!actions) throw new Error("navigation stack is not available");
|
|
251
|
-
actions.popToIndex(index);
|
|
252
|
-
return {
|
|
253
|
-
poppedToIndex: index
|
|
254
|
-
};
|
|
255
|
-
},
|
|
256
|
-
stackGoBack: () => {
|
|
257
|
-
const actions = navigationStackStore.getActions();
|
|
258
|
-
if (!actions) throw new Error("navigation stack is not available");
|
|
259
|
-
actions.goBack();
|
|
260
|
-
return {
|
|
261
|
-
wentBack: true
|
|
262
|
-
};
|
|
263
|
-
},
|
|
264
|
-
stackPopToTop: () => {
|
|
265
|
-
const actions = navigationStackStore.getActions();
|
|
266
|
-
if (!actions) throw new Error("navigation stack is not available");
|
|
267
|
-
actions.popToTop();
|
|
268
|
-
return {
|
|
269
|
-
poppedToTop: true
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
};
|
|
1
|
+
"use strict";import{getSafeRouter as d}from"@buoy-gg/shared-ui";import{routeEventStore as i}from"../stores/routeEventStore";import{navigationStackStore as o}from"../stores/navigationStackStore";import{RouteParser as f}from"../RouteParser";import{loadRouteNode as p,getRouteNodeMetadata as v}from"../expoRouterStore";import{isOverWireBudget as g}from"@buoy-gg/shared-ui";export const SNAPSHOT_PARAM_INLINE_LIMIT=16*1024;const T=Object.freeze({__buoyOmitted:"route-params",note:"Navigation params stay on the device."});function l(t){if(t==null)return false;return g(t,SNAPSHOT_PARAM_INLINE_LIMIT)}function s(t){if(!l(t))return t;return T}function h(t){if(!l(t.params))return t;return{...t,params:s(t.params)}}function k(t){if(!l(t.params))return t;return{...t,params:s(t.params)}}function E(){const t=v();return{routes:f.parseRouteTree(p()),source:t.source,lastUpdatedAt:t.lastLoadedAt}}export const routeEventsSyncAdapter={version:3,getSnapshot:()=>({events:i.getEvents().map(h),sitemap:E(),stack:o.getStack().map(k)}),subscribe:t=>{const e=i.subscribeToEvents(t);const a=o.subscribe(t);let r=null;if(!p()){let c=0;const u=100;const n=()=>{c+=1;if(p()){r=null;t();return}r=c<u?setTimeout(n,100):null};r=setTimeout(n,100)}return()=>{e();a();if(r)clearTimeout(r)}},actions:{clearEvents:()=>{i.clearEvents()},getCurrentRoute:()=>{const t=i.getEvents()[0];if(t&&i.isCapturing()){return{path:t.pathname,params:s(t.params),at:t.timestamp,source:"event"}}const e=o.getStack();const a=e.find(r=>r.isFocused)??e[e.length-1];if(a?.pathname){return{path:a.pathname,params:s(a.params??{}),at:null,source:"stack"}}if(t){return{path:t.pathname,params:s(t.params),at:t.timestamp,source:"event",stale:true}}return{path:null,params:{},at:null,source:null}},navigate:t=>{const e=t;const a=e?.path;if(!a){throw new Error("navigate requires a 'path' param")}const r=d();if(!r){const u=o.getActions();const n=a.replace(/^\//,"");if(!u?.navigateToRoute||!n){throw new Error("expo-router is not available here and no navigation container was captured \u2014 is <RouteTracker /> mounted inside your NavigationContainer?")}const m=u.navigateToRoute(n,e?.params);if(!m){throw new Error(`Could not navigate to "${n}" \u2014 no navigation container is live.`)}return{navigated:a,replaced:false,via:"react-navigation"}}const c=e?.replace===true&&typeof r.replace==="function"?r.replace:r.navigate;c(a);return{navigated:a,replaced:e?.replace===true}},stackNavigateToIndex:t=>{const e=t?.index;if(typeof e!=="number"){throw new Error("stackNavigateToIndex requires a numeric 'index'")}const a=o.getActions();if(!a)throw new Error("navigation stack is not available");a.navigateToIndex(e);return{navigatedToIndex:e}},stackPopToIndex:t=>{const e=t?.index;if(typeof e!=="number"){throw new Error("stackPopToIndex requires a numeric 'index'")}const a=o.getActions();if(!a)throw new Error("navigation stack is not available");a.popToIndex(e);return{poppedToIndex:e}},stackGoBack:()=>{const t=o.getActions();if(!t)throw new Error("navigation stack is not available");t.goBack();return{wentBack:true}},stackPopToTop:()=>{const t=o.getActions();if(!t)throw new Error("navigation stack is not available");t.popToTop();return{poppedToTop:true}}}};
|
|
@@ -1,404 +1 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* useNavigationStack - Hook to access current navigation stack state
|
|
5
|
-
*
|
|
6
|
-
* Provides real-time access to the navigation stack,
|
|
7
|
-
* showing what screens are currently mounted in memory and which is visible.
|
|
8
|
-
*
|
|
9
|
-
* Supports both Expo Router and React Navigation (RN CLI).
|
|
10
|
-
*
|
|
11
|
-
* - Expo Router: uses useNavigation/useNavigationState (inside navigator context)
|
|
12
|
-
* - RN CLI: uses NavigationContainerRefContext (works as sibling of navigators)
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { useState, useEffect, useMemo, useCallback } from "react";
|
|
16
|
-
import { getCommonActions, getStackActions, useSafeNavigationContainerRef } from "./utils/safeReactNavigation";
|
|
17
|
-
import { getSafeRouter, isExpoRouterAvailable } from "@buoy-gg/shared-ui";
|
|
18
|
-
|
|
19
|
-
// Resolved once at module load; null when React Navigation is absent
|
|
20
|
-
// (useSafeNavigationContainerRef then yields undefined, so the dispatch
|
|
21
|
-
// paths below never execute).
|
|
22
|
-
const CommonActions = getCommonActions();
|
|
23
|
-
const StackActions = getStackActions();
|
|
24
|
-
|
|
25
|
-
// ============================================================================
|
|
26
|
-
// Type Definitions
|
|
27
|
-
// ============================================================================
|
|
28
|
-
|
|
29
|
-
// ============================================================================
|
|
30
|
-
// Nested Navigation Helpers
|
|
31
|
-
// ============================================================================
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Find the path from root navigation state to a screen by name.
|
|
35
|
-
* Returns array of route names from root to the target, or null if not found.
|
|
36
|
-
*/
|
|
37
|
-
function findScreenPath(state, screenName) {
|
|
38
|
-
if (!state || !state.routes) return null;
|
|
39
|
-
for (const route of state.routes) {
|
|
40
|
-
if (route.name === screenName) {
|
|
41
|
-
return [route.name];
|
|
42
|
-
}
|
|
43
|
-
if (route.state) {
|
|
44
|
-
const nested = findScreenPath(route.state, screenName);
|
|
45
|
-
if (nested) {
|
|
46
|
-
return [route.name, ...nested];
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Build a nested CommonActions.navigate() from a path of screen names.
|
|
55
|
-
* e.g. ["MainApp", "Settings", "SettingScreen"] becomes:
|
|
56
|
-
* navigate("MainApp", { screen: "Settings", params: { screen: "SettingScreen" } })
|
|
57
|
-
*/
|
|
58
|
-
function buildNestedNavigateAction(path) {
|
|
59
|
-
if (path.length === 0) return null;
|
|
60
|
-
if (path.length === 1) {
|
|
61
|
-
return CommonActions.navigate({
|
|
62
|
-
name: path[0]
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
let params = {
|
|
66
|
-
screen: path[path.length - 1]
|
|
67
|
-
};
|
|
68
|
-
for (let i = path.length - 2; i >= 1; i--) {
|
|
69
|
-
params = {
|
|
70
|
-
screen: path[i],
|
|
71
|
-
params
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
return CommonActions.navigate({
|
|
75
|
-
name: path[0],
|
|
76
|
-
params
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ============================================================================
|
|
81
|
-
// Main Hook
|
|
82
|
-
// ============================================================================
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Recursively collect the routes from the navigation state tree.
|
|
86
|
-
*
|
|
87
|
-
* FOCUS COMES FROM `state.index`, NEVER FROM POSITION. React Navigation names
|
|
88
|
-
* the focused route by index within `routes`; the array's order is declaration
|
|
89
|
-
* order. `buildStack` used to equate the two — "the last route in the collected
|
|
90
|
-
* list is the focused one" — and a hidden tab declared last (`href: null` on an
|
|
91
|
-
* expo-router `Tabs.Screen`) then became the reported current screen for the
|
|
92
|
-
* whole session. Measured on a real app: Home and Rewards both reported as a
|
|
93
|
-
* hidden Menu tab, in every turn of a 55-scenario QA run, because `menu` was
|
|
94
|
-
* declared last.
|
|
95
|
-
*
|
|
96
|
-
* TAB AND DRAWER SIBLINGS ARE NOT A STACK. They are alternative destinations
|
|
97
|
-
* that happen to be mounted at the same time; `canPop` and `popToIndex` never
|
|
98
|
-
* meant anything for them. Only the focused branch of a tab/drawer is
|
|
99
|
-
* collected, so what comes back is the stack you could actually pop.
|
|
100
|
-
*/
|
|
101
|
-
function collectRoutesFromState(state, depth = 0, parentPath = "", onFocusedChain = true) {
|
|
102
|
-
if (!state || !Array.isArray(state.routes) || state.routes.length === 0) {
|
|
103
|
-
return [];
|
|
104
|
-
}
|
|
105
|
-
const routes = [];
|
|
106
|
-
|
|
107
|
-
// An absent or out-of-range `index` is incomplete state (mid-rehydration, a
|
|
108
|
-
// partially built child). Fall back to the last route — a stack's top — and
|
|
109
|
-
// never to an arbitrary sibling.
|
|
110
|
-
const declaredIndex = typeof state.index === "number" ? state.index : NaN;
|
|
111
|
-
const focusedIndex = Number.isInteger(declaredIndex) && declaredIndex >= 0 && declaredIndex < state.routes.length ? declaredIndex : state.routes.length - 1;
|
|
112
|
-
const navigatorType = typeof state.type === "string" ? state.type : undefined;
|
|
113
|
-
const siblingsAreStack = navigatorType !== "tab" && navigatorType !== "drawer";
|
|
114
|
-
for (let i = 0; i < state.routes.length; i++) {
|
|
115
|
-
if (!siblingsAreStack && i !== focusedIndex) continue;
|
|
116
|
-
const route = state.routes[i];
|
|
117
|
-
const isFocused = i === focusedIndex;
|
|
118
|
-
|
|
119
|
-
// Build the path
|
|
120
|
-
let pathname = route.name;
|
|
121
|
-
if (route.name === "index") {
|
|
122
|
-
pathname = "/";
|
|
123
|
-
} else if (!pathname.startsWith("/")) {
|
|
124
|
-
pathname = `/${pathname}`;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Add parent path
|
|
128
|
-
if (parentPath && parentPath !== "/") {
|
|
129
|
-
pathname = `${parentPath}${pathname}`;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Add params to pathname for dynamic routes
|
|
133
|
-
if (route.params) {
|
|
134
|
-
const paramEntries = Object.entries(route.params);
|
|
135
|
-
if (paramEntries.length > 0) {
|
|
136
|
-
pathname = pathname.replace(/\[([^\]]+)\]/g, (match, param) => {
|
|
137
|
-
return route.params[param] || match;
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Descend the focused branch first: whatever it yields owns the focus, and
|
|
143
|
-
// this route only keeps it when nothing deeper claimed it.
|
|
144
|
-
const onChain = onFocusedChain && isFocused;
|
|
145
|
-
const nestedRoutes = onChain && route.state ? collectRoutesFromState(route.state, depth + 1, pathname, true) : [];
|
|
146
|
-
routes.push({
|
|
147
|
-
key: route.key,
|
|
148
|
-
name: route.name,
|
|
149
|
-
path: pathname,
|
|
150
|
-
params: route.params || {},
|
|
151
|
-
state: route.state,
|
|
152
|
-
isFocused: onChain && nestedRoutes.length === 0
|
|
153
|
-
});
|
|
154
|
-
routes.push(...nestedRoutes);
|
|
155
|
-
}
|
|
156
|
-
return routes;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Transform raw navigation state into display items.
|
|
161
|
-
*
|
|
162
|
-
* Exported for tests: the focus rule below is the one thing every route answer
|
|
163
|
-
* in the product rests on, and it was previously only reachable by extracting
|
|
164
|
-
* the function from source with the TypeScript AST.
|
|
165
|
-
*/
|
|
166
|
-
export function buildStack(navigationState, isExpo) {
|
|
167
|
-
if (!navigationState) return [];
|
|
168
|
-
try {
|
|
169
|
-
const routes = collectRoutesFromState(navigationState);
|
|
170
|
-
|
|
171
|
-
// Filter out internal Expo Router routes (layouts, __root, etc.)
|
|
172
|
-
// These only exist in Expo Router; on RN CLI all routes are user-defined
|
|
173
|
-
const filteredRoutes = isExpo ? routes.filter(route => {
|
|
174
|
-
if (route.name.startsWith("__")) return false;
|
|
175
|
-
if (route.name.includes("_layout")) return false;
|
|
176
|
-
if (route.name.startsWith("+not-found")) return false;
|
|
177
|
-
return true;
|
|
178
|
-
}) : routes;
|
|
179
|
-
|
|
180
|
-
// If we filtered everything out, just keep the last route
|
|
181
|
-
const finalRoutes = filteredRoutes.length > 0 ? filteredRoutes : routes.slice(-1);
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Focus survives filtering. It is decided in `collectRoutesFromState` from
|
|
185
|
-
* `state.index`; the only case that falls back to the last item is one
|
|
186
|
-
* where the filter dropped the focused route itself (an internal expo
|
|
187
|
-
* layout), which is the one situation the old rule was ever right for.
|
|
188
|
-
*/
|
|
189
|
-
const collectedFocus = finalRoutes.findIndex(route => route.isFocused);
|
|
190
|
-
const focusIndex = collectedFocus >= 0 ? collectedFocus : finalRoutes.length - 1;
|
|
191
|
-
return finalRoutes.map((route, index) => {
|
|
192
|
-
let cleanPath = route.path || `/${route.name}`;
|
|
193
|
-
// Clean up Expo Router internal /__root prefix
|
|
194
|
-
if (isExpo) {
|
|
195
|
-
cleanPath = cleanPath.replace(/^\/__root/, "");
|
|
196
|
-
}
|
|
197
|
-
// Ensure we always have at least a /
|
|
198
|
-
if (!cleanPath || cleanPath === "") {
|
|
199
|
-
cleanPath = "/";
|
|
200
|
-
}
|
|
201
|
-
return {
|
|
202
|
-
key: route.key,
|
|
203
|
-
name: route.name,
|
|
204
|
-
pathname: cleanPath,
|
|
205
|
-
params: route.params || {},
|
|
206
|
-
isFocused: index === focusIndex,
|
|
207
|
-
index,
|
|
208
|
-
canPop: index > 0
|
|
209
|
-
};
|
|
210
|
-
});
|
|
211
|
-
} catch (err) {
|
|
212
|
-
console.error("Error transforming navigation state:", err);
|
|
213
|
-
return [];
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Access the current navigation stack.
|
|
219
|
-
*
|
|
220
|
-
* Works in both Expo Router and RN CLI apps. On RN CLI, uses the
|
|
221
|
-
* NavigationContainer ref context so it works even as a sibling of navigators.
|
|
222
|
-
*
|
|
223
|
-
* @example
|
|
224
|
-
* ```tsx
|
|
225
|
-
* const { stack, focusedRoute, goBack, popToTop } = useNavigationStack();
|
|
226
|
-
*
|
|
227
|
-
* // Display stack
|
|
228
|
-
* stack.map(item => (
|
|
229
|
-
* <View key={item.key}>
|
|
230
|
-
* <Text>{item.pathname}</Text>
|
|
231
|
-
* {item.isFocused && <Text>VISIBLE</Text>}
|
|
232
|
-
* </View>
|
|
233
|
-
* ));
|
|
234
|
-
* ```
|
|
235
|
-
*/
|
|
236
|
-
export function useNavigationStack() {
|
|
237
|
-
const isExpo = isExpoRouterAvailable();
|
|
238
|
-
const containerRef = useSafeNavigationContainerRef();
|
|
239
|
-
const [navigationState, setNavigationState] = useState(() => containerRef?.getRootState?.() ?? null);
|
|
240
|
-
const [isLoaded, setIsLoaded] = useState(false);
|
|
241
|
-
const [error, setError] = useState(null);
|
|
242
|
-
|
|
243
|
-
// Subscribe to navigation state changes via the container ref
|
|
244
|
-
useEffect(() => {
|
|
245
|
-
if (!containerRef) return;
|
|
246
|
-
|
|
247
|
-
// Get initial state
|
|
248
|
-
const initialState = containerRef.getRootState?.();
|
|
249
|
-
if (initialState) {
|
|
250
|
-
setNavigationState(initialState);
|
|
251
|
-
}
|
|
252
|
-
setIsLoaded(true);
|
|
253
|
-
|
|
254
|
-
// Listen for state changes
|
|
255
|
-
const unsubscribe = containerRef.addListener?.("state", () => {
|
|
256
|
-
const state = containerRef.getRootState?.();
|
|
257
|
-
if (state) {
|
|
258
|
-
setNavigationState(state);
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
return () => {
|
|
262
|
-
unsubscribe?.();
|
|
263
|
-
};
|
|
264
|
-
}, [containerRef]);
|
|
265
|
-
|
|
266
|
-
// Transform navigation state into display items
|
|
267
|
-
const stack = useMemo(() => buildStack(navigationState, isExpo), [navigationState, isExpo]);
|
|
268
|
-
|
|
269
|
-
// Get focused route
|
|
270
|
-
const focusedRoute = useMemo(() => {
|
|
271
|
-
return stack.find(item => item.isFocused) || null;
|
|
272
|
-
}, [stack]);
|
|
273
|
-
|
|
274
|
-
// Helper properties
|
|
275
|
-
const stackDepth = stack.length;
|
|
276
|
-
const isAtRoot = stackDepth <= 1;
|
|
277
|
-
|
|
278
|
-
// Manual refresh
|
|
279
|
-
const refresh = useCallback(() => {
|
|
280
|
-
if (!containerRef) return;
|
|
281
|
-
const state = containerRef.getRootState?.();
|
|
282
|
-
if (state) {
|
|
283
|
-
setNavigationState(state);
|
|
284
|
-
}
|
|
285
|
-
}, [containerRef]);
|
|
286
|
-
|
|
287
|
-
// Navigation actions - use Expo Router when available, fall back to React Navigation
|
|
288
|
-
const navigateToIndex = useCallback(index => {
|
|
289
|
-
if (index >= stack.length || index < 0) return;
|
|
290
|
-
const targetRoute = stack[index];
|
|
291
|
-
if (!targetRoute) return;
|
|
292
|
-
try {
|
|
293
|
-
const expoRouter = getSafeRouter();
|
|
294
|
-
if (expoRouter) {
|
|
295
|
-
expoRouter.navigate(targetRoute.pathname);
|
|
296
|
-
} else if (containerRef) {
|
|
297
|
-
// Walk the state tree to build nested screen/params for deep navigation
|
|
298
|
-
const rootState = containerRef.getRootState?.();
|
|
299
|
-
if (rootState) {
|
|
300
|
-
const path = findScreenPath(rootState, targetRoute.name);
|
|
301
|
-
if (path) {
|
|
302
|
-
const action = buildNestedNavigateAction(path);
|
|
303
|
-
if (action) {
|
|
304
|
-
containerRef.dispatch(action);
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
// Fallback: try direct navigation
|
|
310
|
-
containerRef.dispatch(CommonActions.navigate({
|
|
311
|
-
name: targetRoute.name,
|
|
312
|
-
params: targetRoute.params
|
|
313
|
-
}));
|
|
314
|
-
}
|
|
315
|
-
} catch (err) {
|
|
316
|
-
console.error("Failed to navigate:", err);
|
|
317
|
-
}
|
|
318
|
-
}, [stack, containerRef]);
|
|
319
|
-
const navigateToRoute = useCallback((name, params) => {
|
|
320
|
-
if (!containerRef) return false;
|
|
321
|
-
try {
|
|
322
|
-
// Deep path first, so screens inside nested navigators resolve even
|
|
323
|
-
// when their navigator isn't mounted yet.
|
|
324
|
-
const rootState = containerRef.getRootState?.();
|
|
325
|
-
if (!params && rootState) {
|
|
326
|
-
const path = findScreenPath(rootState, name);
|
|
327
|
-
if (path) {
|
|
328
|
-
const action = buildNestedNavigateAction(path);
|
|
329
|
-
if (action) {
|
|
330
|
-
containerRef.dispatch(action);
|
|
331
|
-
return true;
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
containerRef.dispatch(CommonActions.navigate({
|
|
336
|
-
name,
|
|
337
|
-
params
|
|
338
|
-
}));
|
|
339
|
-
return true;
|
|
340
|
-
} catch (err) {
|
|
341
|
-
console.error("Failed to navigate:", err);
|
|
342
|
-
return false;
|
|
343
|
-
}
|
|
344
|
-
}, [containerRef]);
|
|
345
|
-
const popToIndex = useCallback(index => {
|
|
346
|
-
if (index >= stack.length || index < 0) return;
|
|
347
|
-
const currentIndex = stack.length - 1;
|
|
348
|
-
const popCount = currentIndex - index;
|
|
349
|
-
if (popCount <= 0) return;
|
|
350
|
-
try {
|
|
351
|
-
const expoRouter = getSafeRouter();
|
|
352
|
-
if (expoRouter) {
|
|
353
|
-
for (let i = 0; i < popCount; i++) {
|
|
354
|
-
expoRouter.back();
|
|
355
|
-
}
|
|
356
|
-
} else if (containerRef) {
|
|
357
|
-
containerRef.dispatch(StackActions.pop(popCount));
|
|
358
|
-
}
|
|
359
|
-
} catch (err) {
|
|
360
|
-
console.error("Failed to pop:", err);
|
|
361
|
-
}
|
|
362
|
-
}, [stack, containerRef]);
|
|
363
|
-
const goBack = useCallback(() => {
|
|
364
|
-
if (isAtRoot) return;
|
|
365
|
-
try {
|
|
366
|
-
const expoRouter = getSafeRouter();
|
|
367
|
-
if (expoRouter) {
|
|
368
|
-
expoRouter.back();
|
|
369
|
-
} else if (containerRef) {
|
|
370
|
-
containerRef.goBack();
|
|
371
|
-
}
|
|
372
|
-
} catch (err) {
|
|
373
|
-
console.error("Failed to go back:", err);
|
|
374
|
-
}
|
|
375
|
-
}, [isAtRoot, containerRef]);
|
|
376
|
-
const popToTop = useCallback(() => {
|
|
377
|
-
if (isAtRoot) return;
|
|
378
|
-
try {
|
|
379
|
-
const expoRouter = getSafeRouter();
|
|
380
|
-
if (expoRouter) {
|
|
381
|
-
// Pop to index 0 via expo-router
|
|
382
|
-
popToIndex(0);
|
|
383
|
-
} else if (containerRef) {
|
|
384
|
-
containerRef.dispatch(StackActions.popToTop());
|
|
385
|
-
}
|
|
386
|
-
} catch (err) {
|
|
387
|
-
console.error("Failed to pop to top:", err);
|
|
388
|
-
}
|
|
389
|
-
}, [isAtRoot, containerRef, popToIndex]);
|
|
390
|
-
return {
|
|
391
|
-
stack,
|
|
392
|
-
focusedRoute,
|
|
393
|
-
stackDepth,
|
|
394
|
-
isAtRoot,
|
|
395
|
-
isLoaded,
|
|
396
|
-
error,
|
|
397
|
-
refresh,
|
|
398
|
-
navigateToIndex,
|
|
399
|
-
navigateToRoute,
|
|
400
|
-
popToIndex,
|
|
401
|
-
goBack,
|
|
402
|
-
popToTop
|
|
403
|
-
};
|
|
404
|
-
}
|
|
1
|
+
"use strict";import{useState as I,useEffect as L,useMemo as C,useCallback as S}from"react";import{getCommonActions as W,getStackActions as B,useSafeNavigationContainerRef as j}from"./utils/safeReactNavigation";import{getSafeRouter as b,isExpoRouterAvailable as w}from"@buoy-gg/shared-ui";const F=W();const E=B();function T(e,t){if(!e||!e.routes)return null;for(const o of e.routes){if(o.name===t){return[o.name]}if(o.state){const l=T(o.state,t);if(l){return[o.name,...l]}}}return null}function _(e){if(e.length===0)return null;if(e.length===1){return F.navigate({name:e[0]})}let t={screen:e[e.length-1]};for(let o=e.length-2;o>=1;o--){t={screen:e[o],params:t}}return F.navigate({name:e[0],params:t})}function $(e,t=0,o="",l=true){if(!e||!Array.isArray(e.routes)||e.routes.length===0){return[]}const g=[];const d=typeof e.index==="number"?e.index:NaN;const R=Number.isInteger(d)&&d>=0&&d<e.routes.length?d:e.routes.length-1;const a=typeof e.type==="string"?e.type:void 0;const r=a!=="tab"&&a!=="drawer";for(let i=0;i<e.routes.length;i++){if(!r&&i!==R)continue;const c=e.routes[i];const h=i===R;let u=c.name;if(c.name==="index"){u="/"}else if(!u.startsWith("/")){u=`/${u}`}if(o&&o!=="/"){u=`${o}${u}`}if(c.params){const y=Object.entries(c.params);if(y.length>0){u=u.replace(/\[([^\]]+)\]/g,(x,A)=>{return c.params[A]||x})}}const k=l&&h;const v=k&&c.state?$(c.state,t+1,u,true):[];g.push({key:c.key,name:c.name,path:u,params:c.params||{},state:c.state,isFocused:k&&v.length===0});g.push(...v)}return g}export function buildStack(e,t){if(!e)return[];try{const o=$(e);const l=t?o.filter(a=>{if(a.name.startsWith("__"))return false;if(a.name.includes("_layout"))return false;if(a.name.startsWith("+not-found"))return false;return true}):o;const g=l.length>0?l:o.slice(-1);const d=g.findIndex(a=>a.isFocused);const R=d>=0?d:g.length-1;return g.map((a,r)=>{let i=a.path||`/${a.name}`;if(t){i=i.replace(/^\/__root/,"")}if(!i||i===""){i="/"}return{key:a.key,name:a.name,pathname:i,params:a.params||{},isFocused:r===R,index:r,canPop:r>0}})}catch(o){console.error("Error transforming navigation state:",o);return[]}}export function useNavigationStack(){const e=w();const t=j();const[o,l]=I(()=>t?.getRootState?.()??null);const[g,d]=I(false);const[R,a]=I(null);L(()=>{if(!t)return;const n=t.getRootState?.();if(n){l(n)}d(true);const f=t.addListener?.("state",()=>{const s=t.getRootState?.();if(s){l(s)}});return()=>{f?.()}},[t]);const r=C(()=>buildStack(o,e),[o,e]);const i=C(()=>{return r.find(n=>n.isFocused)||null},[r]);const c=r.length;const h=c<=1;const u=S(()=>{if(!t)return;const n=t.getRootState?.();if(n){l(n)}},[t]);const k=S(n=>{if(n>=r.length||n<0)return;const f=r[n];if(!f)return;try{const s=b();if(s){s.navigate(f.pathname)}else if(t){const p=t.getRootState?.();if(p){const m=T(p,f.name);if(m){const N=_(m);if(N){t.dispatch(N);return}}}t.dispatch(F.navigate({name:f.name,params:f.params}))}}catch(s){console.error("Failed to navigate:",s)}},[r,t]);const v=S((n,f)=>{if(!t)return false;try{const s=t.getRootState?.();if(!f&&s){const p=T(s,n);if(p){const m=_(p);if(m){t.dispatch(m);return true}}}t.dispatch(F.navigate({name:n,params:f}));return true}catch(s){console.error("Failed to navigate:",s);return false}},[t]);const y=S(n=>{if(n>=r.length||n<0)return;const f=r.length-1;const s=f-n;if(s<=0)return;try{const p=b();if(p){for(let m=0;m<s;m++){p.back()}}else if(t){t.dispatch(E.pop(s))}}catch(p){console.error("Failed to pop:",p)}},[r,t]);const x=S(()=>{if(h)return;try{const n=b();if(n){n.back()}else if(t){t.goBack()}}catch(n){console.error("Failed to go back:",n)}},[h,t]);const A=S(()=>{if(h)return;try{const n=b();if(n){y(0)}else if(t){t.dispatch(E.popToTop())}}catch(n){console.error("Failed to pop to top:",n)}},[h,t,y]);return{stack:r,focusedRoute:i,stackDepth:c,isAtRoot:h,isLoaded:g,error:R,refresh:u,navigateToIndex:k,navigateToRoute:v,popToIndex:y,goBack:x,popToTop:A}}
|