@buoy-gg/route-events 7.0.35 → 7.0.38
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 +26 -8
- package/lib/commonjs/RouteTracker.js +10 -0
- package/lib/commonjs/components/RouteEventItemCompact.js +17 -35
- package/lib/commonjs/components/RouteEventsModalWithTabs.js +8 -0
- package/lib/commonjs/stores/routeEventStore.js +39 -0
- package/lib/commonjs/sync/routeEventsSyncAdapter.js +34 -8
- package/lib/commonjs/useNavigationStack.js +52 -14
- package/lib/module/RouteTracker.js +11 -0
- package/lib/module/components/RouteEventItemCompact.js +19 -37
- package/lib/module/components/RouteEventsModalWithTabs.js +8 -0
- package/lib/module/stores/routeEventStore.js +40 -1
- package/lib/module/sync/routeEventsSyncAdapter.js +34 -8
- package/lib/module/useNavigationStack.js +52 -15
- package/lib/typescript/RouteTracker.d.ts.map +1 -1
- package/lib/typescript/components/RouteEventItemCompact.d.ts.map +1 -1
- package/lib/typescript/components/RouteEventsModalWithTabs.d.ts.map +1 -1
- package/lib/typescript/stores/routeEventStore.d.ts +21 -0
- package/lib/typescript/stores/routeEventStore.d.ts.map +1 -1
- package/lib/typescript/sync/routeEventsSyncAdapter.d.ts +9 -0
- package/lib/typescript/sync/routeEventsSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/useNavigationStack.d.ts +8 -0
- package/lib/typescript/useNavigationStack.d.ts.map +1 -1
- package/package.json +6 -7
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@buoy-gg/route-events) [](https://www.npmjs.com/package/@buoy-gg/route-events)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Inspect observed navigation events, browse supported routes, and navigate using the parameters your app requires.
|
|
6
6
|
|
|
7
7
|
Part of [Buoy](https://github.com/Buoy-gg/buoy) — devtools that live inside your React Native app. Install it and it auto-appears in the floating menu from [`@buoy-gg/core`](https://www.npmjs.com/package/@buoy-gg/core).
|
|
8
8
|
|
|
@@ -14,6 +14,24 @@ Part of [Buoy](https://github.com/Buoy-gg/buoy) — devtools that live inside yo
|
|
|
14
14
|
npm install @buoy-gg/core @buoy-gg/route-events
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
## Before you start
|
|
18
|
+
|
|
19
|
+
Use a development build with `@buoy-gg/core` and a Free or Pro Buoy account key. From your app’s directory, sign in:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx --package=@buoy-gg/core buoy login
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
For Expo, initialize Buoy before rendering the menu:
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
import { Buoy } from "@buoy-gg/core";
|
|
29
|
+
|
|
30
|
+
Buoy.init({ licenseKey: process.env.EXPO_PUBLIC_BUOY_KEY });
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The login command writes the Expo key to `.env.local`. For React Native CLI, pass the key from your app’s environment configuration; React Native does not load `.env.local` automatically. Mount `FloatingDevTools` inside the same providers as your screens and restart the development server after installation. The [Quick Start](https://buoy.gg/buoy/latest/docs/quick-start) shows the complete root component setup.
|
|
34
|
+
|
|
17
35
|
## Quick start
|
|
18
36
|
|
|
19
37
|
```tsx
|
|
@@ -31,25 +49,25 @@ export default function RootLayout() {
|
|
|
31
49
|
}
|
|
32
50
|
```
|
|
33
51
|
|
|
34
|
-
|
|
52
|
+
After account setup, open Routes and navigate between two known screens. Check their paths and parameters. Keep your existing navigator and providers; supported data differs between Expo Router and React Navigation. The event duration is time since the previous navigation, not screen render time.
|
|
35
53
|
|
|
36
54
|
## What you get
|
|
37
55
|
|
|
38
|
-
- **
|
|
39
|
-
- **
|
|
56
|
+
- **Sitemap** — browse discovered routes, searchable, with route types (static, dynamic, layout) labeled
|
|
57
|
+
- **Navigate** — select a supported route and supply its required parameters. App navigation guards still apply
|
|
40
58
|
- **Live navigation stack** — watch the stack push, pop, and replace as you move through the app
|
|
41
59
|
- **Navigation event timeline** — every navigation logged with path, params, timestamp, and time since the previous one
|
|
42
60
|
- **Filtering & search** — filter events by pathname pattern, search across routes
|
|
43
|
-
- **
|
|
61
|
+
- **Tracker integration** — the core menu mounts the package’s route tracker
|
|
44
62
|
- **Hooks for custom UIs** — `useRouteEvents`, `useRouteSitemap`, and `useNavigationStack` if you want the data without the modal
|
|
45
63
|
|
|
46
64
|
## Desktop & AI
|
|
47
65
|
|
|
48
|
-
|
|
66
|
+
To connect a React Native app to [Buoy Desktop](https://github.com/Buoy-gg/Buoy-Desktop) or MCP, install `@buoy-gg/external-sync` and follow the [Desktop connection guide](https://buoy.gg/buoy/latest/docs/desktop). Sign in to Desktop separately. [MCP setup](https://buoy.gg/buoy/latest/docs/mcp) also requires a process account and Pro access. Available remote actions depend on the tool and app integration.
|
|
49
67
|
|
|
50
|
-
##
|
|
68
|
+
## Account and plan requirements
|
|
51
69
|
|
|
52
|
-
|
|
70
|
+
Use a Free or Pro Buoy account. History limits and paid features vary by tool; see [pricing](https://buoy.gg/pricing). Production access requires Pro where supported. Development-only hooks and actions remain unavailable in release builds.
|
|
53
71
|
|
|
54
72
|
---
|
|
55
73
|
|
|
@@ -10,6 +10,7 @@ var _useRouteObserver = require("./useRouteObserver");
|
|
|
10
10
|
var _useRouteObserverReactNavigation = require("./useRouteObserverReactNavigation");
|
|
11
11
|
var _useNavigationStack = require("./useNavigationStack");
|
|
12
12
|
var _navigationStackStore = require("./stores/navigationStackStore");
|
|
13
|
+
var _routeEventStore = require("./stores/routeEventStore");
|
|
13
14
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
14
15
|
/**
|
|
15
16
|
* RouteTracker - A component to place inside your navigation tree
|
|
@@ -52,11 +53,20 @@ var _jsxRuntime = require("react/jsx-runtime");
|
|
|
52
53
|
* ```
|
|
53
54
|
*/
|
|
54
55
|
|
|
56
|
+
// Mounted at boot by FloatingDevTools, so this is where capture starts —
|
|
57
|
+
// routeEventStore.hydrateBootCapture explains why the store needs it.
|
|
58
|
+
function useBootCapture() {
|
|
59
|
+
(0, _react.useEffect)(() => {
|
|
60
|
+
_routeEventStore.routeEventStore.hydrateBootCapture();
|
|
61
|
+
}, []);
|
|
62
|
+
}
|
|
55
63
|
function ExpoRouteTracker() {
|
|
64
|
+
useBootCapture();
|
|
56
65
|
(0, _useRouteObserver.useRouteObserver)();
|
|
57
66
|
return null;
|
|
58
67
|
}
|
|
59
68
|
function ReactNavigationRouteTracker() {
|
|
69
|
+
useBootCapture();
|
|
60
70
|
(0, _useRouteObserverReactNavigation.useRouteObserverReactNavigation)();
|
|
61
71
|
return null;
|
|
62
72
|
}
|
|
@@ -131,43 +131,30 @@ const RouteEventItemCompact = exports.RouteEventItemCompact = /*#__PURE__*/(0, _
|
|
|
131
131
|
});
|
|
132
132
|
}, [isExpanded, event, visitNumber, routeTemplate]);
|
|
133
133
|
|
|
134
|
-
//
|
|
135
|
-
|
|
134
|
+
// The Go button lives beside the row, not inside its touchable (see
|
|
135
|
+
// CompactRow.trailingAction), so a press never opens the row as well.
|
|
136
|
+
const goButton = (0, _react.useMemo)(() => {
|
|
136
137
|
if (!onNavigate) return undefined;
|
|
137
|
-
return /*#__PURE__*/(0, _jsxRuntime.
|
|
138
|
-
style: styles.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
onPress: e => {
|
|
148
|
-
e.stopPropagation();
|
|
149
|
-
onNavigate(event.pathname);
|
|
150
|
-
},
|
|
151
|
-
testID: `${testID}-go`,
|
|
152
|
-
accessibilityRole: "button",
|
|
153
|
-
accessibilityLabel: `Go to ${event.pathname}`,
|
|
154
|
-
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
155
|
-
style: styles.goButtonText,
|
|
156
|
-
children: "Go"
|
|
157
|
-
})
|
|
158
|
-
})]
|
|
138
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
|
|
139
|
+
style: styles.goButton,
|
|
140
|
+
onPress: () => onNavigate(event.pathname),
|
|
141
|
+
testID: `${testID}-go`,
|
|
142
|
+
accessibilityRole: "button",
|
|
143
|
+
accessibilityLabel: `Go to ${event.pathname}`,
|
|
144
|
+
children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
|
|
145
|
+
style: styles.goButtonText,
|
|
146
|
+
children: "Go"
|
|
147
|
+
})
|
|
159
148
|
});
|
|
160
|
-
}, [onNavigate, event.pathname,
|
|
149
|
+
}, [onNavigate, event.pathname, testID]);
|
|
161
150
|
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.CompactRow, {
|
|
162
151
|
statusDotColor: statusColor,
|
|
163
152
|
statusLabel: statusLabel,
|
|
164
153
|
statusSublabel: statusSublabel,
|
|
165
|
-
primaryText: event.pathname
|
|
166
|
-
|
|
167
|
-
,
|
|
168
|
-
badgeText: onNavigate ? undefined : badgeText,
|
|
154
|
+
primaryText: event.pathname,
|
|
155
|
+
badgeText: badgeText,
|
|
169
156
|
badgeColor: statusColor,
|
|
170
|
-
|
|
157
|
+
trailingAction: goButton,
|
|
171
158
|
bottomRightText: /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.RelativeTime, {
|
|
172
159
|
timestamp: event.timestamp
|
|
173
160
|
}),
|
|
@@ -183,11 +170,6 @@ const RouteEventItemCompact = exports.RouteEventItemCompact = /*#__PURE__*/(0, _
|
|
|
183
170
|
});
|
|
184
171
|
});
|
|
185
172
|
const styles = _reactNative.StyleSheet.create({
|
|
186
|
-
badgeContainer: {
|
|
187
|
-
flexDirection: "row",
|
|
188
|
-
alignItems: "center",
|
|
189
|
-
gap: 6
|
|
190
|
-
},
|
|
191
173
|
badgeText: {
|
|
192
174
|
fontSize: 11,
|
|
193
175
|
fontWeight: "600",
|
|
@@ -11,6 +11,7 @@ var _safeReactNavigation = require("../utils/safeReactNavigation");
|
|
|
11
11
|
var _sharedUi = require("@buoy-gg/shared-ui");
|
|
12
12
|
var _license = require("@buoy-gg/license");
|
|
13
13
|
var _useRouteEvents = require("../hooks/useRouteEvents");
|
|
14
|
+
var _routeEventStore = require("../stores/routeEventStore");
|
|
14
15
|
var _RouteEventDetail = require("./RouteEventDetail");
|
|
15
16
|
var _RouteFilterViewV = require("./RouteFilterViewV2");
|
|
16
17
|
var _RoutesSitemap = require("./RoutesSitemap");
|
|
@@ -138,6 +139,13 @@ function RouteEventsModalWithTabs({
|
|
|
138
139
|
|
|
139
140
|
// isListening = enabled and store is listening
|
|
140
141
|
const isListening = isListeningEnabled && isStoreListening;
|
|
142
|
+
|
|
143
|
+
// The switch above only governs THIS component's subscription; the store
|
|
144
|
+
// also holds one of its own from boot (routeEventStore.setBootCapture), so
|
|
145
|
+
// pausing here has to release that too or capture would never stop.
|
|
146
|
+
(0, _react.useEffect)(() => {
|
|
147
|
+
_routeEventStore.routeEventStore.setBootCapture(isListeningEnabled);
|
|
148
|
+
}, [isListeningEnabled]);
|
|
141
149
|
const [showFilters, setShowFilters] = (0, _react.useState)(false);
|
|
142
150
|
// Selected event drives the full-screen detail view (parity with the Events
|
|
143
151
|
// tool). null = list view. visitNumber feeds the "Visited N times" section.
|
|
@@ -40,6 +40,45 @@ var _RouteObserver = require("../RouteObserver");
|
|
|
40
40
|
class RouteEventStore extends _sharedUi.BaseEventStore {
|
|
41
41
|
// Unsubscribe function for routeObserver
|
|
42
42
|
routeObserverUnsubscribe = null;
|
|
43
|
+
// The store's own subscription — see setBootCapture.
|
|
44
|
+
bootCaptureRelease = null;
|
|
45
|
+
bootCaptureHydrated = false;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Capture from app launch, not from the first open of the tool.
|
|
49
|
+
*
|
|
50
|
+
* A BaseEventStore only listens while something subscribes to it, and for
|
|
51
|
+
* routes that was the open modal or a watching desktop — so every
|
|
52
|
+
* navigation before the tool was first opened was simply never recorded,
|
|
53
|
+
* and the Events tab opened on "No route events yet" after a session of
|
|
54
|
+
* navigating. (It looked fine on any machine with Buoy Desktop running,
|
|
55
|
+
* because the desktop's watch subscribed at boot.) Network and storage both
|
|
56
|
+
* capture from launch; this holds a subscription of the store's own so
|
|
57
|
+
* routes does too.
|
|
58
|
+
*
|
|
59
|
+
* The hold follows the tool's persisted monitoring switch: the modal calls
|
|
60
|
+
* this on every toggle, and `hydrateBootCapture` reads the persisted value
|
|
61
|
+
* at boot so a paused tool stays paused across a relaunch.
|
|
62
|
+
*/
|
|
63
|
+
setBootCapture(enabled) {
|
|
64
|
+
if (enabled && !this.bootCaptureRelease) {
|
|
65
|
+
this.bootCaptureRelease = this.subscribeToEvents(() => {});
|
|
66
|
+
} else if (!enabled && this.bootCaptureRelease) {
|
|
67
|
+
this.bootCaptureRelease();
|
|
68
|
+
this.bootCaptureRelease = null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Arm boot capture from the persisted monitoring switch (default on). */
|
|
73
|
+
hydrateBootCapture() {
|
|
74
|
+
if (this.bootCaptureHydrated) return;
|
|
75
|
+
this.bootCaptureHydrated = true;
|
|
76
|
+
void _sharedUi.persistentStorage.getItem(_sharedUi.devToolsStorageKeys.routeEvents.isMonitoring()).then(raw => {
|
|
77
|
+
// The modal may already have decided while the read was in flight.
|
|
78
|
+
if (this.bootCaptureRelease) return;
|
|
79
|
+
this.setBootCapture(raw !== "false");
|
|
80
|
+
}).catch(() => this.setBootCapture(true));
|
|
81
|
+
}
|
|
43
82
|
constructor() {
|
|
44
83
|
super({
|
|
45
84
|
storeName: "route-events",
|
|
@@ -131,7 +131,21 @@ const routeEventsSyncAdapter = exports.routeEventsSyncAdapter = {
|
|
|
131
131
|
// BaseEventStore is newest-FIRST, and the store only captures while
|
|
132
132
|
// something is subscribed.
|
|
133
133
|
const latest = _routeEventStore.routeEventStore.getEvents()[0];
|
|
134
|
-
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* A RECORDED event only wins while the recorder is running.
|
|
137
|
+
*
|
|
138
|
+
* The store starts capturing when its first subscriber arrives and stops
|
|
139
|
+
* with the last one, so an app with Ask Buoy on the phone and no
|
|
140
|
+
* dashboard attached has NO route events at all — the stack fallback
|
|
141
|
+
* below is its normal path, not the fresh-launch edge case this code was
|
|
142
|
+
* written for. Worse, events left over from a closed capture window are
|
|
143
|
+
* stale the moment the user navigates again: a live QA session read
|
|
144
|
+
* `/(tabs)/menu` from an event six scenarios after the app had moved to
|
|
145
|
+
* `/cart`. The live stack cannot be stale, so it wins whenever nothing
|
|
146
|
+
* is recording.
|
|
147
|
+
*/
|
|
148
|
+
if (latest && _routeEventStore.routeEventStore.isCapturing()) {
|
|
135
149
|
return {
|
|
136
150
|
path: latest.pathname,
|
|
137
151
|
params: toWireParams(latest.params),
|
|
@@ -139,13 +153,12 @@ const routeEventsSyncAdapter = exports.routeEventsSyncAdapter = {
|
|
|
139
153
|
source: "event"
|
|
140
154
|
};
|
|
141
155
|
}
|
|
142
|
-
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const focused = _navigationStackStore.navigationStackStore.getStack().find(item => item.isFocused) ?? _navigationStackStore.navigationStackStore.getStack().slice(-1)[0];
|
|
156
|
+
|
|
157
|
+
// The live stack knows even when the history is empty, which is the
|
|
158
|
+
// difference between a macro that can run from anywhere and one that
|
|
159
|
+
// only works if you happen to be on the screen it was recorded from.
|
|
160
|
+
const stack = _navigationStackStore.navigationStackStore.getStack();
|
|
161
|
+
const focused = stack.find(item => item.isFocused) ?? stack[stack.length - 1];
|
|
149
162
|
if (focused?.pathname) {
|
|
150
163
|
return {
|
|
151
164
|
path: focused.pathname,
|
|
@@ -154,6 +167,19 @@ const routeEventsSyncAdapter = exports.routeEventsSyncAdapter = {
|
|
|
154
167
|
source: "stack"
|
|
155
168
|
};
|
|
156
169
|
}
|
|
170
|
+
|
|
171
|
+
// No live stack (no <RouteTracker/>) and the recorder is off: an old
|
|
172
|
+
// event is all there is. Say that it may be stale rather than passing it
|
|
173
|
+
// off as where the app is.
|
|
174
|
+
if (latest) {
|
|
175
|
+
return {
|
|
176
|
+
path: latest.pathname,
|
|
177
|
+
params: toWireParams(latest.params),
|
|
178
|
+
at: latest.timestamp,
|
|
179
|
+
source: "event",
|
|
180
|
+
stale: true
|
|
181
|
+
};
|
|
182
|
+
}
|
|
157
183
|
return {
|
|
158
184
|
path: null,
|
|
159
185
|
params: {},
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
+
exports.buildStack = buildStack;
|
|
6
7
|
exports.useNavigationStack = useNavigationStack;
|
|
7
8
|
var _react = require("react");
|
|
8
9
|
var _safeReactNavigation = require("./utils/safeReactNavigation");
|
|
@@ -85,14 +86,39 @@ function buildNestedNavigateAction(path) {
|
|
|
85
86
|
// ============================================================================
|
|
86
87
|
|
|
87
88
|
/**
|
|
88
|
-
* Recursively collect
|
|
89
|
+
* Recursively collect the routes from the navigation state tree.
|
|
90
|
+
*
|
|
91
|
+
* FOCUS COMES FROM `state.index`, NEVER FROM POSITION. React Navigation names
|
|
92
|
+
* the focused route by index within `routes`; the array's order is declaration
|
|
93
|
+
* order. `buildStack` used to equate the two — "the last route in the collected
|
|
94
|
+
* list is the focused one" — and a hidden tab declared last (`href: null` on an
|
|
95
|
+
* expo-router `Tabs.Screen`) then became the reported current screen for the
|
|
96
|
+
* whole session. Measured on a real app: Home and Rewards both reported as a
|
|
97
|
+
* hidden Menu tab, in every turn of a 55-scenario QA run, because `menu` was
|
|
98
|
+
* declared last.
|
|
99
|
+
*
|
|
100
|
+
* TAB AND DRAWER SIBLINGS ARE NOT A STACK. They are alternative destinations
|
|
101
|
+
* that happen to be mounted at the same time; `canPop` and `popToIndex` never
|
|
102
|
+
* meant anything for them. Only the focused branch of a tab/drawer is
|
|
103
|
+
* collected, so what comes back is the stack you could actually pop.
|
|
89
104
|
*/
|
|
90
|
-
function collectRoutesFromState(state, depth = 0, parentPath = "") {
|
|
91
|
-
if (!state || !state.routes)
|
|
105
|
+
function collectRoutesFromState(state, depth = 0, parentPath = "", onFocusedChain = true) {
|
|
106
|
+
if (!state || !Array.isArray(state.routes) || state.routes.length === 0) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
92
109
|
const routes = [];
|
|
110
|
+
|
|
111
|
+
// An absent or out-of-range `index` is incomplete state (mid-rehydration, a
|
|
112
|
+
// partially built child). Fall back to the last route — a stack's top — and
|
|
113
|
+
// never to an arbitrary sibling.
|
|
114
|
+
const declaredIndex = typeof state.index === "number" ? state.index : NaN;
|
|
115
|
+
const focusedIndex = Number.isInteger(declaredIndex) && declaredIndex >= 0 && declaredIndex < state.routes.length ? declaredIndex : state.routes.length - 1;
|
|
116
|
+
const navigatorType = typeof state.type === "string" ? state.type : undefined;
|
|
117
|
+
const siblingsAreStack = navigatorType !== "tab" && navigatorType !== "drawer";
|
|
93
118
|
for (let i = 0; i < state.routes.length; i++) {
|
|
119
|
+
if (!siblingsAreStack && i !== focusedIndex) continue;
|
|
94
120
|
const route = state.routes[i];
|
|
95
|
-
const isFocused = i ===
|
|
121
|
+
const isFocused = i === focusedIndex;
|
|
96
122
|
|
|
97
123
|
// Build the path
|
|
98
124
|
let pathname = route.name;
|
|
@@ -116,25 +142,30 @@ function collectRoutesFromState(state, depth = 0, parentPath = "") {
|
|
|
116
142
|
});
|
|
117
143
|
}
|
|
118
144
|
}
|
|
145
|
+
|
|
146
|
+
// Descend the focused branch first: whatever it yields owns the focus, and
|
|
147
|
+
// this route only keeps it when nothing deeper claimed it.
|
|
148
|
+
const onChain = onFocusedChain && isFocused;
|
|
149
|
+
const nestedRoutes = onChain && route.state ? collectRoutesFromState(route.state, depth + 1, pathname, true) : [];
|
|
119
150
|
routes.push({
|
|
120
151
|
key: route.key,
|
|
121
152
|
name: route.name,
|
|
122
153
|
path: pathname,
|
|
123
154
|
params: route.params || {},
|
|
124
|
-
state: route.state
|
|
155
|
+
state: route.state,
|
|
156
|
+
isFocused: onChain && nestedRoutes.length === 0
|
|
125
157
|
});
|
|
126
|
-
|
|
127
|
-
// If this route has nested state and is focused, recurse
|
|
128
|
-
if (isFocused && route.state) {
|
|
129
|
-
const nestedRoutes = collectRoutesFromState(route.state, depth + 1, pathname);
|
|
130
|
-
routes.push(...nestedRoutes);
|
|
131
|
-
}
|
|
158
|
+
routes.push(...nestedRoutes);
|
|
132
159
|
}
|
|
133
160
|
return routes;
|
|
134
161
|
}
|
|
135
162
|
|
|
136
163
|
/**
|
|
137
|
-
* Transform raw navigation state into display items
|
|
164
|
+
* Transform raw navigation state into display items.
|
|
165
|
+
*
|
|
166
|
+
* Exported for tests: the focus rule below is the one thing every route answer
|
|
167
|
+
* in the product rests on, and it was previously only reachable by extracting
|
|
168
|
+
* the function from source with the TypeScript AST.
|
|
138
169
|
*/
|
|
139
170
|
function buildStack(navigationState, isExpo) {
|
|
140
171
|
if (!navigationState) return [];
|
|
@@ -153,7 +184,14 @@ function buildStack(navigationState, isExpo) {
|
|
|
153
184
|
// If we filtered everything out, just keep the last route
|
|
154
185
|
const finalRoutes = filteredRoutes.length > 0 ? filteredRoutes : routes.slice(-1);
|
|
155
186
|
|
|
156
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Focus survives filtering. It is decided in `collectRoutesFromState` from
|
|
189
|
+
* `state.index`; the only case that falls back to the last item is one
|
|
190
|
+
* where the filter dropped the focused route itself (an internal expo
|
|
191
|
+
* layout), which is the one situation the old rule was ever right for.
|
|
192
|
+
*/
|
|
193
|
+
const collectedFocus = finalRoutes.findIndex(route => route.isFocused);
|
|
194
|
+
const focusIndex = collectedFocus >= 0 ? collectedFocus : finalRoutes.length - 1;
|
|
157
195
|
return finalRoutes.map((route, index) => {
|
|
158
196
|
let cleanPath = route.path || `/${route.name}`;
|
|
159
197
|
// Clean up Expo Router internal /__root prefix
|
|
@@ -169,7 +207,7 @@ function buildStack(navigationState, isExpo) {
|
|
|
169
207
|
name: route.name,
|
|
170
208
|
pathname: cleanPath,
|
|
171
209
|
params: route.params || {},
|
|
172
|
-
isFocused: index ===
|
|
210
|
+
isFocused: index === focusIndex,
|
|
173
211
|
index,
|
|
174
212
|
canPop: index > 0
|
|
175
213
|
};
|
|
@@ -47,12 +47,23 @@ import { useRouteObserver } from "./useRouteObserver";
|
|
|
47
47
|
import { useRouteObserverReactNavigation } from "./useRouteObserverReactNavigation";
|
|
48
48
|
import { useNavigationStack } from "./useNavigationStack";
|
|
49
49
|
import { navigationStackStore } from "./stores/navigationStackStore";
|
|
50
|
+
import { routeEventStore } from "./stores/routeEventStore";
|
|
51
|
+
|
|
52
|
+
// Mounted at boot by FloatingDevTools, so this is where capture starts —
|
|
53
|
+
// routeEventStore.hydrateBootCapture explains why the store needs it.
|
|
50
54
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
55
|
+
function useBootCapture() {
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
routeEventStore.hydrateBootCapture();
|
|
58
|
+
}, []);
|
|
59
|
+
}
|
|
51
60
|
function ExpoRouteTracker() {
|
|
61
|
+
useBootCapture();
|
|
52
62
|
useRouteObserver();
|
|
53
63
|
return null;
|
|
54
64
|
}
|
|
55
65
|
function ReactNavigationRouteTracker() {
|
|
66
|
+
useBootCapture();
|
|
56
67
|
useRouteObserverReactNavigation();
|
|
57
68
|
return null;
|
|
58
69
|
}
|
|
@@ -7,14 +7,14 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { memo, useMemo } from "react";
|
|
10
|
-
import {
|
|
10
|
+
import { Text, TouchableOpacity, StyleSheet } from "react-native";
|
|
11
11
|
import { CompactRow, RelativeTime, buoyColors, night, monoFont } from "@buoy-gg/shared-ui";
|
|
12
12
|
import { RouteEventExpandedContent } from "./RouteEventExpandedContent";
|
|
13
13
|
import { getRouteTemplate } from "../utils/routeTemplate";
|
|
14
14
|
import { eventRowTestID } from "../utils/testIDs";
|
|
15
15
|
|
|
16
16
|
// Route type for color coding
|
|
17
|
-
import { jsx as _jsx
|
|
17
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
18
18
|
// Get route type for color coding
|
|
19
19
|
function getRouteType(event) {
|
|
20
20
|
if (event.pathname === "/") {
|
|
@@ -127,43 +127,30 @@ export const RouteEventItemCompact = /*#__PURE__*/memo(function RouteEventItemCo
|
|
|
127
127
|
});
|
|
128
128
|
}, [isExpanded, event, visitNumber, routeTemplate]);
|
|
129
129
|
|
|
130
|
-
//
|
|
131
|
-
|
|
130
|
+
// The Go button lives beside the row, not inside its touchable (see
|
|
131
|
+
// CompactRow.trailingAction), so a press never opens the row as well.
|
|
132
|
+
const goButton = useMemo(() => {
|
|
132
133
|
if (!onNavigate) return undefined;
|
|
133
|
-
return /*#__PURE__*/
|
|
134
|
-
style: styles.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
onPress: e => {
|
|
144
|
-
e.stopPropagation();
|
|
145
|
-
onNavigate(event.pathname);
|
|
146
|
-
},
|
|
147
|
-
testID: `${testID}-go`,
|
|
148
|
-
accessibilityRole: "button",
|
|
149
|
-
accessibilityLabel: `Go to ${event.pathname}`,
|
|
150
|
-
children: /*#__PURE__*/_jsx(Text, {
|
|
151
|
-
style: styles.goButtonText,
|
|
152
|
-
children: "Go"
|
|
153
|
-
})
|
|
154
|
-
})]
|
|
134
|
+
return /*#__PURE__*/_jsx(TouchableOpacity, {
|
|
135
|
+
style: styles.goButton,
|
|
136
|
+
onPress: () => onNavigate(event.pathname),
|
|
137
|
+
testID: `${testID}-go`,
|
|
138
|
+
accessibilityRole: "button",
|
|
139
|
+
accessibilityLabel: `Go to ${event.pathname}`,
|
|
140
|
+
children: /*#__PURE__*/_jsx(Text, {
|
|
141
|
+
style: styles.goButtonText,
|
|
142
|
+
children: "Go"
|
|
143
|
+
})
|
|
155
144
|
});
|
|
156
|
-
}, [onNavigate, event.pathname,
|
|
145
|
+
}, [onNavigate, event.pathname, testID]);
|
|
157
146
|
return /*#__PURE__*/_jsx(CompactRow, {
|
|
158
147
|
statusDotColor: statusColor,
|
|
159
148
|
statusLabel: statusLabel,
|
|
160
149
|
statusSublabel: statusSublabel,
|
|
161
|
-
primaryText: event.pathname
|
|
162
|
-
|
|
163
|
-
,
|
|
164
|
-
badgeText: onNavigate ? undefined : badgeText,
|
|
150
|
+
primaryText: event.pathname,
|
|
151
|
+
badgeText: badgeText,
|
|
165
152
|
badgeColor: statusColor,
|
|
166
|
-
|
|
153
|
+
trailingAction: goButton,
|
|
167
154
|
bottomRightText: /*#__PURE__*/_jsx(RelativeTime, {
|
|
168
155
|
timestamp: event.timestamp
|
|
169
156
|
}),
|
|
@@ -179,11 +166,6 @@ export const RouteEventItemCompact = /*#__PURE__*/memo(function RouteEventItemCo
|
|
|
179
166
|
});
|
|
180
167
|
});
|
|
181
168
|
const styles = StyleSheet.create({
|
|
182
|
-
badgeContainer: {
|
|
183
|
-
flexDirection: "row",
|
|
184
|
-
alignItems: "center",
|
|
185
|
-
gap: 6
|
|
186
|
-
},
|
|
187
169
|
badgeText: {
|
|
188
170
|
fontSize: 11,
|
|
189
171
|
fontWeight: "600",
|
|
@@ -15,6 +15,7 @@ import { useIsPro } from "@buoy-gg/license";
|
|
|
15
15
|
// Free tier limit for route events
|
|
16
16
|
|
|
17
17
|
import { useRouteEvents } from "../hooks/useRouteEvents";
|
|
18
|
+
import { routeEventStore } from "../stores/routeEventStore";
|
|
18
19
|
import { RouteEventDetail } from "./RouteEventDetail";
|
|
19
20
|
import { RouteFilterViewV2 } from "./RouteFilterViewV2";
|
|
20
21
|
import { RoutesSitemap } from "./RoutesSitemap";
|
|
@@ -135,6 +136,13 @@ export function RouteEventsModalWithTabs({
|
|
|
135
136
|
|
|
136
137
|
// isListening = enabled and store is listening
|
|
137
138
|
const isListening = isListeningEnabled && isStoreListening;
|
|
139
|
+
|
|
140
|
+
// The switch above only governs THIS component's subscription; the store
|
|
141
|
+
// also holds one of its own from boot (routeEventStore.setBootCapture), so
|
|
142
|
+
// pausing here has to release that too or capture would never stop.
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
routeEventStore.setBootCapture(isListeningEnabled);
|
|
145
|
+
}, [isListeningEnabled]);
|
|
138
146
|
const [showFilters, setShowFilters] = useState(false);
|
|
139
147
|
// Selected event drives the full-screen detail view (parity with the Events
|
|
140
148
|
// tool). null = list view. visitNumber feeds the "Visited N times" section.
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
-
import { BaseEventStore } from "@buoy-gg/shared-ui";
|
|
24
|
+
import { BaseEventStore, devToolsStorageKeys, persistentStorage } from "@buoy-gg/shared-ui";
|
|
25
25
|
import { routeObserver } from "../RouteObserver";
|
|
26
26
|
|
|
27
27
|
// Re-export types for convenience
|
|
@@ -37,6 +37,45 @@ import { routeObserver } from "../RouteObserver";
|
|
|
37
37
|
class RouteEventStore extends BaseEventStore {
|
|
38
38
|
// Unsubscribe function for routeObserver
|
|
39
39
|
routeObserverUnsubscribe = null;
|
|
40
|
+
// The store's own subscription — see setBootCapture.
|
|
41
|
+
bootCaptureRelease = null;
|
|
42
|
+
bootCaptureHydrated = false;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Capture from app launch, not from the first open of the tool.
|
|
46
|
+
*
|
|
47
|
+
* A BaseEventStore only listens while something subscribes to it, and for
|
|
48
|
+
* routes that was the open modal or a watching desktop — so every
|
|
49
|
+
* navigation before the tool was first opened was simply never recorded,
|
|
50
|
+
* and the Events tab opened on "No route events yet" after a session of
|
|
51
|
+
* navigating. (It looked fine on any machine with Buoy Desktop running,
|
|
52
|
+
* because the desktop's watch subscribed at boot.) Network and storage both
|
|
53
|
+
* capture from launch; this holds a subscription of the store's own so
|
|
54
|
+
* routes does too.
|
|
55
|
+
*
|
|
56
|
+
* The hold follows the tool's persisted monitoring switch: the modal calls
|
|
57
|
+
* this on every toggle, and `hydrateBootCapture` reads the persisted value
|
|
58
|
+
* at boot so a paused tool stays paused across a relaunch.
|
|
59
|
+
*/
|
|
60
|
+
setBootCapture(enabled) {
|
|
61
|
+
if (enabled && !this.bootCaptureRelease) {
|
|
62
|
+
this.bootCaptureRelease = this.subscribeToEvents(() => {});
|
|
63
|
+
} else if (!enabled && this.bootCaptureRelease) {
|
|
64
|
+
this.bootCaptureRelease();
|
|
65
|
+
this.bootCaptureRelease = null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Arm boot capture from the persisted monitoring switch (default on). */
|
|
70
|
+
hydrateBootCapture() {
|
|
71
|
+
if (this.bootCaptureHydrated) return;
|
|
72
|
+
this.bootCaptureHydrated = true;
|
|
73
|
+
void persistentStorage.getItem(devToolsStorageKeys.routeEvents.isMonitoring()).then(raw => {
|
|
74
|
+
// The modal may already have decided while the read was in flight.
|
|
75
|
+
if (this.bootCaptureRelease) return;
|
|
76
|
+
this.setBootCapture(raw !== "false");
|
|
77
|
+
}).catch(() => this.setBootCapture(true));
|
|
78
|
+
}
|
|
40
79
|
constructor() {
|
|
41
80
|
super({
|
|
42
81
|
storeName: "route-events",
|
|
@@ -129,7 +129,21 @@ export const routeEventsSyncAdapter = {
|
|
|
129
129
|
// BaseEventStore is newest-FIRST, and the store only captures while
|
|
130
130
|
// something is subscribed.
|
|
131
131
|
const latest = routeEventStore.getEvents()[0];
|
|
132
|
-
|
|
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()) {
|
|
133
147
|
return {
|
|
134
148
|
path: latest.pathname,
|
|
135
149
|
params: toWireParams(latest.params),
|
|
@@ -137,13 +151,12 @@ export const routeEventsSyncAdapter = {
|
|
|
137
151
|
source: "event"
|
|
138
152
|
};
|
|
139
153
|
}
|
|
140
|
-
|
|
141
|
-
//
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const focused = navigationStackStore.getStack().find(item => item.isFocused) ?? navigationStackStore.getStack().slice(-1)[0];
|
|
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];
|
|
147
160
|
if (focused?.pathname) {
|
|
148
161
|
return {
|
|
149
162
|
path: focused.pathname,
|
|
@@ -152,6 +165,19 @@ export const routeEventsSyncAdapter = {
|
|
|
152
165
|
source: "stack"
|
|
153
166
|
};
|
|
154
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
|
+
}
|
|
155
181
|
return {
|
|
156
182
|
path: null,
|
|
157
183
|
params: {},
|
|
@@ -82,14 +82,39 @@ function buildNestedNavigateAction(path) {
|
|
|
82
82
|
// ============================================================================
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
|
-
* Recursively collect
|
|
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.
|
|
86
100
|
*/
|
|
87
|
-
function collectRoutesFromState(state, depth = 0, parentPath = "") {
|
|
88
|
-
if (!state || !state.routes)
|
|
101
|
+
function collectRoutesFromState(state, depth = 0, parentPath = "", onFocusedChain = true) {
|
|
102
|
+
if (!state || !Array.isArray(state.routes) || state.routes.length === 0) {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
89
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";
|
|
90
114
|
for (let i = 0; i < state.routes.length; i++) {
|
|
115
|
+
if (!siblingsAreStack && i !== focusedIndex) continue;
|
|
91
116
|
const route = state.routes[i];
|
|
92
|
-
const isFocused = i ===
|
|
117
|
+
const isFocused = i === focusedIndex;
|
|
93
118
|
|
|
94
119
|
// Build the path
|
|
95
120
|
let pathname = route.name;
|
|
@@ -113,27 +138,32 @@ function collectRoutesFromState(state, depth = 0, parentPath = "") {
|
|
|
113
138
|
});
|
|
114
139
|
}
|
|
115
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) : [];
|
|
116
146
|
routes.push({
|
|
117
147
|
key: route.key,
|
|
118
148
|
name: route.name,
|
|
119
149
|
path: pathname,
|
|
120
150
|
params: route.params || {},
|
|
121
|
-
state: route.state
|
|
151
|
+
state: route.state,
|
|
152
|
+
isFocused: onChain && nestedRoutes.length === 0
|
|
122
153
|
});
|
|
123
|
-
|
|
124
|
-
// If this route has nested state and is focused, recurse
|
|
125
|
-
if (isFocused && route.state) {
|
|
126
|
-
const nestedRoutes = collectRoutesFromState(route.state, depth + 1, pathname);
|
|
127
|
-
routes.push(...nestedRoutes);
|
|
128
|
-
}
|
|
154
|
+
routes.push(...nestedRoutes);
|
|
129
155
|
}
|
|
130
156
|
return routes;
|
|
131
157
|
}
|
|
132
158
|
|
|
133
159
|
/**
|
|
134
|
-
* Transform raw navigation state into display items
|
|
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.
|
|
135
165
|
*/
|
|
136
|
-
function buildStack(navigationState, isExpo) {
|
|
166
|
+
export function buildStack(navigationState, isExpo) {
|
|
137
167
|
if (!navigationState) return [];
|
|
138
168
|
try {
|
|
139
169
|
const routes = collectRoutesFromState(navigationState);
|
|
@@ -150,7 +180,14 @@ function buildStack(navigationState, isExpo) {
|
|
|
150
180
|
// If we filtered everything out, just keep the last route
|
|
151
181
|
const finalRoutes = filteredRoutes.length > 0 ? filteredRoutes : routes.slice(-1);
|
|
152
182
|
|
|
153
|
-
|
|
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;
|
|
154
191
|
return finalRoutes.map((route, index) => {
|
|
155
192
|
let cleanPath = route.path || `/${route.name}`;
|
|
156
193
|
// Clean up Expo Router internal /__root prefix
|
|
@@ -166,7 +203,7 @@ function buildStack(navigationState, isExpo) {
|
|
|
166
203
|
name: route.name,
|
|
167
204
|
pathname: cleanPath,
|
|
168
205
|
params: route.params || {},
|
|
169
|
-
isFocused: index ===
|
|
206
|
+
isFocused: index === focusIndex,
|
|
170
207
|
index,
|
|
171
208
|
canPop: index > 0
|
|
172
209
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RouteTracker.d.ts","sourceRoot":"","sources":["../../src/RouteTracker.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;
|
|
1
|
+
{"version":3,"file":"RouteTracker.d.ts","sourceRoot":"","sources":["../../src/RouteTracker.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AA4DH,wBAAgB,YAAY,gCAQ3B"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RouteEventItemCompact.d.ts","sourceRoot":"","sources":["../../../src/components/RouteEventItemCompact.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAKzD,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,gBAAgB,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB;0DACsD;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AA6FD,eAAO,MAAM,qBAAqB,
|
|
1
|
+
{"version":3,"file":"RouteEventItemCompact.d.ts","sourceRoot":"","sources":["../../../src/components/RouteEventItemCompact.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAKH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAKzD,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,gBAAgB,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB;0DACsD;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AA6FD,eAAO,MAAM,qBAAqB,kEA6EhC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RouteEventsModalWithTabs.d.ts","sourceRoot":"","sources":["../../../src/components/RouteEventsModalWithTabs.tsx"],"names":[],"mappings":"AA0DA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AAGnE,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,KAAK,IAAI,CAAC;IACvC,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE;YACR,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;YACrB,MAAM,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,IAAI,CAAC;YAChC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;SAC/B,CAAC;QACF,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAC;QAC3B;;;;WAIG;QACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;QAC7C;;;;;WAKG;QACH,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;QAC5C,2EAA2E;QAC3E,aAAa,CAAC,EAAE,CACd,MAAM,EAAE,yBAAyB,EACjC,OAAO,CAAC,EAAE;YAAE,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,KACzB,IAAI,CAAC;KACX,CAAC;CACH;AAgED,wBAAgB,wBAAwB,CAAC,EACvC,OAAO,EACP,OAAO,EACP,MAAM,EACN,UAAU,EACV,2BAAmC,EACnC,MAAc,EACd,MAAM,GACP,EAAE,6BAA6B,
|
|
1
|
+
{"version":3,"file":"RouteEventsModalWithTabs.d.ts","sourceRoot":"","sources":["../../../src/components/RouteEventsModalWithTabs.tsx"],"names":[],"mappings":"AA0DA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AAGnE,MAAM,WAAW,6BAA6B;IAC5C,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,GAAG,KAAK,IAAI,CAAC;IACvC,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;OAKG;IACH,MAAM,CAAC,EAAE;QACP,OAAO,CAAC,EAAE;YACR,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;YACrB,MAAM,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,IAAI,CAAC;YAChC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;SAC/B,CAAC;QACF,KAAK,CAAC,EAAE,gBAAgB,EAAE,CAAC;QAC3B;;;;WAIG;QACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;QAC7C;;;;;WAKG;QACH,cAAc,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;QAC5C,2EAA2E;QAC3E,aAAa,CAAC,EAAE,CACd,MAAM,EAAE,yBAAyB,EACjC,OAAO,CAAC,EAAE;YAAE,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,KACzB,IAAI,CAAC;KACX,CAAC;CACH;AAgED,wBAAgB,wBAAwB,CAAC,EACvC,OAAO,EACP,OAAO,EACP,MAAM,EACN,UAAU,EACV,2BAAmC,EACnC,MAAc,EACd,MAAM,GACP,EAAE,6BAA6B,sCA4mB/B;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,KAAK,GAAE,IAAI,CAAC,6BAA6B,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAM,+BAKlF"}
|
|
@@ -31,6 +31,27 @@ export type RouteEventListener = (events: RouteChangeEvent[]) => void;
|
|
|
31
31
|
export type RouteEventCallback = (event: RouteChangeEvent) => void;
|
|
32
32
|
declare class RouteEventStore extends BaseEventStore<RouteChangeEvent> {
|
|
33
33
|
private routeObserverUnsubscribe;
|
|
34
|
+
private bootCaptureRelease;
|
|
35
|
+
private bootCaptureHydrated;
|
|
36
|
+
/**
|
|
37
|
+
* Capture from app launch, not from the first open of the tool.
|
|
38
|
+
*
|
|
39
|
+
* A BaseEventStore only listens while something subscribes to it, and for
|
|
40
|
+
* routes that was the open modal or a watching desktop — so every
|
|
41
|
+
* navigation before the tool was first opened was simply never recorded,
|
|
42
|
+
* and the Events tab opened on "No route events yet" after a session of
|
|
43
|
+
* navigating. (It looked fine on any machine with Buoy Desktop running,
|
|
44
|
+
* because the desktop's watch subscribed at boot.) Network and storage both
|
|
45
|
+
* capture from launch; this holds a subscription of the store's own so
|
|
46
|
+
* routes does too.
|
|
47
|
+
*
|
|
48
|
+
* The hold follows the tool's persisted monitoring switch: the modal calls
|
|
49
|
+
* this on every toggle, and `hydrateBootCapture` reads the persisted value
|
|
50
|
+
* at boot so a paused tool stays paused across a relaunch.
|
|
51
|
+
*/
|
|
52
|
+
setBootCapture(enabled: boolean): void;
|
|
53
|
+
/** Arm boot capture from the persisted monitoring switch (default on). */
|
|
54
|
+
hydrateBootCapture(): void;
|
|
34
55
|
constructor();
|
|
35
56
|
/**
|
|
36
57
|
* Start listening to route changes from routeObserver
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routeEventStore.d.ts","sourceRoot":"","sources":["../../../src/stores/routeEventStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"routeEventStore.d.ts","sourceRoot":"","sources":["../../../src/stores/routeEventStore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,EACL,cAAc,EAGf,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAiB,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGxE,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEzD;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,gBAAgB,EAAE,KAAK,IAAI,CAAC;AAEtE;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,KAAK,EAAE,gBAAgB,KAAK,IAAI,CAAC;AAEnE,cAAM,eAAgB,SAAQ,cAAc,CAAC,gBAAgB,CAAC;IAE5D,OAAO,CAAC,wBAAwB,CAA6B;IAE7D,OAAO,CAAC,kBAAkB,CAA6B;IACvD,OAAO,CAAC,mBAAmB,CAAS;IAEpC;;;;;;;;;;;;;;;OAeG;IACH,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAStC,0EAA0E;IAC1E,kBAAkB,IAAI,IAAI;;IAoB1B;;OAEG;IACH,SAAS,CAAC,cAAc,IAAI,IAAI;IAQhC;;OAEG;IACH,SAAS,CAAC,aAAa,IAAI,IAAI;IAO/B;;OAEG;IACH,WAAW,IAAI,OAAO;IAKtB,WAAW,IAAI,OAAO;CAGvB;AAGD,eAAO,MAAM,eAAe,iBAAwB,CAAC;AAGrD,eAAO,MAAM,sBAAsB,GAAI,UAAU,kBAAkB,eACtB,CAAC;AAC9C,eAAO,MAAM,YAAY,GAAI,UAAU,kBAAkB,eACtB,CAAC;AACpC,eAAO,MAAM,cAAc,0BAAoC,CAAC;AAChE,eAAO,MAAM,gBAAgB,YAAsC,CAAC;AACpE,eAAO,MAAM,gBAAgB,eAAsC,CAAC"}
|
|
@@ -66,16 +66,25 @@ export declare const routeEventsSyncAdapter: {
|
|
|
66
66
|
params: Record<string, string | string[]>;
|
|
67
67
|
at: number;
|
|
68
68
|
source: "event";
|
|
69
|
+
stale?: undefined;
|
|
69
70
|
} | {
|
|
70
71
|
path: string;
|
|
71
72
|
params: Record<string, any>;
|
|
72
73
|
at: null;
|
|
73
74
|
source: "stack";
|
|
75
|
+
stale?: undefined;
|
|
76
|
+
} | {
|
|
77
|
+
path: string;
|
|
78
|
+
params: Record<string, string | string[]>;
|
|
79
|
+
at: number;
|
|
80
|
+
source: "event";
|
|
81
|
+
stale: true;
|
|
74
82
|
} | {
|
|
75
83
|
path: null;
|
|
76
84
|
params: {};
|
|
77
85
|
at: null;
|
|
78
86
|
source: null;
|
|
87
|
+
stale?: undefined;
|
|
79
88
|
};
|
|
80
89
|
/**
|
|
81
90
|
* Navigate the device to a concrete path. The dashboard resolves any
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"routeEventsSyncAdapter.d.ts","sourceRoot":"","sources":["../../../src/sync/routeEventsSyncAdapter.ts"],"names":[],"mappings":"AAGA,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAG9D;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,QAAY,CAAC;AA4BrD;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,6EAA6E;IAC7E,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,mEAAmE;IACnE,MAAM,EAAE,OAAO,GAAG,KAAK,GAAG,IAAI,CAAC;IAC/B,6EAA6E;IAC7E,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,OAAO,EAAE,oBAAoB,CAAC;IAC9B,8EAA8E;IAC9E,KAAK,EAAE,gBAAgB,EAAE,CAAC;CAC3B;AAWD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,sBAAsB;;uBAEhB,sBAAsB;0BAKjB,MAAM,IAAI;;;QAkC9B;;;;;;;;WAQG
|
|
1
|
+
{"version":3,"file":"routeEventsSyncAdapter.d.ts","sourceRoot":"","sources":["../../../src/sync/routeEventsSyncAdapter.ts"],"names":[],"mappings":"AAGA,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAE7D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAG9D;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,QAAY,CAAC;AA4BrD;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,6EAA6E;IAC7E,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,mEAAmE;IACnE,MAAM,EAAE,OAAO,GAAG,KAAK,GAAG,IAAI,CAAC;IAC/B,6EAA6E;IAC7E,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED;;;GAGG;AACH,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,gBAAgB,EAAE,CAAC;IAC3B,OAAO,EAAE,oBAAoB,CAAC;IAC9B,8EAA8E;IAC9E,KAAK,EAAE,gBAAgB,EAAE,CAAC;CAC3B;AAWD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,sBAAsB;;uBAEhB,sBAAsB;0BAKjB,MAAM,IAAI;;;QAkC9B;;;;;;;;WAQG;;;;;;;;;;;;;;;;;;;;;;;;;;QAyDH;;;WAGG;4BACiB,OAAO;;;;;;;;;wCAsCK,OAAO;;;mCAUZ,OAAO;;;;;;;;;;CAuBrC,CAAC"}
|
|
@@ -38,6 +38,14 @@ export interface UseNavigationStackResult {
|
|
|
38
38
|
goBack: () => void;
|
|
39
39
|
popToTop: () => void;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Transform raw navigation state into display items.
|
|
43
|
+
*
|
|
44
|
+
* Exported for tests: the focus rule below is the one thing every route answer
|
|
45
|
+
* in the product rests on, and it was previously only reachable by extracting
|
|
46
|
+
* the function from source with the TypeScript AST.
|
|
47
|
+
*/
|
|
48
|
+
export declare function buildStack(navigationState: any, isExpo: boolean): StackDisplayItem[];
|
|
41
49
|
/**
|
|
42
50
|
* Access the current navigation stack.
|
|
43
51
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useNavigationStack.d.ts","sourceRoot":"","sources":["../../src/useNavigationStack.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;
|
|
1
|
+
{"version":3,"file":"useNavigationStack.d.ts","sourceRoot":"","sources":["../../src/useNavigationStack.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAiCH,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC5B,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,wBAAwB;IAEvC,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,YAAY,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACtC,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAGlB,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAGpB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,eAAe,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC;;;;;OAKG;IACH,eAAe,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;IAC5D,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAqJD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,eAAe,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,GAAG,gBAAgB,EAAE,CAwDpF;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,kBAAkB,IAAI,wBAAwB,CAiM7D"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@buoy-gg/route-events",
|
|
3
|
-
"version": "7.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "7.0.38",
|
|
4
|
+
"description": "Inspect reported navigation state and supported route actions in React Native apps using Expo Router or React Navigation.",
|
|
5
5
|
"main": "lib/commonjs/index.js",
|
|
6
6
|
"module": "lib/module/index.js",
|
|
7
7
|
"types": "lib/typescript/index.d.ts",
|
|
@@ -26,12 +26,11 @@
|
|
|
26
26
|
],
|
|
27
27
|
"sideEffects": false,
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@buoy-gg/floating-tools-core": "^7.0.
|
|
30
|
-
"@buoy-gg/shared-ui": "^7.0.
|
|
29
|
+
"@buoy-gg/floating-tools-core": "^7.0.38",
|
|
30
|
+
"@buoy-gg/shared-ui": "^7.0.38"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@buoy-gg/license": "7.0.
|
|
34
|
-
"@react-native-async-storage/async-storage": "^2.0.0 || ^3.1.0",
|
|
33
|
+
"@buoy-gg/license": "7.0.38",
|
|
35
34
|
"@react-navigation/native": "*",
|
|
36
35
|
"expo-router": "*",
|
|
37
36
|
"react": "*",
|
|
@@ -52,7 +51,7 @@
|
|
|
52
51
|
"@types/react-native": "^0.73.0",
|
|
53
52
|
"expo-router": "~5.0.7",
|
|
54
53
|
"typescript": "~5.8.3",
|
|
55
|
-
"@buoy-gg/license": "^7.0.
|
|
54
|
+
"@buoy-gg/license": "^7.0.38"
|
|
56
55
|
},
|
|
57
56
|
"react-native-builder-bob": {
|
|
58
57
|
"source": "src",
|